felt/.planning/phases/01-tournament-engine/01-PLAN-A.md
Mikkel Georgsen 21ff95068e docs(01): create Phase 1 plans (A-N) with research and feedback
14 plans in 6 waves covering all 68 requirements for the Tournament
Engine phase. Includes research (go-libsql, NATS JetStream, Svelte 5
runes, ICM complexity), plan verification (2 iterations), and user
feedback (hand-for-hand UX, SEAT-06 reword, re-entry semantics,
integration test, DKK defaults, JWT 7-day expiry, clock tap safety).

Wave structure:
  1: A (scaffold), B (schema)
  2: C (auth/audit), D (clock), E (templates), J (frontend scaffold)
  3: F (financial), H (seating), M (layout shell)
  4: G (player management)
  5: I (tournament lifecycle)
  6: K (overview/financials), L (players), N (tables/more)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:58:22 +01:00

6.8 KiB

Plan A: Project Scaffold + Core Infrastructure


wave: 1 depends_on: [] files_modified:

  • go.mod
  • go.sum
  • cmd/leaf/main.go
  • internal/server/server.go
  • internal/server/ws/hub.go
  • internal/server/middleware/auth.go
  • internal/server/middleware/role.go
  • internal/nats/embedded.go
  • internal/nats/publisher.go
  • internal/store/db.go
  • frontend/embed.go
  • Makefile autonomous: true requirements: [ARCH-01, ARCH-04, ARCH-05, ARCH-06, ARCH-07]

Goal

A Go binary starts in the LXC container, embeds NATS JetStream (sync_interval: always) and LibSQL, serves HTTP via chi, hosts a WebSocket hub that broadcasts to connected clients within 100ms, and serves a SvelteKit SPA stub via go:embed. This is the skeleton that every subsequent plan builds on.

Context

  • Greenfield project — no existing code
  • Dev environment: x86_64 LXC container (Debian/Ubuntu), Go 1.23+
  • go-libsql has no tagged releases — pin to commit hash in go.mod with comment
  • NATS JetStream sync_interval: always is mandatory (Jepsen 2025 finding for single-node)
  • coder/websocket v1.8.14 for WebSocket hub (not gorilla/websocket)
  • See 01-RESEARCH.md Pattern 1 (Embedded NATS), Pattern 2 (WebSocket Hub), Pattern 3 (SvelteKit Embed)

User Decisions (from CONTEXT.md)

  • The operator is the Tournament Director (TD) — use this term consistently
  • Multi-tournament support means all state must be tournament-scoped from day one (MULTI-01)
  • WebSocket hub must support tournament-scoped broadcasting

Tasks

Create the Go module at the project root. Install all core dependencies pinned to specific versions:
  • github.com/tursodatabase/go-libsql — pin to commit hash (check latest stable commit, add comment in go.mod)
  • github.com/nats-io/nats-server/v2 @ v2.12.4
  • github.com/nats-io/nats.go @ latest
  • github.com/coder/websocket @ v1.8.14
  • github.com/go-chi/chi/v5 @ v5.2.5
  • github.com/golang-jwt/jwt/v5 @ latest
  • golang.org/x/crypto @ latest
  • github.com/google/uuid @ latest
  • github.com/skip2/go-qrcode @ latest

Create the directory structure per 01-RESEARCH.md Recommended Project Structure. Include empty .go files with package declarations where needed to make the module compile.

Create a Makefile with targets:

  • make build — builds cmd/leaf binary
  • make run — builds and runs with default data dir
  • make test — runs all tests
  • make frontend — builds the SvelteKit frontend (placeholder — just creates build dir with stub index.html)
  • make all — frontend + build

The binary must compile and run (serving HTTP on :8080) with make run.

Verification: go build ./cmd/leaf/... succeeds. go vet ./... passes. Directory structure matches the recommended layout.

Wire up all core infrastructure in `cmd/leaf/main.go` and the `internal/` packages:

