Skip to main content

Architecture

SlotBattle ships two deployable binaries and nothing else. Knowing which one does what explains most of the operational surface and most of the constraints.

BinaryRuns asResponsibility
Control planeLong-lived container (ECS Fargate)HTTP API, WebSocket feed, lobby state, queue consumer, dispatch, aggregation
RecorderOne invocation per battle seat (Lambda or ECS task)One headless browser, one video capture, one upload, one callback

The control plane never touches Chrome or ffmpeg. That split is why the control-plane image is ~24 MB and distroless while the recorder image is ~325 MB, and why scaling the audience and scaling the recording are independent problems.

The pieces

Dependencies, and what each is load-bearing for

Postgres is the source of truth. Battles and seats live there, and so do the two tables that make launching survive a crash: durable launch intents and per-seat attempts. A control plane that dies mid-launch resumes from those rather than losing the battle.

Redis does three unrelated jobs:

  1. Cross-process barriers. Seats coordinate their start through union-set barriers, so eight browsers that booted at different speeds still spin together.
  2. Event pub/sub. How an event published by one process reaches WebSocket clients connected to another.
  3. The battle queue (slotbattle:stream). A second way to open a battle, alongside HTTP.

S3 behind a CDN holds the HLS output. Segments are uploaded in order as they are produced. Viewers pull video from the CDN, never from SlotBattle.

How a seat gets recorded

Inside one recorder invocation, per seat:

Capture is a CDP screencast, not an X11 screen grab. It encodes only the frames the game actually renders, and produces constant-frame-rate output for smooth playback.

Recording is event-windowed: capture arms when the free spins start and stops when the game round ends, falling back to a fixed duration if those events never arrive. The output is the bonus, not ten minutes of loading screen.

Clicks are driven by per-game recipes with event waits rather than fixed sleeps. The recorder waits for the game to report it is ready.

The recorder needs a GPU

Under Xvfb, Chrome needs ANGLE/EGL through /dev/dri. Without it, it falls back to Canvas2D at roughly 2 fps: the recording completes and is unwatchable, with nothing in the logs to mark it. SwiftShader is the supported no-GPU fallback and costs CPU instead.

Three ways to dispatch a seat

The control plane picks the first configured, in this order:

ModeWhen it is usedPurpose
LambdaA recorder function is configuredProduction. Real async invoke, one per seat
RIEOtherwise, if a Runtime Interface Emulator URL is setLocal development and smoke tests
ECSOtherwise, if a cluster and task definition are setHigh-CPU quality fallback, same image, ≥8 vCPU

Boot fails if none of the three is configured. An instance that cannot dispatch a seat cannot run a battle, so the failure surfaces at startup rather than on the first battle.

Two ports

The REST API listens on 8070. The WebSocket feed listens on 8071, from its own HTTP server rather than through the framework router.

The framework installs a global timeout middleware that fires a 503 into any connection a handler has already hijacked for a WebSocket upgrade, and a battle's connection stays open for the battle's whole duration, which is minutes. No per-route override is reachable from application code, so the feed runs its own server with its own timeouts: a read timeout on the handshake to keep slowloris protection, and no write timeout.

The operational consequence: your load balancer needs two target groups, one per port, with a listener rule routing /ws to the second. A single target group breaks live viewing while leaving the REST API healthy.

Where the boundaries are enforced

Import direction is dtos ← models ← services ← adapters/repositories ← http/console/runners ← providers. CI checks it as a blocking gate rather than a report, so a violating import fails the build.

Next

  • Tenancy: how one instance serves several casinos
  • Events: what moves through Redis and what reaches a browser
  • What you receive: the surfaces of all this you touch