# 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)