scoreboardtools/README.md
Ashraf Shafiq 9169bea79f Scaffold real-time SSBU scoreboard PWA
pnpm monorepo with three workspaces:
- @sbt/shared: zod ScoreboardState + WebSocket protocol (single source of truth)
- @sbt/server: Fastify REST + raw ws WebSocket + Drizzle/Postgres, run via tsx
- @sbt/web: React + Vite + Tailwind installable PWA

Real-time core: the WebSocket server holds authoritative per-board state in
memory, broadcasts to all clients (editors + OBS overlays) instantly, and
debounces Postgres saves (~750ms). One useScoreboardSync hook powers the editor,
the no-login co-edit control page, and the read-only OBS overlay.

Includes email+password auth (JWT cookie), scoreboard CRUD, logo upload,
customizable scorebug (characters/stocks/score/subtitles/callout/side-swap/theme),
Docker Compose + Caddy/nginx deploy configs, and docs/PLAN.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:09:27 -04:00

88 lines
3.8 KiB
Markdown

# 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=<id>` (cookie) | editor |
| Co-scorekeeper | `?control=<token>` | editor |
| OBS overlay | `?overlay=<token>` | 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.