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

FunctionResponsibility
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.

  1. Fastify is configured with a 50 MB default body limit, CORS, and shutdown hooks.
  2. Global interceptors are added: AsyncSessionInterceptor, optional DecryptionInterceptor, optional ResponseInterceptor, then any pack-supplied interceptors.
  3. HttpExceptionFilter maps thrown errors into the shared error response shape.
  4. A global ValidationPipe transforms DTO values and strips properties not declared by the DTO (whitelist: true).
  5. The server listens on the configured APP_PORT.
ComponentWhat 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.
ValidationPipeApplies 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.

FunctionMain 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, and epoch are core/RGS concerns; they are not passed to the game engine as game rules.
  • gameMode, betAmount, devMode, RGS maxWin, and game-specific extraData are selected/mapped by the core and sent through the SDK plugin input.
  • devMode comes from RGS allowCheat, not from a client-supplied boolean.
  • progressData is trusted only from RGS. On spin, the service overwrites any client-provided extraData.progressData before the plugin call.
  • isRecord(gameData) checks plugin output before the core persists or returns it.
  • The core trims feature spinResults to 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

FunctionResponsibility
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/functionOwner and responsibility
IInitInput / IInitOutputSDK plugin boundary for selected gameMode, paytable, and optional init gameData.
ISpinInput / ISpinOutputSDK contract for a base spin and its win/feature summaries.
IFeatureInput / IFeatureOutputSDK contract for resuming a feature from previousSpinResult; output uses featureComplete.
IInitResponse / ISpinResponse / IFeatureResponseSDK shapes for the data portion returned by shared core endpoints.
IRGSInitResponseSDK 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:

  1. Game plugin output: totalWin, featureTriggered/featureComplete, and game-specific gameData.
  2. Game API data: gameId, roundId, closed, result, playerInfo, and optional promoInfo/betType.
  3. HTTP envelope: success, statusCode, message, and data (or error extensions).

The game-specific feature entry and featureData details are cataloged in the code walkthrough's feature reference.

7. Configuration: Similar Names, Different Owners

Setting/dataOwnerPurpose
Game mathModes / gameModeGame plugin + RGS sessionSelects a game math file/cache entry, such as Carpathian R4.
payTable, reels, weight tables, layoutGame math JSONDetermines game-specific outcomes and scoring.
preferences, opCnf, rtp, promoInfo, epochRGS init/session responsePlayer/operator/session information returned through init; keys inside operator preferences are externally defined.
APP_PORT, NODE_ENV, DISABLE_DECRYPTIONShared app configurationControls service process/bootstrap behavior.
RGS_URL, pool settings, request timeout, retry settingsRGS client configurationControls outbound service communication.
gameId, token, roundId, bet/feature fieldsHTTP/core/RGS transactionIdentifies 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 GameService or GameController, 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

Built with LogoFlowershow