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>
88 lines
3.8 KiB
Markdown
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.
|