# Scoreboard Tools A self-hosted, real-time **Super Smash Bros Ultimate** tournament scoreboard PWA. Log in, build scoreboards, edit a customizable scorebug, open it as a live OBS overlay, and hand co-scorekeeping to someone else via a no-login link. The full design rationale is in [`docs/PLAN.md`](docs/PLAN.md). ## Stack | Layer | Choice | | ----------- | --------------------------------------------------------------------- | | Web | React + Vite + TypeScript + Tailwind, installable PWA (`vite-plugin-pwa`) | | Server | Fastify (REST) + raw `ws` WebSocket, run via `tsx` | | Persistence | Postgres via Drizzle ORM (`postgres.js` driver) | | Shared | `@sbt/shared` — zod schema + types for state and the WS protocol | | Deploy | Docker Compose (db + server) behind your nginx/Caddy TLS reverse proxy | Monorepo via pnpm workspaces: `apps/web`, `apps/server`, `packages/shared`. ## The real-time model (the important bit) The WebSocket server holds each scoreboard's state **in memory** as the single source of truth. Editors push full-state `update` messages; the server applies them, **broadcasts** to every client in that board's room (editors + OBS overlays) for instant updates, and **debounces** a Postgres save (~750ms) so we never write per-keystroke. One hook — `apps/web/src/hooks/useScoreboardSync.ts` — powers all three consumers; only the connect credential and role differ: | Consumer | Connects with | Role | | -------------- | -------------------- | -------- | | Owner editor | `?board=` (cookie) | editor | | Co-scorekeeper | `?control=` | editor | | OBS overlay | `?overlay=` | viewer (read-only) | ## Local development Requires Node 20+, pnpm, and a Postgres on `localhost:5432` (or use the compose `db`). ```bash pnpm install # point the server at a database (defaults to postgres://postgres:postgres@localhost:5432/scoreboardtools) export DATABASE_URL=postgres://postgres:postgres@localhost:5432/scoreboardtools export JWT_SECRET=dev-secret pnpm dev # runs web (:5173) and server (:3000) together ``` Open http://localhost:5173 — sign up, create a scoreboard, open it, and copy the overlay link into a second tab to watch real-time sync. Vite proxies `/api` and `/ws` to `:3000`. ```bash pnpm test # shared schema/protocol unit tests pnpm typecheck # type-check all workspaces ``` ## Production (self-hosted, Docker) ```bash cp deploy/.env.example .env # set JWT_SECRET (openssl rand -hex 32) and a DB password docker compose up -d --build # brings up Postgres + the server on 127.0.0.1:3000 ``` Then put your TLS reverse proxy in front (TLS is required — PWA install and OBS browser sources need HTTPS, and the WebSocket must be `wss://`): - **Caddy** (auto-TLS, simplest): see [`deploy/Caddyfile`](deploy/Caddyfile) - **nginx**: see [`deploy/nginx.conf`](deploy/nginx.conf) — note the `Upgrade`/`Connection` headers that let `/ws` pass through. The server container serves the built web app, the REST API, the WebSocket, and uploaded logos (persisted in the `uploads` volume). ## Status All six build phases from the plan are scaffolded and wired end to end. Next session picks up on the Linux server: `pnpm install`, bring up Docker, point the reverse proxy at it, and verify the overlay live in OBS. See `docs/PLAN.md` → Verification. Known follow-ups (intentionally deferred): - Character art is **placeholder initials** — map fighter ids to real stock icons later. - Schema is created via `ensureSchema()` on startup (idempotent `CREATE TABLE IF NOT EXISTS`); switch to Drizzle Kit migrations once the schema starts evolving. - `POST /api/uploads` is intentionally not auth-gated so no-login co-editors can set a logo.