Who's the Impostor
A self-hosted, accountless multiplayer social-deduction word game. Every player uses their own device for private roles, word hints, clues, voting, and final guesses.
Current MVP
The authoritative TypeScript engine supports Classic and Last Man Standing,
multiple impostors, runoff votes, final guesses, scoring, recipient-specific
snapshots, and REVEAL_ONLY word privacy. Room state is revisioned,
idempotent, persisted in PostgreSQL, mirrored into normalized history tables,
published through a transactional outbox, and advanced through restart-safe
Valkey/BullMQ deadlines.
The Svelte PWA supports private/public room creation, public discovery, room-code joining, Socket.IO synchronization, host controls, rematches, and mobile private play. Browser storage contains only room capabilities, never an active role or word.
Administrators manage policies, packs, immutable reviewed cards, DB-backed WN-LMF lexical sources and review imports, bulk entries, blocked/rejected terms, freezing, tombstones, reports, archives, redacted inspection, forensic audit events with request correlation and before/after state, indexed service logs with a configurable unified retention limit, and aggregate analytics. Administrator sessions use CSRF protection, RBAC, rate limits, TOTP MFA, and WebAuthn passkeys.
See the MVP plan for the full product contract and the verified roadmap for the current release boundary.
Local production-style stack
Create a mode-0600 .env from the example and replace every blank secret
with a distinct URL-safe random value (for example, openssl rand -hex 32):
install -m 600 .env.example .env
POSTGRES_OWNER_PASSWORD, APP_DATABASE_PASSWORD, LEXICAL_DATABASE_PASSWORD, ADMIN_BOOTSTRAP_TOKEN, and ADMIN_MFA_SECRET are
required, as is VALKEY_PASSWORD; Compose deliberately refuses to start when
they are missing, weak, or still use a published placeholder. Set
PUBLIC_ORIGIN to the exact browser origin used for passkeys.
docker compose up --build
The web app is available at http://127.0.0.1:8080 by default. This is a
loopback-only local stack, not a public deployment. Public deployments must put
the web service behind TLS, set an https:// PUBLIC_ORIGIN, and leave
ALLOW_INSECURE_LOCAL_HTTP unset or false. Bootstrap the
first administrator using ADMIN_BOOTSTRAP_TOKEN, then sign in at /#admin.
A fresh installation intentionally has no playable cards: create or review one
before starting a room.
Secure internet deployment
Run the Compose stack on a host you control and expose only a TLS-terminating reverse proxy to the internet. Do not publish the Compose web port on every interface and do not publish the server, PostgreSQL, or Valkey ports.
-
Create a public DNS record, for example
game.example.com, pointing to the host. Allow inbound TCP ports 80 and 443 only for the reverse proxy. -
Create a production
.envwith unique, randomly generated values. Keep it readable only by the deployment account:Create the file with
install -m 600 .env.example .env, then set:POSTGRES_OWNER_PASSWORD=<long-random-owner-password> APP_DATABASE_PASSWORD=<different-long-random-runtime-password> LEXICAL_DATABASE_PASSWORD=<third-long-random-worker-password> VALKEY_PASSWORD=<at-least-24-random-characters> ADMIN_BOOTSTRAP_TOKEN=<at-least-24-random-characters> ADMIN_MFA_SECRET=<at-least-32-random-characters> PUBLIC_ORIGIN=https://game.example.com WEB_BIND_ADDRESS=127.0.0.1:8080 ALLOW_INSECURE_LOCAL_HTTP=falsePUBLIC_ORIGINmust exactly match the public HTTPS origin; WebAuthn passkeys depend on it. Production startup rejects a non-HTTPS public origin. Rotate the bootstrap token after the first administrator has been provisioned. -
Start the private application stack:
docker compose up -d --build docker compose ps curl --fail http://127.0.0.1:8080/ready -
Put a TLS proxy in front of the loopback listener. Caddy is a simple option: it obtains and renews certificates automatically and proxies Socket.IO WebSockets without additional directives. Install Caddy on the host and use this
/etc/caddy/Caddyfile:game.example.com { reverse_proxy 127.0.0.1:8080 header Strict-Transport-Security "max-age=31536000; includeSubDomains" }Reload Caddy after replacing the hostname:
sudo caddy validate --config /etc/caddy/Caddyfile sudo systemctl reload caddyThe TLS proxy must preserve
Host, proxy WebSocket upgrades, and replace untrusted client forwarding headers; Caddy does this by default. Bundled Nginx acceptsX-Forwarded-Foronly from loopback or the local Docker bridge, resolves the client address recursively, and forwards one canonical address to the application. ConfigureTRUSTED_PROXY_CIDRSto only the Compose proxy path and never trust forwarding headers from arbitrary public peers. -
Verify the public endpoint before inviting users:
curl --fail --head https://game.example.com/ curl --fail https://game.example.com/readyConfirm that HTTPS is enforced,
Strict-Transport-Securityis present at the TLS edge, and the browser can create and join a room over WebSockets. Keep regular encrypted backups of the PostgreSQL volume and test restoring them before relying on the service.
WN-LMF lexical imports
The administrator source registry accepts WN-LMF 1.1–1.4 XML, gzip, or tar.xz artifacts. Uploads and exact allowlisted HTTPS/GitHub acquisitions are processed asynchronously by the isolated lexical worker. No release creates a batch or publishes a card automatically; editors map semantic values, build a filtered batch into a live same-language pack, and review each candidate.
Optionally preflight a licensed local release before uploading it:
pnpm run prepare:lexicon -- --release /path/to/wordnet.xml.gz --language en
Downloads enforce per-source HTTPS host allowlists, DNS and redirect checks,
and limits of 32 MiB compressed, 256 MiB expanded, and 500,000 entries.
Discovery and indexing progress is pushed live to connected administrators.
The unified service log exposes redacted structured lexical-worker stages and
failures; the same events remain available to container operators with
docker compose logs lexical-worker. Application entries contain Fastify
lifecycle/request context, while HTTP entries highlight unsuccessful and slow
responses without retaining credentials, request bodies, URL queries, or
client network addresses.
Verification
pnpm run typecheck
pnpm test
pnpm run build
pnpm run verify:pwa
pnpm run verify:compose
POSTGRES_OWNER_PASSWORD='...' APP_DATABASE_PASSWORD='...' LEXICAL_DATABASE_PASSWORD='...' ADMIN_BOOTSTRAP_TOKEN='...' ADMIN_MFA_SECRET='...' docker compose config
The API contract is at /openapi.json; liveness and readiness are /health
and /ready.
License
Licensed under the Apache License 2.0.