Games is a TypeScript monorepo for browser-based party games and logic puzzles. It includes a React + Vite player app, a Bun/Hono API, a Next.js admin dashboard, shared Drizzle/Zero contracts, and a local Postgres + Zero development stack.
This project is a full refactor of an earlier version: oyuh/games-arch.
Live links:
- Web app: games.lawsonhart.me
- Shikaku puzzle SVG endpoint: api.games.lawsonhart.me/api/shikaku/puzzle
- Games
Community files:
The app currently exposes seven playable experiences.
| Game | Mode | Players | Route | State model |
|---|---|---|---|---|
| Imposter | Social deduction | 3-12 | /imposter/:id |
Multiplayer, Zero-synced |
| Password | Team word guessing | 4+ | /password/:id/begin, /password/:id, /password/:id/results |
Multiplayer, Zero-synced |
| Chain Reaction | Competitive word-chain duel | 2 | /chain/:id |
Multiplayer, Zero-synced |
| Shade Signal | Color clue guessing | 3-8 | /shade/:id |
Multiplayer, Zero-synced |
| Location Signal | Map clue guessing | 3-8 | /location/:id |
Multiplayer, Zero-synced |
| Shikaku | Timed rectangle logic puzzle | Solo | /shikaku |
Shared seeded engine + REST leaderboard |
| Pips | Timed domino logic run | Solo | /pips |
Shared seeded engine + REST leaderboard |
Multiplayer games share room creation, join codes, public lobby visibility, spectators, host controls, chat, session presence, admin kicks, and synchronized state through Rocicorp Zero.
Solo games do not require the Zero cache to be awake. Shikaku and Pips run their puzzle engines in the browser and call REST endpoints only for eligibility checks, leaderboard reads, and score submission. Ranked submissions include replay data, and the API reuses the same shared engines to regenerate the public seed and verify the submitted solve before storing leaderboard rows.
Each game has its own document with rules, flow, scoring, and implementation notes.
| Game | What it covers | Doc |
|---|---|---|
| Imposter | Clue phase, voting, categories, round history | docs/game-imposter.md |
| Password | Teams, clue givers, guessers, target score | docs/game-password.md |
| Chain Reaction | Word chains, turns, scoring, chain generation | docs/game-chain-reaction.md |
| Shade Signal | Color grid, leader rotation, clues, proximity scoring | docs/game-shade-signal.md |
| Location Signal | Map picking, clues, distance scoring, leader rounds | docs/game-location-signal.md |
| Shikaku | Puzzle generation, run modes, scoring, leaderboard validation | docs/game-shikaku.md |
| Pips | Domino placement, seeded runs, split timing, leaderboard design | docs/game-pips.md |
.
+-- apps/
| +-- web/ # React 19 + Vite player app
| +-- api/ # Bun/Hono API, Zero handlers, REST endpoints
| +-- admin/ # Next.js 15 admin dashboard
+-- packages/
| +-- shared/ # Drizzle/Zero contracts, metadata, shared solo puzzle engines
+-- docs/ # Game docs and maintenance notes
+-- scripts/ # Local stack and production DB helper scripts
+-- docker-compose.yml # Local Postgres + Zero cache stack for Windows script path
+-- Dockerfile # API container image
+-- railway.toml # API Railway deployment config
+-- vercel.json # Web Vercel deployment config with SPA + bot preview rewrites
+-- turbo.json # Workspace task orchestration
+-- package.json # Bun workspace scripts
The web app is a React 19 single-page application built by Vite. It owns the public game experience.
Main responsibilities:
- Browser routes for the home page, multiplayer rooms, Shikaku, Pips, and score admin helper route.
- A module-scoped Zero client for realtime multiplayer sync.
- Local browser identity, recent games, display name, and first-visit state.
- HTTP session sync and presence heartbeats against the API.
- Bun WebSocket subscriptions for global admin broadcasts, targeted user events, and live Password typing.
- Lazy-loaded game pages and vendor chunks for smaller initial loads.
- Sync wake/idle messaging when the multiplayer Zero cache is cold or paused.
- Mobile-specific pages and bottom sheets for the multiplayer experience.
Key files:
apps/web/src/App.tsxapps/web/src/pages/apps/web/src/mobile/apps/web/src/lib/zero.tsapps/web/src/lib/session.tspackages/shared/src/games/shikaku-engine.tspackages/shared/src/games/pips-engine.ts
Shikaku and Pips use shared TypeScript engine modules so the browser and API agree on the exact same ranked rules even when the API is not needed for local play. The web app imports the engines through thin wrappers in apps/web/src/lib/*-engine.ts; the API imports the shared modules directly for leaderboard validation.
packages/shared/src/games/shikaku-engine.tsowns seeded Shikaku generation, rectangle validation, scoring, auto-filled1x1detection, and ranked replay verification.packages/shared/src/games/pips-engine.tsowns seeded Pips generation, board/region validation, domino placement validation, solver utilities, run time scoring, and ranked replay verification.- Ranked score requests include replay payloads: Shikaku sends solved rectangles for each of the five puzzles, and Pips sends solved domino placements for Easy, Medium, and Hard.
- On the server, the API regenerates the canonical run from the submitted seed, validates the replay against those generated puzzles, recalculates or checks score/time invariants, then applies duplicate, top-20, rate-limit, and ban checks before writing to Postgres.
The API is a Bun-powered Hono service. It handles REST endpoints, Zero query/mutation forwarding, admin operations, signed session identity, score validation, and cleanup work.
Main responsibilities:
POST /api/zero/queryandPOST /api/zero/mutate- Signed session cookies and signed Zero session proofs.
- Session sync and presence heartbeat updates.
- Bun WebSocket upgrade auth and admin event triggers.
- Server-held secret keys for hidden game data.
- Shikaku and Pips leaderboard, eligibility, and score validation.
- Location Signal map tile config and geocode proxy.
- Admin dashboard API under
/api/admin/*. - Scheduled and manual cleanup of stale games and sessions.
/healthand/debug/build-infodiagnostics.
Key files:
apps/api/src/index.tsapps/api/src/admin-routes.tsapps/api/src/broadcast-server.tsapps/api/src/session-identity.tsapps/api/src/db-provider.ts
The admin dashboard is a Next.js 16 app protected by NextAuth. It proxies admin requests to the API with ADMIN_SECRET.
Main responsibilities:
- Dashboard summary and recent activity.
- Connected client/session browsing.
- Active game inspection, ending, and kicking.
- Session, IP, and region bans.
- Restricted name patterns and forced name overrides.
- Global broadcasts, refresh commands, update warnings, and custom status banners.
- Shikaku score management.
- Pips score management.
Key files:
apps/admin/src/auth.tsapps/admin/src/lib/api.tsapps/admin/src/app/(dashboard)/apps/admin/src/components/admin/
The shared package is the contract layer used by the web app and API.
It contains:
- Drizzle Postgres schema.
- Zero schema.
- Shared query definitions.
- Domain-split Zero mutators.
- Shared game types and game metadata.
- Drizzle Kit config and migrations.
Mutators live under packages/shared/src/zero/mutators/:
packages/shared/src/zero/mutators/
+-- index.ts
+-- helpers.ts
+-- word-banks.ts
+-- sessions.ts
+-- chat.ts
+-- imposter.ts
+-- password.ts
+-- chain-reaction.ts
+-- shade-signal.ts
+-- location-signal.ts
+-- demo.ts
- Bun 1.3.x or newer
- Docker Desktop, OrbStack, Colima, or another Docker daemon
- Git
bun install
bun run local:upThen open:
- Web app:
http://localhost:5173 - API:
http://localhost:3001 - Admin app:
http://localhost:3002 - Zero cache:
http://localhost:4848
bun run local:up starts local Postgres, pushes the Drizzle schema, resets the Zero replica, starts Zero cache, and launches the workspace dev servers.
If you want to run each piece yourself:
bun install
docker compose up -d
bun run db:push
bun run dev# Windows
bun run local:down
# macOS
bun run local:down:mac
# Linux
bun run local:down:linux# Windows
bun run local:up
bun run local:reset
# macOS
bun run local:up:mac
bun run local:reset:mac
# Linux
bun run local:up:linux
bun run local:up:linux:host
bun run local:reset:linuxThe Linux/macOS script uses standalone Docker containers and volumes. The Windows script uses docker compose.
The API and shared database tooling load the repository root .env. For local development, this is the practical minimum:
NODE_ENV=development
DATABASE_URL=postgres://postgres:postgres@localhost:5432/games
ZERO_UPSTREAM_DB=postgres://postgres:postgres@localhost:5432/games
ZERO_CVR_DB=postgres://postgres:postgres@localhost:5432/games
ZERO_CHANGE_DB=postgres://postgres:postgres@localhost:5432/games
ZERO_ADMIN_PASSWORD=dev-password
CLEANUP_SECRET=cleanup-local
SESSION_COOKIE_SECRET=games-dev-session-secret
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173Optional map variables:
MAP_TILE_URL_TEMPLATE=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
MAP_TILE_ATTRIBUTION=(c) OpenStreetMap contributors
MAP_GEOCODE_URL=https://nominatim.openstreetmap.org/searchThe web app defaults to local endpoints when these are omitted:
VITE_ZERO_CACHE_URL=http://localhost:4848
VITE_API_URL=http://localhost:3001
VITE_WS_URL=ws://localhost:3001/ws
VITE_STYLE_ONLY=falseVITE_WS_URL is optional if it is just the API URL plus /ws; the client can derive it from VITE_API_URL.
The admin app talks to the API through a proxy route and sends ADMIN_SECRET as a bearer token.
GAMES_API_URL=http://localhost:3001
ADMIN_SECRET=<same_secret_used_by_api>
AUTH_SECRET=<long_random_secret>
# NEXTAUTH_SECRET can also be used if the deployment already relies on it.
GITHUB_CLIENT_ID=<github_oauth_client_id>
GITHUB_CLIENT_SECRET=<github_oauth_client_secret>
ADMIN_GITHUB_IDS=<comma_separated_allowed_github_logins>For local development, a credentials login can be enabled:
ADMIN_DEV_SECRET=<local_admin_password>The API must also have:
ADMIN_SECRET=<same_secret_used_by_admin_app>For a deployed Zero cache service:
NODE_ENV=production
ZERO_UPSTREAM_DB=<postgres_url>
ZERO_QUERY_URL=https://<api-domain>/api/zero/query
ZERO_MUTATE_URL=https://<api-domain>/api/zero/mutate
ZERO_ADMIN_PASSWORD=<strong_secret>Optional:
ZERO_CVR_DB=<postgres_url>
ZERO_CHANGE_DB=<postgres_url>For Zero 1.5, keep the Zero cache version aligned with @rocicorp/zero in the workspace. A cache/server version mismatch can pass health checks while breaking browser sync connections.
Run from the repository root.
| Command | Purpose |
|---|---|
bun run dev |
Start all workspace dev servers through Turbo |
bun run local:up |
Start local DB/Zero, push schema, run dev servers on Windows |
bun run local:up:mac |
Start local DB/Zero, push schema, run dev servers on macOS |
bun run local:up:linux |
Start local DB/Zero, push schema, run dev servers on Linux |
bun run local:down |
Stop local dev ports and Docker services on Windows |
bun run build |
Build all workspaces |
bun run typecheck |
Typecheck all workspaces |
bun run test |
Run Vitest suites |
bun run test:ci |
Run CI-style Vitest suites |
bun run test:local |
Run shared local integration tests |
bun run lint |
Placeholder lint scripts |
bun run db:push |
Push Drizzle schema to the configured database |
bun run db:studio |
Open Drizzle Studio |
bun run db:push:prod |
Push schema to PROD_DB_URL after confirmation |
Package-scoped examples:
bun --filter @games/web build
bun --filter @games/web test
bun --filter @games/api test
bun --filter @games/admin typecheck
bun --filter @games/shared db:pushReact Doctor commands are documented in docs/react-doctor-guide.md.
GitHub Actions runs the CI workflow on pull requests, pushes to main or master, and merge queue checks. The required job is named Quality Gate and runs:
bun run lint
bun run typecheck
bun run test:ci
bun run buildThe Deploy Hooks workflow listens for successful CI runs on main or master. It only calls deploy hooks after CI passes. Add these repository secrets if you want GitHub Actions to trigger deployments:
VERCEL_DEPLOY_HOOK_URL=<vercel_deploy_hook_url>
RAILWAY_DEPLOY_HOOK_URL=<optional_custom_or_platform_deploy_trigger_url>To keep production from deploying failing commits, protect the production branch in GitHub and require Quality Gate before merging. Also configure the hosts themselves:
- Vercel: use Deployment Checks for the production project and select the GitHub Actions
Quality Gatecheck, or disable automatic Git production deploys and rely on the post-CI deploy hook. - Railway: enable Wait for CI on each GitHub-connected service, or disable automatic deploys and trigger Railway from a post-CI workflow.
Keep the CI job name stable. GitHub, Vercel, and Railway use check names to identify which result should gate a merge or deployment.
The primary schema lives in packages/shared/src/drizzle/schema.ts.
Important tables:
| Table | Purpose |
|---|---|
sessions |
Browser-backed player identity, current game attachment, IP/region/fingerprint, last seen |
status |
Footer/database health sentinel |
imposter_games |
Imposter room state, players, clues, votes, history, settings |
password_games |
Password teams, rounds, active rounds, scores, settings |
chain_reaction_games |
Word-chain state, submitted chains, turn, scores, round history |
shade_signal_games |
Color-grid target, leader rotation, clues, guesses, scores |
location_signal_games |
Map target, leader rotation, clues, guesses, distance scoring |
chat_messages |
Per-game chat history |
game_encryption_keys |
Server-held keys for hidden game secrets |
shikaku_scores |
Shikaku leaderboard entries and replay metadata |
shikaku_banned_sessions |
Shikaku abuse bans |
pips_scores |
Pips leaderboard entries with easy/medium/hard splits |
pips_banned_sessions |
Pips abuse bans |
admin_bans |
Session, IP, and region bans |
admin_restricted_names |
Restricted display-name patterns |
admin_name_overrides |
Forced display names by session |
The multiplayer game tables intentionally store much of their live state in JSON columns. That keeps room snapshots simple and keeps game transitions close to the mutator logic.
| Endpoint | Purpose |
|---|---|
GET /health |
Railway/API healthcheck |
GET /debug/build-info |
API build, uptime, platform, and database sentinel status |
POST /api/session/sync |
Resolve or create signed browser session identity |
POST /api/presence/heartbeat |
Refresh session/game presence |
GET /ws |
Upgrade to the authenticated Bun WebSocket transport |
GET /api/admin-status |
Current site-wide admin status payload |
GET /api/public/names/restricted |
Public restricted-name pattern list |
GET /api/embed/html |
Rich social/bot preview HTML |
GET /api/shikaku/puzzle |
Shikaku puzzle viewer page |
GET /api/shikaku/puzzle.svg |
Dynamic Shikaku puzzle SVG |
GET /api/maps/config |
Location Signal map tile configuration |
GET /api/maps/geocode |
Location Signal geocoding proxy |
POST /api/game-secret/init |
Initialize server-held game secret material |
POST /api/game-secret/pre-reveal |
Prepare hidden data before reveal |
POST /api/game-secret/key |
Resolve game secret key for authorized reveal paths |
GET/POST /api/cleanup |
Run authenticated stale-game/session cleanup |
GET/POST /api/activity |
Run authenticated activity report |
| Endpoint | Purpose |
|---|---|
POST /api/zero/query |
Resolve shared Zero query requests |
POST /api/zero/mutate |
Resolve shared Zero mutation requests |
| Endpoint | Purpose |
|---|---|
GET /api/shikaku/leaderboard |
Read Shikaku leaderboard |
POST /api/shikaku/score/eligibility |
Check Shikaku score eligibility and canonical replay validity |
POST /api/shikaku/score |
Submit Shikaku score with solved-rectangle replay |
GET /api/pips/leaderboard |
Read Pips leaderboard |
POST /api/pips/score/eligibility |
Check Pips run eligibility and canonical replay validity |
POST /api/pips/score |
Submit Pips run with solved-domino replay |
Admin routes are mounted under /api/admin/* and require Authorization: Bearer <ADMIN_SECRET>.
Major groups:
/clients/games/bans/broadcast/*/status/names/*/shikaku/scores/pips/scores
The admin app lives in apps/admin and runs locally on port 3002.
Pages:
| Route | Purpose |
|---|---|
/login |
GitHub or local dev login |
/ |
Dashboard summary |
/clients |
Connected sessions and client actions |
/games |
Active room inspection and moderation |
/bans |
Session/IP/region bans, restricted names, name overrides |
/names |
Redirects to /bans |
/shikaku |
Shikaku leaderboard management |
/pips |
Pips leaderboard management |
Capabilities:
- Use broadcast controls from the dashboard shell.
- View live sessions, names, fingerprints, regions, and game attachments.
- Inspect active games by type and phase.
- End one game or all games.
- Kick a player from a game.
- Ban by session ID, IP address, or region.
- Send global or targeted toast messages.
- Force refresh all clients.
- Publish custom site-wide status.
- Schedule update warnings.
- Override player names and maintain restricted name patterns.
- Create, edit, delete, and bulk-clear Shikaku and Pips score records.
The web app has a separate mobile surface under apps/web/src/mobile.
Mobile routing is selected by the desktop page components through useIsMobile() at the 768px breakpoint. Mobile pages use their own app shell, bottom navigation, sheets, and m- prefixed CSS classes so desktop and mobile changes stay isolated.
Current mobile-specific pages include:
- Home
- Imposter
- Password begin/game/results
- Chain Reaction
- Shade Signal
- Location Signal
Solo games render their own pages. Shikaku is documented as desktop-only; Pips owns its own responsive puzzle interface rather than a separate MobilePipsPage.
Production is split across separate services:
- Vercel:
apps/web - Railway:
apps/api - Railway: Zero cache
- Railway Postgres or Neon: database
- Bun WebSockets: admin broadcasts, targeted events, and live Password typing
vercel.json disables automatic Git deploys from main and master. The default production web path is the post-CI deploy hook, not a raw push that has not passed Quality Gate. If you prefer Vercel's built-in Deployment Checks flow, remove or adjust that git.deploymentEnabled block and configure Quality Gate as the required deployment check in Vercel.
vercel.json currently uses Bun:
- install:
bun install --frozen-lockfile - build:
bun run --filter @games/web build - output:
apps/web/dist
It also includes:
- SPA rewrite to
/index.html. - Bot/social-preview rewrite to the API embed endpoint.
Required Vercel variables:
VITE_ZERO_CACHE_URL=https://<zero-domain>
VITE_API_URL=https://<api-domain>
VITE_WS_URL=wss://<api-domain>/wsIf Vercel Git auto-deploys are enabled again, configure Vercel Deployment Checks so production is not promoted until Quality Gate passes. The current repo config disables main and master Git autodeploys, so the safer default is to store a Vercel deploy hook as VERCEL_DEPLOY_HOOK_URL and let GitHub Actions call it after CI passes.
railway.toml builds from Dockerfile and starts:
bun apps/api/src/index.tsRequired API variables:
NODE_ENV=production
DATABASE_URL=<postgres_url>
CLEANUP_SECRET=<strong_secret>
SESSION_COOKIE_SECRET=<long_random_secret>
ADMIN_SECRET=<strong_admin_secret>
CORS_ALLOWED_ORIGINS=https://<web-domain>If Railway GitHub autodeploys are enabled, turn on Wait for CI in the Railway service settings so Railway waits for GitHub Actions before deploying. If using a manual/API trigger instead, disable automatic deploys and trigger it from the post-CI workflow after Quality Gate passes.
Useful API checks after deploy:
GET https://<api-domain>/health
GET https://<api-domain>/debug/build-infoDeploy Zero cache as a separate service. Keep it on the same @rocicorp/zero version used by the workspace (1.5.0).
Recommended start command:
bunx @rocicorp/zero@1.5.0 zero-cache --port "$PORT"Required Zero variables:
NODE_ENV=production
ZERO_UPSTREAM_DB=<postgres_url>
ZERO_QUERY_URL=https://<api-domain>/api/zero/query
ZERO_MUTATE_URL=https://<api-domain>/api/zero/mutate
ZERO_ADMIN_PASSWORD=<strong_secret>Do not set ZERO_PORT to the literal string "$PORT" on Railway. Railway does not shell-expand environment variable values in that field.
Zero needs a direct Postgres connection with logical replication support. For local development, docker-compose.yml starts Postgres 16 with:
postgres -c wal_level=logicalFor production, use a direct Postgres URL for ZERO_UPSTREAM_DB; avoid transaction poolers for the Zero upstream connection.
The app uses browser-local identity rather than user accounts for public gameplay. The API signs a long-lived games_session cookie and issues a signed Zero session proof. Mutations are checked so one browser session cannot submit actions for another player.
Presence is inferred from HTTP heartbeats, not WebSockets. Game pages periodically call POST /api/presence/heartbeat, and the API updates sessions.lastSeen plus the current game attachment.
Admin broadcasts use the API's Bun WebSocket service:
broadcastfor global messages.user:{sessionId}for targeted kicks, name changes, and direct toasts.password-team:{gameId}:{teamIndex}for team-only Password live typing.
The API can clean stale games and sessions in two ways:
- scheduled cleanup inside the API process
- manual
GETorPOST /api/cleanupwith bearer auth fromCLEANUP_SECRET
Cleanup marks abandoned games as ended, detaches stale sessions, and removes old ended rows.
/debug/build-info reads the status table and reports whether the configured sentinel exists and matches.
Default values:
DB_STATUS_KEY=footer
DB_STATUS_EXPECTED_VALUE=okSeed or repair the row with:
INSERT INTO status (key, value, updated_at)
VALUES ('footer', 'ok', EXTRACT(EPOCH FROM NOW())::bigint * 1000)
ON CONFLICT (key)
DO UPDATE SET
value = EXCLUDED.value,
updated_at = EXCLUDED.updated_at;After a deploy:
- Open the web app.
- Create one room for each multiplayer game.
- Join a room from a second tab or device.
- Confirm chat, presence, phase transitions, and host controls.
- Play one Shikaku run and verify leaderboard submission.
- Play one Pips run and verify leaderboard submission.
- Open the admin dashboard and verify clients, games, broadcasts, bans, Shikaku scores, and Pips scores.
- Check
/health,/debug/build-info, and the Zero cache public URL.
- Public gameplay has browser-local identity only; there is no player account system.
- Lint scripts are placeholders.
- Multiplayer game state is mostly JSON-column snapshots by design.
- The API runs TypeScript directly through Bun in production instead of a compiled
distentry. - The Zero cache and workspace
@rocicorp/zeroversions should stay aligned. - Shikaku and Pips are not Zero-synced multiplayer games; they use REST only for leaderboard flows.