Overview
Fortalis is a multiplayer strategy game I'm building from scratch — auth infrastructure, game backend, and client are all my own code. The idea is a castle-building strategy game where players manage resources, form alliances, and fight each other on a shared world map, similar to titles like Travian or Lords Mobile but with a backend architecture I actually want to work with.
The project has three components:
- ▸Auth Service — standalone Spring Boot 4 authentication backend, open source
- ▸Game Backend — Spring Modulith server with 8 domain modules, private repo
- ▸Game Client — Unity, targeting Android, iOS, Web, and PC from one codebase
Auth service
The auth service is its own deployable, separate from the game backend. It owns all account data and token issuance. Game backends never see passwords — they just fetch the JWKS endpoint and verify tokens locally.
The stack is Spring Boot 4, Java 21, PostgreSQL, and Flyway for migrations. BouncyCastle handles the RSA key operations. Security headers are hardened out of the box (HSTS, Content-Security-Policy: default-src 'none', X-Frame-Options DENY).
How authentication works:
- ▸User registers with email and password (hashed with Argon2id)
- ▸On login, the service issues a 15-minute RS256-signed JWT access token and an opaque refresh token (30-day TTL)
- ▸Refresh tokens are stored hashed (SHA-256) in PostgreSQL, rotated on every use, and individually revocable
- ▸If the user has TOTP 2FA enabled, login is two-step:
/auth/login/startreturns a 5-minute challenge ticket, then/auth/login/completetakes the ticket plus the TOTP code. TOTP secrets are encrypted at rest with AES-256-GCM. Each account also gets 10 hashed backup codes for recovery. - ▸Game backends verify access tokens by fetching the public key from
/.well-known/jwks.json
The JWT carries iss, sub (account UUID), aud, exp, and an mfa boolean claim so backends know whether the session was MFA-verified.
Rate limiting runs per-IP (20 requests per 60 seconds), per-email (5 login attempts per 15 minutes), and per-ticket (10 MFA attempts per 15 minutes). Rate-limited responses include Retry-After headers.
All error responses follow RFC 7807 Problem Details with typed error URIs like https://auth.fortalis.game/errors/invalid-credentials, proper HTTP status codes, and machine-readable structure. There are 13 distinct error types covering validation, auth failures, rate limiting, and conflict cases.
The identity model is built to support Google/Apple OAuth later — the account_identity table already has a provider + subject structure. Email verification and password reset are also planned.
Game backend
The game backend is a Spring Modulith modular monolith — one deployable JAR, but with hard module boundaries that Spring Modulith enforces at test time. The idea: develop like a monolith now, extract into microservices later if the player count demands it. Built with Spring Boot 4, Java 21, PostgreSQL, Redis, and Spring Modulith 2.0.2.
Each module follows the same internal structure: api/ (controllers and DTOs), domain/ (entities), repository/ (data access), and service/ (business logic).
| Module | What it does |
|---|---|
player | Registration, profiles, guest auth via device binding, account linking |
castle | Main castles, outposts, building placement, resource production, troop queues |
alliance | Alliance creation, membership management, role-based permissions, shared resources |
combat | March dispatching, battle resolution, report generation |
map | World map with kingdoms, special buildings, and map objects |
mail | Unified mailbox — DMs, battle reports, system announcements, claimable rewards |
chat | Real-time chat over WebSocket with global, alliance, kingdom, and event channels |
event | Timed game events with individual and alliance scoring |
Modules communicate through Spring's ApplicationEventPublisher — loose coupling by design. When combat resolves a battle, it publishes a BattleCompletedEvent; the mail module picks it up and sends the battle report. No module calls another's internals directly.
Authentication integrates with the auth service via JWKS: the backend fetches the public key and verifies RS256 tokens locally, no direct service call needed. Guest players authenticate through device binding and can later link a full account without losing progress.
A game loop scheduler runs timed ticks: build queue completion every 5 seconds, march arrival every 2 seconds (combat needs to feel responsive), resource production every 60 seconds (lazy-calculated on read), and map cleanup every 5 minutes. Game config and special building rules are stored as JSONB for flexibility.
Real-time updates go over WebSocket (STOMP protocol with SockJS fallback). Database migrations use Flyway. Redis is available for caching. The whole thing runs on AWS with Docker.
The backend has Testcontainers-based integration tests that spin up PostgreSQL for each test run. Spring Modulith's test infrastructure verifies that module boundaries are respected — no module reaches into another's internals.
Game client
The client is being built in Unity (C#), targeting Android, iOS, Web, and PC from one codebase. It connects to the game backend over REST and WebSocket, and authenticates through the auth service. Currently in early development.