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

FieldPurpose
manifestfrozen GameManifest from defineGame
platformany PlatformAdapter implementation
transportany ClientTransport implementation (use binaryTransport)
versiongame version string, surfaced in /healthz
adminPortomit for single-port (admin on transport's port); set to split
idempotency{ generate?, ttlMs? } — default uuid-v4, 5 min TTL
shutdownDrainMsdrain window on SIGTERM/SIGINT, default 30 000
installSignalHandlersdefault true; set false in tests
metricsbring your own registry, else the standard one is created
isDevoverride 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

FieldPurpose
idgame identifier, lowercased URL-safe
declaredRtpoverall RTP for the certification record
modesRecord<string, GameMode> — see below
defaultModefallback mode when the client doesn't specify one
maxWinMultipliergame-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).