Skip to content

New Game Service

This guide walks through scaffolding a new game service that implements CGSP v1.1 (see the CGSP v1.1 Specification).

A CGSP service is a regular HTTP + WebSocket process that:

  1. Exposes a small set of platform-facing internal endpoints (the control plane).
  2. Calls a small set of platform endpoints (config pull + ledger + wallet).
  3. Owns its own database.
  4. Starts, upgrades, and shuts down independently of every other service.

The reference implementations satisfy this contract end to end; use them as working examples. The layout below follows a Node + TypeScript stack, but the protocol is language-agnostic — only the HTTP and WebSocket calls are required.

  • A runtime and an HTTP/WebSocket stack (the reference implementations use Node 22+).
  • Familiarity with the engine package pattern: a pure game-logic package (seeded, headless, no HTTP) wired to the network, the platform, and the database by a thin service layer.
  • A persistent datastore the service owns (the reference implementations use Postgres).

Pick a short, lowercase identifier — for example roulette or dice. In the admin UI this becomes the service slug. The wire identity, however, is always the service_uid (16 chars) the platform assigns at registration; a service learns it from the first desired-state push and never hardcodes it. See Engine identity.

The slug is also a natural basis for the database name (cloud1x2_<slug>) and the app role (<slug>_app).

A typical layout:

<service-name>/
├── package.json # name, scripts (dev, start, db:migrate, build, typecheck)
├── tsconfig.json
├── scripts/
│ └── db/
│ └── migrate.ts # CGSP-compliant, self-contained migrator
├── src/
│ ├── server.ts # HTTP + WebSocket + CGSP endpoints
│ ├── db/
│ │ ├── pool.ts # service-owned datastore pool
│ │ └── migrations/ # SQL files starting at 001_<name>_engine.sql
│ ├── platformRuntimeProfileClient.ts # config pull
│ ├── platformLedgerClient.ts # open/settle ledger
│ └── platformWalletClient.ts # debit/credit/rollback
└── public/ # optional static assets / the game client

The three platform clients are small (30–60 lines each) and differ only in the auth header and the endpoints they call.

{
"scripts": {
"dev": "tsx watch src/server.ts",
"start": "node src/server.ts",
"db:migrate": "tsx scripts/db/migrate.ts",
"typecheck": "tsc --noEmit"
}
}

Create a pure-logic package:

packages/<name>-engine/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # public exports
├── engine.ts # createEngine() — the pure game loop
└── types.ts # config shape, bet input, etc.

Three design rules:

  • No database access from the engine. The engine receives a loadRuntimeConfig(profileId) callback from the hosting service.
  • Emit events, do not write. On each bet placement/settle, the engine emits a ledger event; the service layer forwards it to the platform.
  • Per-bet detail lives in the service DB. The platform ledger row is financial only, keyed by service_bet_uid; it never duplicates per-bet game metadata.

The minimum viable service hosts:

  • PUT /internal/engines/desired-state — reconcile to the platform’s desired state.
  • GET /internal/engines — return in-memory engine summaries.
  • GET /internal/engines/:engineId/status — per-engine detail.
  • GET /health — liveness.
  • A WebSocket connection handler that broadcasts round events to clients.

Two more endpoints are optional but unlock v1.1 LiveOps features:

  • POST /internal/engines/:engineId/pause and /resume — admin pause/resume.
  • the service also calls POST /internal/engines/round-start-check each round for operator-suspend handling.

Full request/response shapes are in Control Plane.

Three HTTP clients call back to the platform:

ClientHeaderPurpose
Runtime-profile clientx-internal-config-secretPull the runtime profile per-round / on reconcile
Ledger clientx-internal-wallet-secretPOST /internal/consolidated/tx + /settle
Wallet clientx-internal-wallet-secretOperator wallet debit / credit / rollback

See Config + Ledger and the Wallet API.

  1. Create the role and database (one-time, with the platform’s superuser):

    CREATE ROLE <name>_app LOGIN PASSWORD '<name>_app';
    CREATE DATABASE cloud1x2_<name> OWNER <name>_app;
  2. Add <NAME>_DATABASE_URL to the environment.

  3. Write the first migration at src/db/migrations/001_<name>_engine.sql. No cross-DB foreign keys — reference games.external_id and the runtime profile ID by integer only.

  4. Verify locally:

    Terminal window
    <NAME>_DATABASE_URL=postgres://<name>_app:<name>_app@127.0.0.1:5432/cloud1x2_<name> \
    npm run db:migrate --prefix <service-name>
  1. Add a container service block mirroring the reference implementations (DATABASE_URL, the four INTERNAL_*_SECRET values, PLATFORM_INTERNAL_BASE_URL).
  2. Register the service in the platform so the launcher proxy can route browser traffic to it (see Register and Launch).
  3. If the service serves a web client, configure the launcher upstream URL for the new service.

Before marking a new service integration-complete:

  • typecheck passes.
  • The service imports no platform DB modules (the isolation contract is strict).
  • The container boots, runs its migrations, and reports healthy.
  • Admin actions on a matching runtime profile or game propagate to the service (confirm via the desired-state log line).
  • A full smoke scenario works: launch → bet → settle → admin suspend/resume → the ledger reflects amounts, status, and dates.
  • protocolVersion: "1.1" appears in every request / response payload.

Can the engine read platform tables directly? No. Every piece of platform data comes through a CGSP endpoint. If a needed field is not yet exposed, request a minor-bump endpoint.

Can a different database engine be used? The protocol is HTTP-only and does not constrain storage. The reference tooling assumes Postgres; another store is possible with equivalent migration tooling.

How are per-operator / per-currency bet limits handled? Through bet templates — GET /internal/bet-templates (v1.1).

Once the service is scaffolded, continue to Register and Launch to connect it to a running platform.