A phone-friendly Reversi/Othello game for the Tokyo Game Show booth. Two visitors join a queue, receive a private match actor, and play on a live 8 × 8 board. A TV wall shows matches created by this app process. The UI is English-led with concise Japanese subtitles; PostgreSQL is optional for local development and required on AppThrust. When DATABASE_URL is absent, the mock development store remains in memory while the actor still owns each match's game state.
Use Node.js 24 and npm:
npm ci
ACTOR_BACKEND=mock npm run devOpen http://localhost:3000. Use two separate browser profiles/private sessions to play both seats; two tabs in the same profile share a player cookie. /wall is the big-screen view. For another port:
ACTOR_BACKEND=mock npm run dev -- --port 3097The mock is a development-only, in-memory implementation of the actor methods. It deliberately delays its first call by approximately 1.2 seconds so the waiting screen can be exercised. The UI labels mock timings as a local simulation. NODE_ENV=production rejects ACTOR_BACKEND=mock.
Copy .env.example to .env.local for local configuration. Supply production values as runtime environment variables/secrets, never build arguments or NEXT_PUBLIC_* values. No credentials are needed to build the app.
| Variable | Meaning |
|---|---|
ACTOR_BACKEND |
platform-api (default), or mock in development only. |
DATABASE_URL |
PostgreSQL connection URL. Optional locally (in-memory fallback); required on AppThrust. |
PLATFORM_API_URL |
Platform API origin, for example https://dashboard.appthrust.dev. Do not append /api/v1. |
APPTHRUST_PROJECT_ID |
Existing project containing the ActorType. Required for Platform mode. |
APPTHRUST_ACTOR_TYPE_ID |
Existing ActorType serving the Reversi fixture; defaults to reversi. |
KEYCLOAK_ISSUER |
Realm issuer, for example https://iam.appthrust.dev/realms/appthrust. |
KEYCLOAK_CLIENT_ID |
Confidential service-account client with project actor write access. |
KEYCLOAK_CLIENT_SECRET |
Client secret, supplied through the deployment's secret facility. |
PORT |
HTTP listen port; defaults to 3000. |
The same client-credentials JWT must be accepted by both Platform API and the actor gateway. This app mints and uses it only in server-only modules; credentials and upstream response bodies are never sent to the browser or logs.
Phone → Next.js route handler → Platform API → actor gateway → Reversi actor
proxy.tsissues an httpOnlyreversipidcookie on the first visit. It is SameSite=Lax and Secure on HTTPS, including TLS terminated by a trusted proxy. The ingress must preserve the public Host and set/overwrite X-Forwarded-Proto.- The queue reserves two different players in PostgreSQL when
DATABASE_URLis set (with an in-memory fallback otherwise). It allocatesmatch-<n>from the database sequence. lib/appthrust.tsmints a Keycloakclient_credentialstoken server-side, reusing it until 60 seconds before expiry. Concurrent token requests share one in-flight mint.- Player one is seated by
POST /api/v1/projects/{project}/actor-types/{type}/actorswith{actorId, method:"reversi-join", body:{player,name}}. Player two joins viaPOST .../actors/{id}/invokewith{method:"reversi-join", body:{player,name}}. Both routes get an idempotency key. - Platform API owns activation retry/backoff. The app waits for its bounded response and does not retry game mutations. The queue shows real elapsed time, then activation attempts and timings.
- Every placement and state read runs through the actor. Row 0 is the top row in JSON and UI. The initial four discs are
[3][3]=2,[3][4]=1,[4][3]=1,[4][4]=2, and player 1 starts. The actor validates bracketing in all eight directions, flips every bracketed disc, passes automatically when the next player has no legal move, and ends when neither player can move or the board is full. The frontend does not optimistically decide a move or winner.
The actor card keeps creation/activation timings separate from the last call and last turn. The wall uses last-known snapshots and does not wake every actor just to render the TV.
All routes are same-origin, JSON, and Cache-Control: no-store.
| Route | Request / response |
|---|---|
POST /api/queue |
{name} (trimmed, 1–24 characters) → queue status. |
GET /api/queue |
idle, waiting, spawning, matched, or error; includes elapsedMs and matchId/seat when reserved. |
GET /api/match/{id} |
Invokes reversi-state; returns actor reply plus matchId, seat (0 for spectator), activation, creation, lastTurnMs, backend. |
POST /api/match/{id}/place |
{row,column} → invokes reversi-place with the cookie's player ID, not an ID supplied by the browser. |
POST /api/match/{id}/reset |
Invokes reversi-reset; keeps both players. Only seated players may mutate a match. |
GET /api/wall |
{total, backend, matches}; each match contains state, activation, creation, lastTurnMs, and createdAt. |
Actor ok:false maps to HTTP 409, preserving its error code and state. Game codes are bad_request, match_full, not_playing, unknown_player, not_your_turn, bad_cell, and illegal_move. Malformed/oversized app JSON is 400 (bad_request); an invalid name is 400 (bad_name). A different browser origin is 403 (cross_origin_request), and a match outside this process's directory is 404 (match_not_found). Backend failures use sanitized 502/503 codes rather than exposing credentials or raw upstream messages.
Build this repository as one AppThrust Application component using Dockerfile. It follows the sample deployment pattern: Node 24 slim dependency/build/runtime stages, npm ci, Next.js standalone output, copied public/static assets, and a non-root runtime user. The container starts node server.js on 0.0.0.0:3000.
Run one or more instances. Matchmaking and the TV index use PostgreSQL when configured; the database survives restarts and coordinates concurrent replicas. The actor owns game state and is not replaced by the database. Without DATABASE_URL, local mock development intentionally uses a process-local fallback.
The project keeps npm and package-lock.json canonical. The deployment workflow builds the Docker image for both amd64 and arm64. Use two cookie jars or separate browser profiles for a local mock smoke flow; remove any local cookie jars after use.