Boot
A game server is one TypeScript file. It composes a
GameManifest, a PlatformAdapter, and a
ClientTransport and hands them to createServer.
Minimum boot
import { createServer, binaryTransport, loadLuaMath } from "@open-rgs/core";
import { defineGame } from "@open-rgs/contract";
import { MockPlatform } from "@open-rgs/platform-mock";
import pkg from "../package.json" with { type: "json" };
const math = await loadLuaMath("./maths/spin.lua");
const manifest = defineGame({
id: "hello-spin",
declaredRtp: 0.95,
defaultMode: "default",
maxWinMultiplier: 5000,
modes: {
default: { math, stakeMultiplier: 1 },
},
});
await createServer({
manifest,
platform: new MockPlatform({ startingBalance: 100_00 }),
transport: binaryTransport({ port: Number(process.env.PORT ?? 80) }),
version: pkg.version,
shutdownDrainMs: 30_000,
}); ServerConfig
| Field | Purpose |
|---|---|
manifest | frozen GameManifest from defineGame |
platform | any PlatformAdapter implementation |
transport | any ClientTransport implementation (use binaryTransport) |
version | game version string, surfaced in /healthz |
adminPort | omit for single-port (admin on transport's port); set to split |
idempotency | { generate?, ttlMs? } — default uuid-v4, 5 min TTL |
shutdownDrainMs | drain window on SIGTERM/SIGINT, default 30 000 |
installSignalHandlers | default true; set false in tests |
metrics | bring your own registry, else the standard one is created |
isDev | override env-detected dev flag |
createServer returns { stop, metrics }.
stop() drains in-flight requests, disconnects the
platform, and shuts admin + transport. Called automatically on
SIGTERM / SIGINT unless installSignalHandlers: false.
GameManifest
| Field | Purpose |
|---|---|
id | game identifier, lowercased URL-safe |
declaredRtp | overall RTP for the certification record |
modes | Record<string, GameMode> — see below |
defaultMode | fallback mode when the client doesn't specify one |
maxWinMultiplier | game-wide cap (× bet). Per-mode overrides supported |
autoclose | { idleMs, policy } — settle-at-current / as-loss / hold / math-decides |
recovery | { onRestart: "resume" | "forfeit" | "autoclose" } |
GameMode
modes: {
default: { math, stakeMultiplier: 1, declaredRtp: 0.95 },
ante: { math, stakeMultiplier: 1.25, declaredRtp: 0.96, label: "Ante" },
buy: { math, stakeMultiplier: 60, declaredRtp: 0.96, label: "Buy bonus" },
freespins: { math: fsMath, stakeMultiplier: 0, internal: true }, // not client-requestable
} stakeMultiplier multiplies the bet ladder
(allowedBets[betIndex] × priceMultiplier × stakeMultiplier).
internal: true hides the mode from the client catalog —
reachable only via another mode's nextMode.
maxWinMultiplier per mode overrides the manifest cap.
Ports
Default (recommended): one port. The transport's
Bun.serve handles WS upgrade and mounts
/livez, /readyz, /healthz,
/admin/* on the same listener. Set
adminPort to split admin onto a private interface
(useful when ingress only exposes the public port).