Fortalis: Multiplayer Strategy Game

2025 – Present

Java 21Spring Boot 4Spring ModulithJWT/JWKSPostgreSQLRedisFlywayDockerAWSREST APIsWebSocketUnityC#
GitHub
Fortalis: Multiplayer Strategy Game

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:

  1. User registers with email and password (hashed with Argon2id)
  2. On login, the service issues a 15-minute RS256-signed JWT access token and an opaque refresh token (30-day TTL)
  3. Refresh tokens are stored hashed (SHA-256) in PostgreSQL, rotated on every use, and individually revocable
  4. If the user has TOTP 2FA enabled, login is two-step: /auth/login/start returns a 5-minute challenge ticket, then /auth/login/complete takes 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.
  5. 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).

ModuleWhat it does
playerRegistration, profiles, guest auth via device binding, account linking
castleMain castles, outposts, building placement, resource production, troop queues
allianceAlliance creation, membership management, role-based permissions, shared resources
combatMarch dispatching, battle resolution, report generation
mapWorld map with kingdoms, special buildings, and map objects
mailUnified mailbox — DMs, battle reports, system announcements, claimable rewards
chatReal-time chat over WebSocket with global, alliance, kingdom, and event channels
eventTimed 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.