The FAF Message Broker is a high-performance gateway and message routing service for the Forged Alliance Forever (FAF) platform. The service is built to be the central handler of all messaging between clients and backend services.
Built on Quarkus and Java 25 with virtual threads, it bridges real-time client WebSocket connections with backend RabbitMQ messaging infrastructure, enforcing JSON schema validation and JWT authentication.
- Overview & Architecture
- Key Features
- Tech Stack
- Messaging & Communication Flow
- Configuration Reference
- Database & Migrations
- Getting Started
- Related Guides
faf-message-broker acts as the edge communication broker between FAF game/lobby clients and backend server
microservices:
- Client Connectivity: Manages client lifecycle, session establishment, user-agent detection, and keepalive pings over WebSockets.
- Authentication & Policy Validation: Verifies client JWT tokens against Ory Hydra JWKS and optionally checks client connections against a policy server.
- Dynamic Schema Validation & Error Reporting: Enforces strict JSON Schema (Draft 2020-12) validation for all
inbound lobby commands and GPG game passthrough messages before dispatching them downstream. Invalid messages or
unregistered commands return an immediate
failed_messageresponse to the client. - Active Player & Session Tracking: Maintains player-to-session mappings, emits periodic session heartbeats, and answers active player queries from backend microservices via RabbitMQ.
- Targeted Message Distribution: Delivers outbound messages from backend services to individual sessions, specific players across sessions, or broadcast to all active connections.
+--------------------+ WebSocket (Port 8003) +------------------------+
| FAF Client / Game | <=================================> | faf-message-broker |
+--------------------+ +------------------------+
| | |
+------------------------------------+ | +-----------------------+
| (JWT & Policy Verification) | (Schema Persistence) | (Event Brokering)
v v v
+-------------------+ +-----------------+ +-----------------+
| Hydra / Policy API| | PostgreSQL (DB) | | RabbitMQ Broker |
+-------------------+ +-----------------+ +-----------------+
- Virtual Thread Concurrency: Utilizes Java 25 virtual threads (
@RunOnVirtualThread) for scalable connection handling and asynchronous message draining. - Asynchronous Session Message Queues:
- Bounded per-session incoming and outgoing queues (
ArrayBlockingQueue) drained asynchronously on virtual threads. - Prevents blocking on slow network clients or broker dispatch and tracks queue metrics and dropped messages.
- Bounded per-session incoming and outgoing queues (
- Bi-directional Passthrough & Routing:
- Inbound lobby and GPG (
target: game) messages validated and published to RabbitMQ. - Inbound messages failing validation or lacking registered schemas trigger a structured
failed_messageclient response. - Outbound messages routed by target criteria:
Session(sessionId),Player(playerId), orAll().
- Inbound lobby and GPG (
- Dynamic & Persisted Schema Registry:
- Requests registered schemas on startup from backend services via RabbitMQ (
lobby-schema-request). - Automatically compiles and registers schema definitions on startup.
- Dynamically registers new schemas via RabbitMQ (
lobby-schema-registration). - Persists schemas in PostgreSQL (
lobby_command_schematable) via Flyway migrations.
- Requests registered schemas on startup from backend services via RabbitMQ (
- Player Session & State Tracking:
- Emits real-time connection lifecycle events (
new,existing,gone) with TTL expiry to RabbitMQ (player-connection). - Responds to on-demand active player session requests via RabbitMQ (
player-active-request) using RPC reply-to routing.
- Emits real-time connection lifecycle events (
- Zero-Downtime Session Management: Thread-safe concurrent session maps tracking player-to-session relationships.
- Strict Code Quality & Null Safety: Enforced with Error Prone and NullAway in JSpecify mode.
- Java: 25
- Framework: Quarkus 4.x
- Messaging: SmallRye Reactive Messaging (RabbitMQ Connector)
- WebSockets: Quarkus WebSockets Next
- Database & Persistence: PostgreSQL, Hibernate ORM with Panache, Flyway
- Authentication: SmallRye JWT with Ory Hydra JWKS validation
- Schema Validation: NetworkNT JSON Schema Validator & Victools JSON Schema Generator
- Build System: Gradle (Kotlin DSL)
- Path:
/(Default port:8003) - Connection Handshake & Lifecycle:
ask_session(Message.Connection.SessionRequest): Client sends agent and version, receives a generatedsessionID (Message.Connection.SessionResponse).auth(Message.Connection.AuthenticateRequest): Client submits JWT token and unique machine ID. Token is parsed to obtainplayerId, verified with Hydra, and checked against the policy server (if enabled).ping/pong: Bi-directional heartbeat keepalive messages.- Passthrough Messages: Inbound commands (
LobbyorGPG) routed after authentication. Inbound messages are validated against registered JSON schemas before dispatching downstream. - Validation Error Reporting (
failed_message): If an inbound message fails schema validation or specifies an unknown command, the broker rejects the message and sends afailed_message(ProtocolMessage.Outgoing) directly back to the client session over WebSocket:Registered lobby schemas dynamically include an optional{ "command": "failed_message", "request_id": "<request_id_from_client_or_null>", "message": "Invalid message received", "message_key": "message.invalid" }request_idproperty, allowing clients to correlate validation error responses with specific outbound requests.
- Session Message Queuing & Draining:
- Inbound Queue: Incoming passthrough messages are placed into a bounded blocking queue (
ArrayBlockingQueue) per session. Once the session is authenticated, messages are drained asynchronously on a virtual thread (drainExecutor) and forwarded toMessageBroker. - Outbound Queue: Outgoing protocol messages sent to the session are buffered in a bounded queue and drained asynchronously on a dedicated virtual thread directly to the WebSocket connection.
- Backpressure & Capacity Limits: The queue capacity is configured via
faf.max-message-queue-size(default:100). If a queue is full, new messages are dropped and tracked via thefaf.messages.droppedcounter. - Session Teardown: When a session closes or disconnects, remaining queued messages are drained before the connection is terminated.
- Inbound Queue: Incoming passthrough messages are placed into a bounded blocking queue (
| Channel | Direction | VHost | Target Exchange / Queue | Description |
|---|---|---|---|---|
lobby-inbound |
Outgoing (Broker → RabbitMQ) | /faf-core |
Exchange: lobby-message-inbound |
Transmits validated inbound client messages with routing keys matching lobby.<route>.<command> or gpg.<command>. |
lobby-outbound |
Incoming (RabbitMQ → Broker) | /faf-core |
Exchange: lobby-message-outboundQueue: broker.message.outbound.${faf.instance-id} |
Receives outbound messages targeted to a session (Session), player (Player), or broadcast (All). |
lobby-schema-request |
Outgoing (Broker → RabbitMQ) | /faf-core |
Exchange: lobby-schema-request |
Dispatches a schema request on broker startup with reply_to pointing to the instance's schema registration queue (broker.schema.registration.${faf.instance-id}). |
lobby-schema-registration |
Incoming (RabbitMQ → Broker) | /faf-core |
Exchange: lobby-schema-registerQueue: broker.schema.registration.${faf.instance-id} |
Receives runtime schema registrations to dynamically update validation rules and persist them to PostgreSQL. |
player-active-request |
Incoming (RabbitMQ → Broker) | /faf-core |
Exchange: lobby-session-requestQueue: broker.session.request.${faf.instance-id} |
Receives queries for all currently active player sessions. The broker responds by publishing current player connection states with reply-to routing. |
player-connection |
Outgoing (Broker → RabbitMQ) | /faf-core |
Exchange: lobby-session-state |
Broadcasts player session lifecycle states (new, existing keepalives / query responses, and gone) with TTL expiry to backend services. |
- Connection Lifecycle Events:
new: Emitted viaplayer-connection(to exchangelobby-session-state) when a player authenticates and establishes a new session. Includes TTL expiry duration (faf.player-session-status-refresh-period * 2).gone: Emitted viaplayer-connectionwhen a session disconnects or is unregistered.existing: Periodically broadcast viaplayer-connectionas a keepalive heartbeat for all active sessions on a scheduled interval (faf.player-session-status-refresh-period, default1m).
- Active Player Query (RPC):
- Backend services can request an immediate snapshot of active players by publishing a message to exchange
lobby-session-request(player-active-request) with a RabbitMQreplyToqueue header. - The broker handles the request by iterating over all active player sessions and publishing
existingstate messages addressed to the requester'sreplyTorouting key viaplayer-connection.
- Backend services can request an immediate snapshot of active players by publishing a message to exchange
The application is configured via src/main/resources/application.yml and supports the following environment variables:
| Variable | Description | Default |
|---|---|---|
DATABASE_HOST |
PostgreSQL host | localhost |
DATABASE_PORT |
PostgreSQL port | 5432 |
DATABASE_NAME |
Database name | faf-messages |
DATABASE_USER |
Database user | faf-message-broker |
DATABASE_PASS |
Database password | banana |
RABBITMQ_HOST |
RabbitMQ host | localhost |
RABBITMQ_PORT |
RabbitMQ port | 5672 |
RABBITMQ_USER |
RabbitMQ user | faf-message-broker |
RABBITMQ_PASSWORD |
RabbitMQ password | banana |
RABBITMQ_VHOST |
RabbitMQ virtual host | /faf-core |
ALLOWED_HOST |
Allowed HTTP/WebSocket host header validation | ws.faforever.localhost,localhost |
HYDRA_TOKEN_ISSUER |
Expected JWT issuer | http://ory-hydra:4444 |
HYDRA_JWKS_URL |
Ory Hydra JWKS public key endpoint | http://localhost:4444/.well-known/jwks.json |
USE_POLICY_SERVER |
Enable or disable verification with policy server | false |
POLICY_SERVER_URL |
Base URL of the policy verification service | http://localhost:8097 |
HOSTNAME |
Instance identifier (faf.instance-id) used for unique RabbitMQ queue names |
local |
STATUS_REFRESH_PERIOD |
Periodic refresh interval for active player session status broadcasts (faf.player-session-status-refresh-period) |
1m |
REAL_IP_HEADER |
HTTP header used to extract real client IP from WebSocket connection (faf.real-ip-header) |
X-Real-IP |
MAX_MESSAGE_QUEUE_SIZE |
Maximum message queue capacity for inbound and outbound session queues (faf.max-message-queue-size) |
100 |
PUBLISH_UNKNOWN_MESSAGES |
Forward unknown inbound messages without registered schemas (faf.publish-unknown-messages) |
false |
UNKNOWN_MESSAGE_ROUTE |
Route key used when forwarding unknown inbound messages (faf.unknown-message-route) |
legacy |
The service uses PostgreSQL for storing registered command schemas. Flyway runs automatically on startup:
V1__lobby_command_schema.sql: Createslobby_command_schematable:command(VARCHAR): Command identifier.direction(VARCHAR):inboundoroutbound.schema(JSONB): JSON Schema Draft 2020-12 specification.
- Java JDK: 25 or higher
- PostgreSQL: 14+ (or compatible container)
- RabbitMQ: 3.9+ with
/faf-corevirtual host configured - Ory Hydra: (for JWT authentication verification)
For local development and orchestrating all supporting backend services (PostgreSQL, RabbitMQ, Hydra, etc.), you can use the FAForever gitops-stack Tilt setup.
The dev config is setup to run as a local service using Tilt via gitops-stack
Run the application in Quarkus development mode with live reload:
./gradlew quarkusDev(On Windows PowerShell: .\gradlew.bat quarkusDev)
In dev mode, traffic logging for WebSockets and SQL statement logging are enabled by default.
To execute unit and integration tests:
./gradlew testTo build the executable application package:
./gradlew buildDockerfiles for various deployment models are available under src/main/docker/:
Dockerfile.jvm: Standard OpenJDK container image.Dockerfile.legacy-jar: Thin jar packaging.Dockerfile.native: Native binary container image.Dockerfile.native-micro: Minimalist micro image for native executable.
- Quarkus RabbitMQ Connector Guide: Reactive messaging with RabbitMQ.
- Quarkus WebSockets Next Reference: WebSocket API documentation.
- Quarkus YAML Configuration: YAML configuration in Quarkus.
- Quarkus Datasource / PostgreSQL Guide: Connecting to PostgreSQL via JDBC.
- Hibernate ORM with Panache: Persistence code simplified for Hibernate ORM.