Shared Runtime Architecture: Game Backend
Shared Runtime Architecture: Game Backend
This guide explains the shared runtime around a game package: how a pack starts, how HTTP requests reach a game plugin, where RGS money/state operations happen, and how the response returns to the client. It is aimed at developers new to the repository who need to know which layer owns a change.
The guide follows Pack Alpha and the shared libraries as they are currently implemented. Individual game rules and the meaning of each game's featureData live in the game code walkthrough; Carpathian gameplay rules are in the gameplay guide.
1. Runtime at a Glance
flowchart TD
A[Pack main.ts] --> B[PlatformBootstrap]
B --> C[AppModule registers game plugins]
C --> D[GameCoreModule]
D --> E[GamePluginLoaderService loads plugins]
F[Client request] --> G[Global interceptors and validation]
G --> H[GameController]
H --> I[GameService]
I --> J[RgsService and HttpClient]
I --> K[Selected game plugin]
K --> L[Game engine and game logic]
L --> M[SDK helpers and math JSON]
J --> N[RGS]
I --> O[ApiResponseDto]
O --> P[Client response]
The key boundary is that a game plugin calculates game outcomes; the shared core handles HTTP, player sessions, debit/credit, round persistence, and response assembly. Plugins implement the SDK contract as pure TypeScript and do not call RGS themselves.
2. Startup and Plugin Registration
Pack entry point
apps/pack-alpha/src/main.ts creates PlatformBootstrap and passes it the root AppModule and service name. apps/pack-alpha/src/app.module.ts constructs the game's plugin instances and calls GameCoreModule.register(plugins).
GameCoreModule.register(plugins)
The dynamic module wires AppConfigModule, monitoring, RgsModule, the plugin loader, GameService, and GameController. It provides the plugin array under the GAME_PLUGINS injection token. Packs can extend the controller/service or supply additional bootstrap middleware when they need pack-specific behavior.
GamePluginLoaderService
| Function | Responsibility |
|---|---|
onModuleInit() | Iterates registered plugins, awaits each plugin.onLoad() so its math is ready, injects the monitoring logger when setLogger exists, and stores the plugin under gameId. |
getPlugin(gameId) | Returns the registered plugin or raises a not-found server error. Used by GameService for each request. |
getAvailableGameIds() | Returns registered IDs for the health endpoint. |
onModuleDestroy() | Calls optional onUnload() hooks and clears the plugin map. |
Most games support unfinished-round recovery by default. GameService checks plugin.supportsUnfinished ?? true during init; a game can set it to false when it does not support that flow, as Royale81 currently does.
3. HTTP Bootstrap Pipeline
PlatformBootstrap.run(options) creates a NestJS Fastify application and installs the platform-wide behavior once for the pack. Game plugins do not install these per-game.
- Fastify is configured with a 50 MB default body limit, CORS, and shutdown hooks.
- Global interceptors are added:
AsyncSessionInterceptor, optionalDecryptionInterceptor, optionalResponseInterceptor, then any pack-supplied interceptors. HttpExceptionFiltermaps thrown errors into the shared error response shape.- A global
ValidationPipetransforms DTO values and strips properties not declared by the DTO (whitelist: true). - The server listens on the configured
APP_PORT.
| Component | What it does |
|---|---|
AsyncSessionInterceptor.getMeta() | Reads/creates the request ID, captures request metadata, and places it in async-local storage for correlated logs and downstream RGS headers. Health endpoints are skipped. |
AsyncSessionInterceptor.intercept() | Creates the request context, logs start/end and duration, and records errors before rethrowing them. |
DecryptionInterceptor.intercept() | At a high level, attempts to decrypt non-empty JSON request bodies unless disabled. Its cryptographic algorithm and hash/key flow are intentionally deferred to the security guide. |
ResponseInterceptor.intercept() | Wraps a raw successful controller return value in ApiResponseDto; if the controller already returned an ApiResponseDto, it passes that through. |
HttpExceptionFilter.catch() | Converts HTTP, platform-rich, and unexpected errors into a consistent error response and HTTP status. |
HttpExceptionFilter.buildExtensions() | Copies client-safe error details and includes internal fields only when SHOW_ERROR_INTERNALS=true. |
ValidationPipe | Applies DTO transformations and validation before the service handles input. Invalid fields are reported as NotValidClientError. |
PlatformBootstrap supports disableDecryption, disableResponseWrapper, custom interceptors, filters, and pipes. DISABLE_DECRYPTION is an app-level setting; it is not a game math option.
4. Controller and Core Responsibilities
GameController
The controller exposes /games/health, /games/init, /games/spin, and /games/feature. Each game endpoint passes a validated DTO to GameService. The controller uses ApiResponseDto.success for successful init/spin/feature responses; the global response interceptor recognizes the wrapper and does not nest another one.
GameService
GameService is the transaction/orchestration boundary between DTOs, RGS, and plugins.
| Function | Main work |
|---|---|
healthCheck() | Returns status: 'ok' and the loaded game IDs. |
init(input) | Resolves the plugin, calls RGS init, derives devMode, invokes plugin.init({ gameMode }), maps player information, and optionally loads an unfinished round. |
spin(input) | Creates a round ID, selects the bet type, debits through RGS, builds trusted plugin input, calls plugin.spin, then updates an open feature round or credits a completed spin. |
feature(input) | Loads the prior round from RGS, validates that it is still open and that the plugin supports features, calls plugin.feature, then updates or credits the round based on featureComplete. |
Important ownership rules:
gameId,token, andepochare core/RGS concerns; they are not passed to the game engine as game rules.gameMode,betAmount,devMode, RGSmaxWin, and game-specificextraDataare selected/mapped by the core and sent through the SDK plugin input.devModecomes from RGSallowCheat, not from a client-supplied boolean.progressDatais trusted only from RGS. On spin, the service overwrites any client-providedextraData.progressDatabefore the plugin call.isRecord(gameData)checks plugin output before the core persists or returns it.- The core trims feature
spinResultsto the latest entry before persistence/response. Feature implementations must not assume the full history survives every request.
Transaction sequences
Init:
HTTP DTO -> GameService.init -> RGS /init -> plugin.init -> optional RGS last-spin lookup -> IInitResponse -> ApiResponseDto
Spin:
HTTP DTO -> GameService.spin -> RGS /bet -> plugin.spin -> RGS /updateRound (feature remains open) OR RGS /win (round completed) -> ISpinResponse -> ApiResponseDto
Feature:
HTTP DTO -> GameService.feature -> RGS /lastSpinDetails -> plugin.feature -> RGS /updateRound (feature continues) OR RGS /win (feature complete) -> IFeatureResponse -> ApiResponseDto
The game plugin never performs the debit or credit. Its output values such as totalWin, featureTriggered, and featureComplete tell the core which settlement path to take.
5. RGS Client and HTTP Transport
RgsService
| Function | Responsibility |
|---|---|
onModuleInit() | Creates one shared HttpClient configured with the RGS base URL, timeout, pool, retry policy, JSON headers, and a request-ID header interceptor. |
onModuleDestroy() | Closes the HTTP client's connection pool. |
init(request) | Posts the session token and device type to RGS /init. |
loadLastSpin(request) | Reads persisted round state from /lastSpinDetails, optionally using epoch. |
debit(request) | Posts a wager to /bet; retries are disabled because it is a monetary operation. |
credit(request) | Posts win settlement to /win; retries are disabled. RGS returns an array and the client maps the first item into its local response type. |
updateResult(request) | Saves the current open-round state to /updateRound; retries are enabled because this is an idempotent upsert. |
validateToken(token, context) | Rejects an empty token before making an RGS call. |
postRgs(...) | Logs and sends one RGS request, then delegates failures to the error mapper. |
handleRgsError(...) | Converts HTTP-client and unexpected failures to platform ServerError values while preserving appropriate safe details. |
HttpClient
The shared client wraps Undici. get, post, put, patch, and delete delegate to request; request merges config and applies retry behavior around doRequest; doRequest runs the request interceptor, sends the request, validates status, parses JSON, then runs the response interceptor. close() releases an internally created pool. rawRequest() is an escape hatch that does not apply retries and requires callers to consume the response body.
RgsService adds the current request ID from async-local storage to outgoing RGS headers. The HTTP-client pool/timeout/retry options are configuration; game-specific math does not belong there.
6. Shared Types and Response Ownership
The SDK is the shared, leaf-level contract used by every game package. ISlotGamePlugin defines lifecycle methods and generic init/spin/feature inputs/outputs. Generic game data is intentionally opaque to GameService: the core validates that it is object-shaped, then passes/persists it without calculating game rules.
| Type/function | Owner and responsibility |
|---|---|
IInitInput / IInitOutput | SDK plugin boundary for selected gameMode, paytable, and optional init gameData. |
ISpinInput / ISpinOutput | SDK contract for a base spin and its win/feature summaries. |
IFeatureInput / IFeatureOutput | SDK contract for resuming a feature from previousSpinResult; output uses featureComplete. |
IInitResponse / ISpinResponse / IFeatureResponse | SDK shapes for the data portion returned by shared core endpoints. |
IRGSInitResponse | SDK shape for raw RGS session data before the core maps it for a client. |
getPlayerInfo(playerData) | SDK mapping from raw RGS player fields to a frontend-friendly player object. |
ApiResponseDto.success(...) / error(...) | Shared HTTP envelope in shared-nestjs; wraps the endpoint data or structured error extensions. |
Do not confuse the three response layers:
- Game plugin output:
totalWin,featureTriggered/featureComplete, and game-specificgameData. - Game API data:
gameId,roundId,closed,result,playerInfo, and optionalpromoInfo/betType. - HTTP envelope:
success,statusCode,message, anddata(or errorextensions).
The game-specific feature entry and featureData details are cataloged in the code walkthrough's feature reference.
7. Configuration: Similar Names, Different Owners
| Setting/data | Owner | Purpose |
|---|---|---|
Game mathModes / gameMode | Game plugin + RGS session | Selects a game math file/cache entry, such as Carpathian R4. |
payTable, reels, weight tables, layout | Game math JSON | Determines game-specific outcomes and scoring. |
preferences, opCnf, rtp, promoInfo, epoch | RGS init/session response | Player/operator/session information returned through init; keys inside operator preferences are externally defined. |
APP_PORT, NODE_ENV, DISABLE_DECRYPTION | Shared app configuration | Controls service process/bootstrap behavior. |
RGS_URL, pool settings, request timeout, retry settings | RGS client configuration | Controls outbound service communication. |
gameId, token, roundId, bet/feature fields | HTTP/core/RGS transaction | Identifies game/player/round and performs session and money operations. |
If you need to change a value, first identify its owner. A similar label does not mean two settings are interchangeable: RGS rtp, plugin gameMode, and game JSON paytable data have distinct purposes.
8. Extension Points and Scope
- Add a game by implementing
ISlotGamePlugin, placing it in the pack's plugin list, and supplying its math/config according to the game's conventions. - Keep one-game mechanics in that game's
engine.ts,logic.ts, or a focused helper; shared behavior belongs in SDK only when multiple games genuinely need the same semantics. - Packs can subclass/override
GameServiceorGameController, or append bootstrap interceptors/filters/pipes, for pack-specific behavior. - Do not move RGS debit/credit into a game plugin. The core owns transaction ordering and the trust boundary for persisted state.
This document describes ownership and call flow only. See certification and request encoding for certified build artifacts, hash generation/verification, RNG-trace packaging, and the observed Base64 request decoder. Detailed cryptographic algorithms and signed artifact procedures remain outside that guide's scope unless their owning implementation is confirmed.
9. Source Files
- Pack startup
- Pack plugin registration
- Platform bootstrap
- Bootstrap options
- Game core module
- Plugin loader
- Game controller
- Game service
- Request DTOs
- SDK plugin contract
- SDK request/response contract
- SDK API responses
- SDK raw RGS init contract
- RGS client
- RGS client configuration
- HTTP client
- Async request context
- Decryption interceptor entry point
- Response interceptor
- HTTP exception filter
- API response DTO
- Monitoring service