# Felt — Repository Structure ## Monorepo Layout ``` felt/ ├── .forgejo/ │ └── workflows/ # CI/CD pipelines │ ├── lint.yml │ ├── test.yml │ ├── build.yml │ └── release.yml │ ├── cmd/ # Go entrypoints (main packages) │ ├── felt-core/ # Core cloud service │ │ └── main.go │ ├── felt-leaf/ # Leaf node service │ │ └── main.go │ └── felt-cli/ # Management CLI tool │ └── main.go │ ├── internal/ # Private Go packages (not importable externally) │ ├── auth/ # Authentication (PIN, JWT, OIDC) │ │ ├── jwt.go │ │ ├── pin.go │ │ ├── oidc.go │ │ ├── middleware.go │ │ └── roles.go │ │ │ ├── tournament/ # Tournament engine (core domain) │ │ ├── engine.go # Clock, state machine, level progression │ │ ├── clock.go # Millisecond-precision countdown │ │ ├── blinds.go # Blind structure, wizard, templates │ │ ├── financial.go # Buy-in, rebuy, add-on, bounty, prize pool │ │ ├── payout.go # Prize calculation, ICM, chop │ │ ├── receipt.go # Transaction receipts │ │ └── engine_test.go │ │ │ ├── player/ # Player management │ │ ├── database.go # CRUD, search, import │ │ ├── tournament.go # In-tournament player state │ │ ├── bust.go # Bust-out, undo, ranking │ │ └── player_test.go │ │ │ ├── table/ # Table & seating │ │ ├── seating.go # Random seat, manual moves │ │ ├── balance.go # Auto-balance algorithm │ │ ├── layout.go # Table definitions, blueprints │ │ └── balance_test.go │ │ │ ├── league/ # League & points system │ │ ├── league.go # League, season, standings │ │ ├── formula.go # Formula parser & evaluator (sandboxed) │ │ ├── formula_test.go │ │ └── achievements.go │ │ │ ├── events/ # Events & automation engine │ │ ├── engine.go # Trigger → condition → action pipeline │ │ ├── triggers.go # Trigger definitions │ │ ├── actions.go # Action executors (sound, message, view change) │ │ └── rules.go # Rule storage & builder │ │ │ ├── display/ # Display node management │ │ ├── registry.go # Node discovery, heartbeat, status │ │ ├── routing.go # View assignment, cycling, multi-tournament │ │ └── protocol.go # WebSocket protocol (state push, assign, heartbeat) │ │ │ ├── content/ # Digital signage & info screen system │ │ ├── editor.go # WYSIWYG content CRUD (templates, custom content) │ │ ├── playlist.go # Playlist management, scheduling, priority │ │ ├── renderer.go # Template → HTML/CSS bundle generation │ │ ├── ai.go # AI-assisted content generation (prompt → layout) │ │ └── templates/ # Built-in content templates (events, menus, promos) │ │ │ ├── import/ # Data import from external systems │ │ ├── tdd.go # TDD XML parser + converter (templates, players, history) │ │ ├── csv.go # Generic CSV import (players, results) │ │ └── wizard.go # Import wizard logic (preview, mapping, confirmation) │ │ │ ├── sync/ # Leaf ↔ Core sync engine │ │ ├── publisher.go # Leaf: publish events to NATS JetStream │ │ ├── consumer.go # Core: consume events, upsert to PostgreSQL │ │ ├── reconcile.go # Conflict resolution, reverse sync │ │ └── messages.go # Message type definitions │ │ # NOTE: No relay needed — Netbird reverse proxy tunnels │ │ # player traffic directly to Leaf via WireGuard │ │ │ ├── websocket/ # WebSocket hub │ │ ├── hub.go # Connection manager, broadcast, rooms │ │ ├── operator.go # Operator channel (full control) │ │ ├── display.go # Display node channel (view data) │ │ ├── player.go # Player channel (read-only) │ │ └── hub_test.go │ │ │ ├── api/ # HTTP API layer │ │ ├── router.go # chi router setup, middleware chain │ │ ├── tournaments.go # Tournament endpoints │ │ ├── players.go # Player endpoints (in-tournament + database) │ │ ├── tables.go # Table/seating endpoints │ │ ├── displays.go # Display node endpoints │ │ ├── leagues.go # League/standings endpoints │ │ ├── export.go # Export endpoints (CSV, JSON, HTML) │ │ └── health.go # Health check, version, status │ │ │ ├── store/ # Database access layer │ │ ├── libsql.go # LibSQL connection, migrations (Leaf) │ │ ├── postgres.go # PostgreSQL connection, migrations (Core) │ │ ├── migrations/ # SQL migration files │ │ │ ├── leaf/ │ │ │ │ ├── 001_initial.sql │ │ │ │ └── ... │ │ │ └── core/ │ │ │ ├── 001_initial.sql │ │ │ └── ... │ │ ├── queries/ # SQL queries (sqlc or hand-written) │ │ │ ├── tournaments.sql │ │ │ ├── players.sql │ │ │ ├── tables.sql │ │ │ ├── transactions.sql │ │ │ └── leagues.sql │ │ └── store.go # Store interface (implemented by libsql + postgres) │ │ │ ├── audit/ # Audit trail │ │ ├── logger.go # Append-only audit record writer │ │ └── types.go # Audit event types │ │ │ ├── audio/ # Sound playback (Leaf only) │ │ └── player.go # mpv subprocess, sound queue │ │ │ └── config/ # Configuration │ ├── config.go # Typed config struct, env var loading │ ├── defaults.go # Default values │ └── validate.go # Config validation │ ├── pkg/ # Public Go packages (shared types, importable) │ ├── models/ # Shared domain models │ │ ├── tournament.go │ │ ├── player.go │ │ ├── table.go │ │ ├── transaction.go │ │ ├── league.go │ │ ├── display.go │ │ └── event.go │ │ │ └── protocol/ # WebSocket message types (shared by Go + JS) │ ├── messages.go │ └── messages.json # JSON Schema (for JS client codegen) │ ├── web/ # Frontend (SvelteKit) │ ├── operator/ # Operator UI (mobile-first, served by Leaf + Core) │ │ ├── src/ │ │ │ ├── lib/ │ │ │ │ ├── components/ # Reusable UI components │ │ │ │ │ ├── Clock.svelte │ │ │ │ │ ├── PlayerList.svelte │ │ │ │ │ ├── SeatingChart.svelte │ │ │ │ │ ├── BlindsSchedule.svelte │ │ │ │ │ ├── QuickActions.svelte │ │ │ │ │ ├── Toast.svelte │ │ │ │ │ ├── Badge.svelte │ │ │ │ │ ├── Card.svelte │ │ │ │ │ ├── DataTable.svelte │ │ │ │ │ └── ... │ │ │ │ ├── stores/ # Svelte stores (WebSocket state) │ │ │ │ │ ├── ws.ts # WebSocket connection manager │ │ │ │ │ ├── tournament.ts # Tournament state store │ │ │ │ │ ├── players.ts │ │ │ │ │ ├── tables.ts │ │ │ │ │ └── displays.ts │ │ │ │ ├── api/ # REST API client │ │ │ │ │ └── client.ts │ │ │ │ ├── theme/ # Design system │ │ │ │ │ ├── tokens.css # CSS custom properties (Catppuccin Mocha) │ │ │ │ │ ├── typography.css │ │ │ │ │ ├── components.css │ │ │ │ │ └── themes.ts # Theme definitions (dark/light/custom) │ │ │ │ └── utils/ │ │ │ │ ├── format.ts # Money, chip count, time formatting │ │ │ │ └── localFirst.ts # Optional: try felt.local before proxy URL │ │ │ ├── routes/ │ │ │ │ ├── +layout.svelte # Root layout (persistent header, nav) │ │ │ │ ├── +page.svelte # Dashboard / tournament selector │ │ │ │ ├── login/ │ │ │ │ │ └── +page.svelte # PIN / SSO login │ │ │ │ ├── tournament/[id]/ │ │ │ │ │ ├── +layout.svelte # Tournament layout (header + tabs) │ │ │ │ │ ├── +page.svelte # Overview dashboard │ │ │ │ │ ├── players/ │ │ │ │ │ │ └── +page.svelte # Player list + actions │ │ │ │ │ ├── tables/ │ │ │ │ │ │ └── +page.svelte # Seating chart │ │ │ │ │ ├── clock/ │ │ │ │ │ │ └── +page.svelte # Clock control │ │ │ │ │ ├── financials/ │ │ │ │ │ │ └── +page.svelte # Prize pool, transactions │ │ │ │ │ └── settings/ │ │ │ │ │ └── +page.svelte # Tournament config │ │ │ │ ├── displays/ │ │ │ │ │ └── +page.svelte # Display node management │ │ │ │ ├── players/ │ │ │ │ │ └── +page.svelte # Player database │ │ │ │ ├── leagues/ │ │ │ │ │ └── +page.svelte # League management │ │ │ │ └── settings/ │ │ │ │ └── +page.svelte # Venue settings │ │ │ └── app.html │ │ ├── static/ │ │ │ ├── sounds/ # Default sound files │ │ │ └── fonts/ # Inter, JetBrains Mono (self-hosted) │ │ ├── svelte.config.js │ │ ├── vite.config.js │ │ ├── tailwind.config.js # Catppuccin color tokens │ │ ├── package.json │ │ └── tsconfig.json │ │ │ ├── player/ # Player mobile PWA (shared components with operator) │ │ ├── src/ │ │ │ ├── lib/ │ │ │ │ ├── components/ # Subset of operator components │ │ │ │ ├── stores/ │ │ │ │ │ └── ws.ts # WebSocket with smart routing │ │ │ │ └── theme/ # Shared theme tokens │ │ │ ├── routes/ │ │ │ │ ├── +layout.svelte │ │ │ │ ├── +page.svelte # Clock + blinds (default view) │ │ │ │ ├── schedule/ │ │ │ │ │ └── +page.svelte # Blind schedule │ │ │ │ ├── rankings/ │ │ │ │ │ └── +page.svelte # Live rankings │ │ │ │ ├── payouts/ │ │ │ │ │ └── +page.svelte # Prize structure │ │ │ │ ├── league/ │ │ │ │ │ └── +page.svelte # League standings │ │ │ │ └── me/ │ │ │ │ └── +page.svelte # Personal status (PIN-gated) │ │ │ └── app.html │ │ ├── static/ │ │ │ └── manifest.json # PWA manifest │ │ ├── svelte.config.js │ │ ├── package.json │ │ └── tsconfig.json │ │ │ └── display/ # Display node views (vanilla HTML/CSS/JS) │ ├── views/ │ │ ├── clock/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── clock.js │ │ ├── rankings/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── rankings.js │ │ ├── seating/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── seating.js │ │ ├── schedule/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── schedule.js │ │ ├── lobby/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── lobby.js │ │ ├── prizepool/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── prizepool.js │ │ ├── movement/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── movement.js │ │ ├── league/ │ │ │ ├── index.html │ │ │ ├── style.css │ │ │ └── league.js │ │ └── welcome/ │ │ ├── index.html │ │ ├── style.css │ │ └── welcome.js │ ├── shared/ │ │ ├── ws-client.js # WebSocket client (reconnect, interpolation) │ │ ├── theme.css # Shared display theme (Catppuccin) │ │ ├── typography.css │ │ └── animations.css # Level transitions, bust-out effects │ └── README.md │ ├── deploy/ # Deployment & infrastructure │ ├── leaf/ # Leaf node OS image & config │ │ ├── Makefile # Build Felt OS image (Armbian-based) │ │ ├── overlay/ # Files overlaid on base OS │ │ │ ├── etc/ │ │ │ │ ├── systemd/system/felt-leaf.service │ │ │ │ ├── systemd/system/felt-nats.service │ │ │ │ ├── nftables.conf │ │ │ │ └── netbird/ │ │ │ └── opt/felt/ │ │ │ └── config.toml.example │ │ └── scripts/ │ │ ├── setup-wizard.sh │ │ └── update.sh │ │ │ ├── display/ # Display node OS image & config │ │ ├── Makefile │ │ ├── overlay/ │ │ │ ├── etc/ │ │ │ │ ├── systemd/system/felt-kiosk.service │ │ │ │ ├── chromium-flags.conf │ │ │ │ └── netbird/ │ │ │ └── opt/felt-display/ │ │ │ └── boot.sh │ │ └── scripts/ │ │ └── provision.sh │ │ │ ├── core/ # Core infrastructure (PVE LXC/VM configs) │ │ ├── ansible/ # Ansible playbooks for PVE provisioning │ │ │ ├── inventory/ │ │ │ │ ├── dev.yml │ │ │ │ └── production.yml │ │ │ ├── playbooks/ │ │ │ │ ├── setup-pve.yml │ │ │ │ ├── deploy-core-api.yml │ │ │ │ ├── deploy-nats.yml │ │ │ │ ├── deploy-authentik.yml │ │ │ │ ├── deploy-netbird.yml # Unified server + reverse proxy + Traefik │ │ │ │ ├── deploy-postgres.yml │ │ │ │ └── deploy-pbs.yml │ │ │ └── roles/ │ │ │ └── ... │ │ ├── pve-templates/ # LXC/VM config templates │ │ │ ├── felt-core-api.conf │ │ │ ├── felt-nats.conf │ │ │ ├── felt-postgres.conf │ │ │ └── ... │ │ └── docker-compose/ # For services that run in Docker (Authentik) │ │ └── authentik/ │ │ └── docker-compose.yml │ │ │ └── backup/ # Backup configuration │ ├── pbs-config.md # PBS setup documentation │ ├── wal-archive.sh # PostgreSQL WAL archiving script │ └── restore-runbook.md │ ├── docs/ # Documentation │ ├── spec/ │ │ └── felt_phase1_spec.md # This product spec (living document) │ ├── architecture/ │ │ ├── decisions/ # Architecture Decision Records (ADR) │ │ │ ├── 001-monorepo.md │ │ │ ├── 002-go-backend.md │ │ │ ├── 003-libsql-leaf.md │ │ │ ├── 004-nats-sync.md │ │ │ ├── 005-authentik-idp.md │ │ │ ├── 006-pve-core.md │ │ │ └── 007-sveltekit-ui.md │ │ └── diagrams/ │ ├── api/ # API documentation (auto-generated from Go) │ ├── operator-guide/ # End-user documentation │ └── security/ │ ├── threat-model.md │ └── incident-response.md │ ├── templates/ # Tournament templates & presets │ ├── blind-structures/ │ │ ├── turbo.json │ │ ├── standard.json │ │ ├── deepstack.json │ │ └── wsop-style.json │ └── event-rules/ │ └── defaults.json │ ├── testdata/ # Test fixtures │ ├── players.csv │ ├── tournaments/ │ └── blind-structures/ │ ├── go.mod ├── go.sum ├── Makefile # Top-level build orchestration ├── .env.example ├── .gitignore ├── LICENSE └── README.md ``` ## Build Targets (Makefile) ```makefile # Go backends build-leaf: Cross-compile felt-leaf for linux/arm64 build-core: Build felt-core for linux/amd64 build-cli: Build felt-cli for current platform # Frontends build-operator: SvelteKit → static SPA → embed in Go binary build-player: SvelteKit → static PWA → embed in Go binary build-display: Copy display views → embed in Go binary # Combined build-all: All of the above build-leaf-full: build-operator + build-player + build-display + build-leaf → single felt-leaf binary with all web assets embedded # Testing test: Go tests + Svelte tests test-go: go test ./... test-web: npm test in each web/ subdirectory lint: golangci-lint + eslint + prettier # Database migrate-leaf: Run LibSQL migrations migrate-core: Run PostgreSQL migrations # Deployment deploy-core: Ansible playbook for Core services deploy-leaf-image: Build Felt OS image for Leaf SBC deploy-display-image: Build display node image # Development dev-leaf: Run felt-leaf locally (with hot reload via air) dev-operator: Run SvelteKit operator UI in dev mode dev-player: Run SvelteKit player PWA in dev mode dev-core: Run felt-core locally ``` ## Key Design Decisions ### Single Binary Deployment (Leaf) The Leaf node ships as a single Go binary with all web assets embedded via `go:embed`: ```go //go:embed web/operator/build web/player/build web/display var webAssets embed.FS ``` This means: - Flash SSD → boot → one binary serves everything - No npm, no node_modules, no webpack on the Pi - Atomic updates: replace one binary, restart service - Rollback: keep previous binary, switch systemd symlink ### Shared Types Between Go and JavaScript The `pkg/protocol/messages.json` file is a JSON Schema that defines all WebSocket message types. This is the single source of truth consumed by: - Go: generated types via tooling - TypeScript: generated types for Svelte stores - Display views: lightweight type checking ### Database Abstraction The `internal/store/store.go` defines a `Store` interface. Both LibSQL (Leaf) and PostgreSQL (Core) implement this interface. The tournament engine, player management, etc. all depend on the interface, not on a specific database. ```go type Store interface { // Tournaments CreateTournament(ctx context.Context, t *models.Tournament) error GetTournament(ctx context.Context, id string) (*models.Tournament, error) // ... // Players GetPlayer(ctx context.Context, id string) (*models.Player, error) SearchPlayers(ctx context.Context, query string) ([]*models.Player, error) // ... } ``` This allows the same domain logic to run on Leaf (LibSQL) and Core (PostgreSQL) without code duplication. ### Development Workflow with Claude Code The monorepo is designed for Claude Code's agentic workflow: 1. **Context**: Claude Code can read the spec in `docs/spec/`, understand the architecture from ADRs, and see the full codebase 2. **Atomicity**: A feature that touches Go API + Svelte UI + display view is one commit 3. **Testing**: `make test` from root runs everything 4. **Building**: `make build-leaf-full` produces a deployable binary ### Forgejo CI `.forgejo/workflows/` contains pipelines that: - Lint Go + JS on every push - Run tests on every PR - Build binaries on merge to main - Build OS images on release tag