2026-08-26 16:58:46 +02:00
2026-08-26 19:29:34 +02:00

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.

  1. 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.

  2. Create a production .env with 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=false
    

    PUBLIC_ORIGIN must 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.

  3. Start the private application stack:

    docker compose up -d --build
    docker compose ps
    curl --fail http://127.0.0.1:8080/ready
    
  4. 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 caddy
    

    The TLS proxy must preserve Host, proxy WebSocket upgrades, and replace untrusted client forwarding headers; Caddy does this by default. Bundled Nginx accepts X-Forwarded-For only from loopback or the local Docker bridge, resolves the client address recursively, and forwards one canonical address to the application. Configure TRUSTED_PROXY_CIDRS to only the Compose proxy path and never trust forwarding headers from arbitrary public peers.

  5. Verify the public endpoint before inviting users:

    curl --fail --head https://game.example.com/
    curl --fail https://game.example.com/ready
    

    Confirm that HTTPS is enforced, Strict-Transport-Security is 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.11.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.

Description
A selfhosted vibecoded web app to play "Who's the Impostor?"
Readme Apache-2.0 687 KiB
Languages
TypeScript 66.1%
Svelte 21.3%
CSS 9.4%
JavaScript 1.8%
Shell 1.1%
Other 0.2%