1. Embedded NATS Server (internal/nats/embedded.go):

  • Start NATS server in-process with DontListen: true (no TCP listener)
  • JetStream enabled with JetStreamSyncInterval: 0 (sync_interval: always — MANDATORY per Jepsen finding)
  • Memory limit: 64MB, Disk limit: 1GB
  • Wait for ready with 5-second timeout
  • Clean shutdown on context cancellation
  • Create initial JetStream streams: AUDIT (subject: tournament.*.audit), STATE (subject: tournament.*.state.*)

2. NATS Publisher (internal/nats/publisher.go):

  • Publish function that writes to JetStream with subject routing
  • Helper to publish tournament-scoped events: Publish(ctx, tournamentID, subject, data)

3. LibSQL Database (internal/store/db.go):

  • Open LibSQL with sql.Open("libsql", "file:"+dbPath)
  • Enable WAL mode: PRAGMA journal_mode=WAL
  • Enable foreign keys: PRAGMA foreign_keys=ON
  • Configurable data directory (flag or env var)
  • Close on context cancellation

4. WebSocket Hub (internal/server/ws/hub.go):

  • Connection registry with mutex protection
  • Client struct: conn, tournamentID (subscription scope), send channel (buffered 256)
  • Register(client) / Unregister(client)
  • Broadcast(tournamentID, messageType, data) — sends to all clients subscribed to that tournament (or all if tournamentID is empty)
  • On connect: accept WebSocket, register client, send initial state snapshot (stub for now — just a "connected" message)
  • Read pump and write pump goroutines per client
  • Drop messages for slow consumers (non-blocking channel send)
  • Graceful shutdown: close all connections

5. HTTP Server (internal/server/server.go):

  • chi router with middleware: logger, recoverer, request ID, CORS (permissive for dev)
  • Route groups:
    • /api/v1/ — REST API endpoints (stub 200 responses for now)
    • /ws — WebSocket upgrade endpoint → Hub.HandleConnect
    • /* — SvelteKit SPA fallback handler
  • Configurable listen address (default :8080)
  • Graceful shutdown with context

6. SvelteKit Embed Stub (frontend/embed.go):

  • //go:embed all:build directive
  • Handler() function that serves static files with SPA fallback
  • For now, create a frontend/build/index.html stub file with minimal HTML that says "Felt — Loading..."

7. Main (cmd/leaf/main.go):

  • Parse flags: --data-dir (default ./data), --addr (default :8080)
  • Startup order: LibSQL → NATS → Hub → HTTP Server
  • Signal handling (SIGINT, SIGTERM) with graceful shutdown in reverse order
  • Log startup and shutdown events

Verification:

  • make run starts the binary, prints startup log showing LibSQL opened, NATS ready, HTTP listening on :8080
  • curl http://localhost:8080/ returns the stub HTML
  • curl http://localhost:8080/api/v1/health returns 200 with JSON showing all subsystems operational (LibSQL: SELECT 1 succeeds, NATS server ready, WebSocket hub accepting connections)
  • A WebSocket client connecting to ws://localhost:8080/ws receives a connected message
  • CTRL+C triggers graceful shutdown with log messages

Verification Criteria

  1. go build ./cmd/leaf/... succeeds with zero warnings
  2. go vet ./... passes
  3. make run starts the binary and serves HTTP on :8080
  4. LibSQL database file created in data directory with WAL mode
  5. NATS JetStream streams (AUDIT, STATE) exist after startup
  6. WebSocket connection to /ws succeeds and receives initial message
  7. SPA fallback serves index.html for unknown routes
  8. Graceful shutdown on SIGINT/SIGTERM completes without errors

Must-Haves (Goal-Backward)

  • Go binary compiles and runs as a single process with all infrastructure embedded
  • NATS JetStream uses sync_interval: always (data durability guarantee)
  • WebSocket hub supports tournament-scoped broadcasting
  • SvelteKit SPA served via go:embed with fallback routing
  • All state is tournament-scoped from day one (no global singletons)