From a254f79bd28ea9aea16af9554bd769337eb2695c Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Sat, 28 Feb 2026 15:37:21 +0100 Subject: [PATCH] Initial docs/project description including financial models --- docs/felt_capacity_model.py | 337 +++++ docs/felt_financials_v5.py | 109 ++ docs/felt_grand_vision.md | 726 +++++++++++ docs/felt_phase1_spec_v05.md | 2106 +++++++++++++++++++++++++++++++ docs/felt_phase2_spec.md | 552 ++++++++ docs/felt_phase3_spec.md | 894 +++++++++++++ docs/felt_pitch_deck.pptx | Bin 0 -> 755752 bytes docs/felt_pricing_model_v2.py | 323 +++++ docs/felt_repo_structure_v02.md | 485 +++++++ 9 files changed, 5532 insertions(+) create mode 100644 docs/felt_capacity_model.py create mode 100644 docs/felt_financials_v5.py create mode 100644 docs/felt_grand_vision.md create mode 100644 docs/felt_phase1_spec_v05.md create mode 100644 docs/felt_phase2_spec.md create mode 100644 docs/felt_phase3_spec.md create mode 100644 docs/felt_pitch_deck.pptx create mode 100644 docs/felt_pricing_model_v2.py create mode 100644 docs/felt_repo_structure_v02.md diff --git a/docs/felt_capacity_model.py b/docs/felt_capacity_model.py new file mode 100644 index 0000000..04da841 --- /dev/null +++ b/docs/felt_capacity_model.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +""" +Felt Infrastructure Capacity Planning + +Target server: Hetzner AX102 or similar +- 16c/32t (AMD Ryzen 9 7950X3D or similar) +- 64-128GB DDR5 RAM +- 2× 1TB NVMe (RAID1 or split) +- ~€100/mo + +Architecture: +1. Core Server(s) - handles NATS, PostgreSQL, API, admin dashboard +2. Virtual Leaf Server(s) - runs virtual Leaf instances for free tier +3. Pro venues have physical Leaf nodes — Core just receives NATS sync + +Key insight: Virtual Leafs are HEAVIER than Pro sync because they +run the full tournament engine. Pro Leafs run on-premise — Core +just stores their sync data. +""" + +print("=" * 70) +print("FELT INFRASTRUCTURE CAPACITY PLANNING") +print("=" * 70) + +print(""" +┌─────────────────────────────────────────────────────────────────┐ +│ ARCHITECTURE OVERVIEW │ +│ │ +│ FREE TIER (Virtual Leaf) PRO TIER (Physical Leaf) │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Go backend │ ← runs on │ Go backend │ ← runs on │ +│ │ SQLite DB │ OUR server │ SQLite DB │ THEIR hw │ +│ │ WebSocket │ │ NVMe storage │ │ +│ │ NATS client │ │ NATS client │ │ +│ └──────┬───────┘ └──────┬───────┘ │ +│ │ full engine │ sync only │ +│ ▼ ▼ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ CORE SERVER │ │ +│ │ NATS JetStream │ PostgreSQL │ API │ Admin UI │ │ +│ └────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +""") + +# ================================================================ +# VIRTUAL LEAF RESOURCE ESTIMATES +# ================================================================ +print("=" * 70) +print("VIRTUAL LEAF (Free Tier) — Resource Estimates") +print("=" * 70) + +print(""" +Each Virtual Leaf runs: + - Go binary (tournament engine + HTTP server + WebSocket hub) + - SQLite database (venue's tournaments, players, results) + - NATS client (sync to Core when needed) + - WebSocket connections (operator UI + player mobile + display proxy) + +Per Virtual Leaf process: +""") + +# Memory estimates +go_binary_base = 30 # MB - Go runtime + loaded binary +sqlite_working_set = 10 # MB - SQLite in-memory cache (small venues) +websocket_per_conn = 0.05 # MB per WebSocket connection +avg_concurrent_ws = 20 # During a tournament: 1 operator + ~15 players + displays +ws_memory = websocket_per_conn * avg_concurrent_ws +nats_client = 5 # MB - NATS client overhead +http_buffers = 10 # MB - HTTP server buffers, template cache +signage_content = 5 # MB - Cached signage content bundles + +total_per_vleaf_active = go_binary_base + sqlite_working_set + ws_memory + nats_client + http_buffers + signage_content +total_per_vleaf_idle = go_binary_base + 5 + nats_client # Idle: minimal working set + +print(f" Component Active Idle") +print(f" ─────────────────────────────────────────────") +print(f" Go runtime + binary {go_binary_base:>5} MB {go_binary_base:>5} MB") +print(f" SQLite working set {sqlite_working_set:>5} MB {5:>5} MB") +print(f" WebSocket ({avg_concurrent_ws} conns) {ws_memory:>5.1f} MB {0:>5} MB") +print(f" NATS client {nats_client:>5} MB {nats_client:>5} MB") +print(f" HTTP buffers/cache {http_buffers:>5} MB {2:>5} MB") +print(f" Signage content cache {signage_content:>5} MB {0:>5} MB") +print(f" ─────────────────────────────────────────────") +print(f" TOTAL per Virtual Leaf {total_per_vleaf_active:>5.1f} MB {go_binary_base + 5 + nats_client:>5} MB") + +# CPU estimates +print(f""" + CPU usage: + Idle (no tournament): ~0.01 cores (just NATS heartbeat) + Active tournament: ~0.05-0.1 cores (timer ticks, WS pushes) + Peak (level change): ~0.2 cores (burst: recalculate, push all) + Signage editor (AI): ~0.5 cores (burst, rare) + + Disk usage per venue: + SQLite database: 1-50 MB (scales with history) + Signage content: 10-100 MB (images, templates) + Average per venue: ~50 MB +""") + +# ================================================================ +# PRO VENUE CORE LOAD +# ================================================================ +print("=" * 70) +print("PRO VENUE — Core Server Load (sync only)") +print("=" * 70) +print(""" +Pro venues run their own Leaf hardware. Core only handles: + - NATS JetStream: receive sync messages (tournament results, + player updates, financial data) — async, bursty, small payloads + - PostgreSQL: upsert synced data into venue's partition + - API: serve admin dashboard, player profiles, public venue page + - Reverse proxy config: Netbird routing for player/operator access + +Per Pro venue on Core: +""") + +nats_per_pro = 2 # MB - JetStream consumer state + buffers +pg_per_pro = 5 # MB - PostgreSQL working set per venue +api_per_pro = 1 # MB - Cached API responses +total_pro_core = nats_per_pro + pg_per_pro + api_per_pro + +print(f" Component Memory") +print(f" ───────────────────────────────────") +print(f" NATS consumer state {nats_per_pro:>5} MB") +print(f" PostgreSQL working set {pg_per_pro:>5} MB") +print(f" API cache {api_per_pro:>5} MB") +print(f" ───────────────────────────────────") +print(f" TOTAL per Pro venue {total_pro_core:>5} MB") + +print(f""" + CPU usage per Pro venue: + Idle (between syncs): ~0.001 cores (negligible) + Active sync burst: ~0.02 cores (deserialize + upsert) + API requests: ~0.01 cores (occasional dashboard/mobile) + + Disk usage per venue (PostgreSQL): + Average: 10-200 MB (scales with history) + With years of data: up to 500 MB +""") + +# ================================================================ +# SERVER CAPACITY CALCULATIONS +# ================================================================ +print("=" * 70) +print("SERVER CAPACITY — Hetzner 16c/32t, 64GB RAM, 2×1TB NVMe") +print("=" * 70) + +total_ram_mb = 64 * 1024 # 64 GB +total_cores = 32 # threads +total_disk_gb = 2000 # 2× 1TB (RAID1 = 1TB usable, or split = 2TB) +server_cost = 100 # €/mo + +# OS + base services overhead +os_overhead_ram = 2048 # MB +nats_server_ram = 512 # MB - NATS JetStream server +pg_server_ram = 4096 # MB - PostgreSQL shared buffers + overhead +netbird_ram = 256 # MB - Netbird controller +api_server_ram = 512 # MB - Core API + admin frontend +monitoring_ram = 512 # MB - Prometheus, logging +reserve_ram = 2048 # MB - headroom / burst + +base_overhead = os_overhead_ram + nats_server_ram + pg_server_ram + netbird_ram + api_server_ram + monitoring_ram + reserve_ram +available_ram = total_ram_mb - base_overhead + +os_cores = 1 +nats_cores = 1 +pg_cores = 2 +api_cores = 1 +base_cores = os_cores + nats_cores + pg_cores + api_cores +available_cores = total_cores - base_cores + +print(f"\n Base infrastructure overhead:") +print(f" OS + system: {os_overhead_ram:>6} MB {os_cores} cores") +print(f" NATS JetStream: {nats_server_ram:>6} MB {nats_cores} cores") +print(f" PostgreSQL: {pg_server_ram:>6} MB {pg_cores} cores") +print(f" Netbird controller: {netbird_ram:>6} MB") +print(f" Core API + admin: {api_server_ram:>6} MB {api_cores} cores") +print(f" Monitoring: {monitoring_ram:>6} MB") +print(f" Reserve/headroom: {reserve_ram:>6} MB") +print(f" ─────────────────────────────────────────") +print(f" Total overhead: {base_overhead:>6} MB {base_cores} cores") +print(f" Available for venues: {available_ram:>6} MB {available_cores} cores") +print(f" Available disk: ~{total_disk_gb // 2} GB (RAID1) or ~{total_disk_gb} GB (split)") + +# ================================================================ +# SCENARIO: DEDICATED VIRTUAL LEAF SERVER +# ================================================================ +print(f"\n{'─' * 70}") +print(f"SCENARIO A: Dedicated Virtual Leaf Server (free tier only)") +print(f"{'─' * 70}") + +# Key insight: most virtual Leafs are IDLE most of the time +# A venue running 3 tournaments/week has active tournaments maybe +# 12 hours/week = 7% of the time +active_ratio = 0.10 # 10% of virtual Leafs active at any given time + +# Memory: need to keep all Leafs loaded (Go processes) +# but SQLite working sets and WS connections only for active ones +mem_per_vleaf = total_per_vleaf_idle # Base: all idle +mem_per_active_delta = total_per_vleaf_active - total_per_vleaf_idle # Extra when active + +# CPU: only active Leafs consume meaningful CPU +cpu_per_active = 0.1 # cores per active tournament + +# Solve for max Virtual Leafs (memory-bound) +# N * idle_mem + N * active_ratio * active_delta <= available_ram +# N * (idle_mem + active_ratio * active_delta) <= available_ram +effective_mem_per_vleaf = mem_per_vleaf + active_ratio * mem_per_active_delta +max_vleaf_by_ram = int(available_ram / effective_mem_per_vleaf) + +# Check CPU bound +max_active = int(available_cores / cpu_per_active) +max_vleaf_by_cpu = int(max_active / active_ratio) + +max_vleaf = min(max_vleaf_by_ram, max_vleaf_by_cpu) + +print(f"\n Assumptions:") +print(f" Active ratio (tournaments running): {active_ratio:.0%}") +print(f" Memory per Leaf (idle): {mem_per_vleaf:.0f} MB") +print(f" Memory per Leaf (active delta): {mem_per_active_delta:.0f} MB") +print(f" Effective memory per Leaf: {effective_mem_per_vleaf:.0f} MB") +print(f" CPU per active tournament: {cpu_per_active} cores") +print(f"") +print(f" Capacity (RAM-limited): {max_vleaf_by_ram} Virtual Leafs") +print(f" Capacity (CPU-limited): {max_vleaf_by_cpu} Virtual Leafs") +print(f" ═══════════════════════════════════════════") +print(f" PRACTICAL CAPACITY: ~{max_vleaf} Virtual Leafs per server") +print(f" At {active_ratio:.0%} active: ~{int(max_vleaf * active_ratio)} concurrent tournaments") +print(f" Server cost: €{server_cost}/mo") +print(f" Cost per Virtual Leaf: €{server_cost/max_vleaf:.2f}/mo") + +# Disk check +avg_disk_per_vleaf = 50 # MB +total_disk_used = max_vleaf * avg_disk_per_vleaf / 1024 +print(f" Disk usage ({max_vleaf} venues): ~{total_disk_used:.0f} GB (well within {total_disk_gb//2} GB)") + +# ================================================================ +# SCENARIO: CORE SERVER (Pro venues + API + services) +# ================================================================ +print(f"\n{'─' * 70}") +print(f"SCENARIO B: Core Server (Pro venue sync + API + all services)") +print(f"{'─' * 70}") + +max_pro_by_ram = int(available_ram / total_pro_core) +max_pro_by_cpu = int(available_cores / 0.03) # ~0.03 cores average per Pro venue +max_pro = min(max_pro_by_ram, max_pro_by_cpu) + +print(f"\n Per Pro venue on Core: {total_pro_core} MB RAM, ~0.03 cores avg") +print(f" Capacity (RAM-limited): {max_pro_by_ram} Pro venues") +print(f" Capacity (CPU-limited): {max_pro_by_cpu} Pro venues") +print(f" ═══════════════════════════════════════════") +print(f" PRACTICAL CAPACITY: ~{min(max_pro, 5000)} Pro venues per server") +print(f" (PostgreSQL becomes the bottleneck before RAM/CPU)") +print(f" Server cost: €{server_cost}/mo") +print(f" Cost per Pro venue: €{server_cost/min(max_pro,2000):.2f}/mo") + +# ================================================================ +# COMBINED: REALISTIC DEPLOYMENT TOPOLOGY +# ================================================================ +print(f"\n{'=' * 70}") +print(f"RECOMMENDED DEPLOYMENT TOPOLOGY") +print(f"{'=' * 70}") + +print(f""" + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 1: Single Server (~€100/mo) │ + │ │ + │ One Hetzner box runs EVERYTHING: │ + │ Core (NATS + PostgreSQL + API) + Virtual Leafs │ + │ │ + │ Capacity: ~300-400 Virtual Leafs + ~200 Pro venues │ + │ This gets you through Year 1-2 easily. │ + │ │ + │ Actual cost per venue: │ + │ If 200 free + 20 Pro: €100 / 220 = €0.45/venue/mo │ + │ Way under the €4/mo we budgeted per free venue! │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 2: Split (2 servers, ~€200/mo) │ + │ │ + │ Server 1: Core (NATS + PG + API + Netbird controller) │ + │ Server 2: Virtual Leaf farm │ + │ │ + │ Capacity: ~{max_vleaf} Virtual Leafs + ~2000 Pro venues │ + │ Split when you hit ~400 free venues or need more RAM. │ + └─────────────────────────────────────────────────────────────┘ + + ┌─────────────────────────────────────────────────────────────┐ + │ PHASE 3: Regional (3+ servers, ~€300+/mo) │ + │ │ + │ EU: Core + VLeaf farm (Hetzner Falkenstein) │ + │ US: Core replica + VLeaf farm (Hetzner Ashburn) │ + │ APAC: Core replica + VLeaf farm (OVH Singapore) │ + │ │ + │ NATS super-cluster for cross-region sync │ + │ Player profiles: globally replicated │ + │ Venue data: stays in region (GDPR) │ + └─────────────────────────────────────────────────────────────┘ +""") + +# ================================================================ +# THE PUNCHLINE +# ================================================================ +print(f"{'=' * 70}") +print(f"THE PUNCHLINE") +print(f"{'=' * 70}") + +year1_free = 200 +year1_pro = 20 +year1_total = year1_free + year1_pro +year1_servers = 1 +year1_infra = 100 + +print(f""" + Year 1 reality: {year1_total} venues ({year1_free} free + {year1_pro} Pro) + Infrastructure: {year1_servers} server @ €{year1_infra}/mo + + Actual cost per free venue: €{year1_infra / year1_total:.2f}/mo (not €4!) + Actual cost per Pro venue: €{year1_infra / year1_total:.2f}/mo (not €6!) + + We budgeted €4/mo per free venue. Reality is €0.45/mo. + That means the free tier is ~9× cheaper than we estimated. + + The original financial model is CONSERVATIVE. + With real infrastructure costs: + + Year 1 net improvement: +€{(4 - year1_infra/year1_total) * year1_free * 12:.0f}/yr saved on free tier + + This doesn't change when you hit salary targets, + but it means your runway is much longer and your + margins are much better than the financial model suggests. + + One €100/mo server handles your first ~500 venues. + You won't need a second server until you're already profitable. +""") + diff --git a/docs/felt_financials_v5.py b/docs/felt_financials_v5.py new file mode 100644 index 0000000..98846a8 --- /dev/null +++ b/docs/felt_financials_v5.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Felt Financial Model v5 — Bare bones reality +Only costs that are truly required. No fluff. +""" + +EUR_TO_DKK = 7.45 + +print("=" * 75) +print("FELT FINANCIAL MODEL v5 — BARE BONES BOOTSTRAP") +print("=" * 75) + +# Year 1-2: absolute minimum +# Year 3+: add things as revenue justifies them + +phases = [ + { + "label": "Year 1-2 (Bootstrap)", + "costs": [ + ("Hetzner dedicated (16c/32t, 64GB, 2×1TB)", 100), + ("Domains (felt.io + a couple reserves)", 8), + ("Transactional email (Mailgun free tier → $15)", 10), + ], + }, + { + "label": "Year 3+ (Revenue covers it)", + "costs": [ + ("Hetzner primary", 100), + ("Hetzner backup/second server", 100), + ("Domains", 8), + ("Transactional email", 15), + ("SMS (Twilio)", 10), + ("Accounting (€1k/yr ÷ 12)", 83), + ("Insurance (if casino clients require it)", 50), + ], + }, +] + +for phase in phases: + total = sum(c for _, c in phase["costs"]) + print(f"\n {phase['label']}:") + for name, cost in phase["costs"]: + print(f" {name:<55s} €{cost:>5}/mo") + print(f" {'─' * 60}") + print(f" {'TOTAL:':<55s} €{total:>5}/mo = €{total*12:,}/yr") + +bootstrap_monthly = sum(c for _, c in phases[0]["costs"]) + +print(f"\n Your actual burn in Year 1-2: €{bootstrap_monthly}/mo") +print(f" That's {bootstrap_monthly * EUR_TO_DKK:.0f} DKK/mo. Less than a tournament buy-in.") + +# ================================================================ +# 5-YEAR WITH REAL COSTS +# ================================================================ +print(f"\n{'=' * 75}") +print("5-YEAR PROJECTION") +print(f"{'=' * 75}") + +years = [ + ("Year 1 — Build + DK early adopters", 40, 3, 2, 0, 0, 118, 0), + ("Year 2 — DK saturated, word of mouth", 120, 12, 6, 0, 0, 118, 1000), + ("Year 3 — Nordics, first casino", 300, 30, 18, 2, 0, 366, 1000), + ("Year 4 — N. Europe, casino growth", 550, 50, 35, 4, 3, 466, 1000), + ("Year 5 — International, lifestyle", 800, 70, 50, 6, 8, 566, 1000), +] +# columns: label, free, offline, pro, casino_starter, casino_pro_properties, monthly_fixed, annual_accounting + +cumulative = 0 + +for i, (label, free, off, pro, cs, cp, fixed, acct) in enumerate(years): + rev = off * 25 + pro * 100 + cs * 249 + cp * 499 + annual_rev = rev * 12 + + var = free * 0.45 + off * 0.65 + pro * 1.15 + cs * 5.15 + cp * 10.15 + annual_cost = (fixed + var) * 12 + acct + + net = annual_rev - annual_cost + cumulative += net + net_mo = net / 12 + + paying = off + pro + cs + cp + total = free + paying + + print(f"\n Year {i+1}: {label}") + print(f" {total} venues ({free} free, {paying} paying) | Fixed: €{fixed}/mo") + print(f" Revenue: €{rev:>7,}/mo = €{annual_rev:>8,}/yr") + print(f" Costs: €{fixed+var:>7,.0f}/mo = €{annual_cost:>8,.0f}/yr") + print(f" Net: €{net_mo:>7,.0f}/mo = €{net:>8,.0f}/yr (cum: €{cumulative:>8,.0f})") + + if net > 0: + trips = int(net / 1500) + dkk = net_mo * EUR_TO_DKK + print(f" → {dkk:,.0f} DKK/mo | ~{trips} poker weekends/yr funded") + +print(f"\n{'=' * 75}") +print("THAT'S IT.") +print(f"{'=' * 75}") +print(f""" + €{bootstrap_monthly}/mo gets you from zero to a proven product in Denmark. + + By Year 2 the business pays for itself. + By Year 3 it funds your poker travel. + By Year 4 it's real income. + By Year 5 it's a lifestyle business doing €130k+/yr on 90% margins. + + Total investment to get there: ~€{bootstrap_monthly * 18:,} over 18 months + before it's self-sustaining. That's the price of a used car. +""") + diff --git a/docs/felt_grand_vision.md b/docs/felt_grand_vision.md new file mode 100644 index 0000000..9116b5e --- /dev/null +++ b/docs/felt_grand_vision.md @@ -0,0 +1,726 @@ +# Felt — Grand Vision + +## The Operating System for Poker Venues + +**Version:** 1.0 +**Date:** 2026-02-28 + +--- + +## Table of Contents + +1. [The World We're Building](#1-the-world-were-building) +2. [Why This Doesn't Exist Yet](#2-why-this-doesnt-exist-yet) +3. [The Felt Platform](#3-the-felt-platform) +4. [The Three Phases](#4-the-three-phases) +5. [The Ecosystem](#5-the-ecosystem) +6. [Architecture Philosophy](#6-architecture-philosophy) +7. [Business Model](#7-business-model) +8. [Competitive Moats](#8-competitive-moats) +9. [Market Landscape](#9-market-landscape) +10. [Long-Term Value & Natural Exits](#10-long-term-value--natural-exits) +11. [Risks & Mitigations](#11-risks--mitigations) + +--- + +## 1. The World We're Building + +Imagine you own a poker room. Maybe it's a corner of a Copenhagen bar with three tables. Maybe it's a 40-table casino floor in Vegas. Either way, you run tournaments, cash games, leagues, and events — and you want every aspect of your operation managed by one system. + +Today, you can't have that. You run tournaments on a 15-year-old Windows desktop app. You manage your cash game waitlist on a whiteboard. You track league points in a spreadsheet. Your dealers text you when they can't make a shift. Your players have no idea what their tournament history looks like, and your TVs show tournament clocks via HDMI cables that fail every other week. + +**Felt replaces all of it.** + +One small device behind the bar. Wireless displays on every TV. Every player on their phone. Every dealer scheduled. Every dollar tracked. Every screen under your control — tournaments during play, drink specials during breaks, event promos after hours. Cloud-synced when online, fully autonomous when offline. + +The venue plugs in a box, connects to any internet, and they're live. No IT department. No Windows PC. No cables. No configuration. Just poker. + +--- + +## 2. Why This Doesn't Exist Yet + +Three things had to happen before Felt could exist: + +**Cheap, powerful ARM hardware.** A €100 single-board computer now outperforms the Windows PCs that venues currently use. A €20 display device replaces a €50+ HDMI cable run. This hardware price point didn't exist five years ago. It makes the "device per venue" model economically viable at scale. + +**Mature zero-trust mesh networking.** WireGuard-based overlay networks (specifically Netbird) now allow encrypted, NAT-traversed, plug-and-play networking. Every byte of Felt traffic flows through WireGuard tunnels — no firewall rules, no port forwarding, no static IPs, no IT involvement. A venue's network is irrelevant. This was impossible three years ago. + +**Live poker's post-pandemic boom.** Global live poker is expanding. New venues opening, existing venues upgrading, amateur leagues growing. The market is bigger than it's been in a decade, and the tools haven't kept up. TDD (The Tournament Director) last had a meaningful update around 2018. Blind Valet is shallow. Poker Atlas is discovery-only. No one is building a modern, integrated venue platform. + +The window is open. The technology is ready. The competition is asleep. + +--- + +## 3. The Felt Platform + +Felt is three things: + +### 3.1 A Hardware Platform + +**Leaf Node** — A small ARM64 SBC (~€100) that runs the entire venue. NVMe storage for reliability. Runs Go backend + lightweight frontend. Works completely offline during a tournament. This is the venue's brain. + +**Display Nodes** — €20 devices (Pi Zero or similar) that plug into any TV via HDMI. They connect wirelessly to the Leaf over local WiFi. They show tournament clocks, player rankings, seating charts, event promos, drink specials, sponsor ads, league standings — whatever the venue needs. No cables between Leaf and displays. One system for all screens. + +**Player Devices** — Players' own phones. They scan a QR code and get live tournament info, their personal stats, league standings, and mobile access to everything the venue shows on the big screens. No app to install — it's a PWA. + +### 3.2 A Software Platform + +The software spans three phases (see Section 4), but the architecture is unified from day one: + +- **Leaf software** — Go backend, embedded SQLite, lightweight web UI. Runs tournaments, cash games, displays, signage, and local operations. The single source of truth while the venue is operating. +- **Core software** — Go backend, PostgreSQL, SvelteKit admin dashboard. Handles cross-venue leagues, player profiles, analytics, remote management, public venue pages, and the API that ties everything together. +- **Player PWA** — SvelteKit progressive web app. Mobile-first. Real-time WebSocket updates. Works at the venue (local-first) or remotely (via cloud proxy). +- **WYSIWYG Content Editor** — Visual drag-and-drop editor with AI assistance for creating info screen content. Templates, venue branding auto-applied, AI-generated promo cards from text prompts. The venue owner doesn't need a graphic designer. + +### 3.3 A Network Platform + +All Felt traffic flows through a self-hosted Netbird mesh network: + +- **WireGuard encryption** — Every connection between every Felt device is encrypted. The venue's local network never sees Felt data. +- **NAT traversal** — Works on any internet connection. Coffee shop WiFi, hotel LAN, 4G hotspot, corporate firewall — doesn't matter. Netbird punches through. +- **Zero configuration** — The Leaf boots, connects to Netbird, and it's on the mesh. Display nodes discover the Leaf via mDNS locally. Players access via HTTPS through Netbird's reverse proxy. No ports, no IPs, no firewall rules. +- **Identity-aware SSH** — Admins access Leaf nodes remotely via Netbird SSH, authenticated through Authentik (OIDC). No SSH keys to manage. Onboarding/offboarding is instant. +- **Reverse proxy** — Player and operator access to Leaf nodes routes through Netbird's built-in reverse proxy with automatic TLS. Each venue gets `play.venue-name.felt.io` and `admin.venue-name.felt.io`. + +This makes Felt **network agnostic**. A Copenhagen bar, a Las Vegas ballroom, and a rural community center all work identically. We never have to ask a venue about their network. We never have to troubleshoot their firewall. We never have to send someone on-site to fix a cable. + +--- + +## 4. The Three Phases + +### Phase 1: Live Tournament Management *(In Development)* + +**Replaces:** The Tournament Director, Blind Valet + +This is the cornerstone. If we don't nail tournaments, nothing else matters. Phase 1 delivers a tournament engine that matches TDD's depth while feeling like a modern product. + +**Core capabilities:** +- Full tournament clock with blinds, antes, levels, breaks, chip-ups +- Financial engine: buy-ins, rebuys, add-ons, bounties (including progressive/mystery), payouts, prize pool calculation +- Player management: database, tournament registration, bust-out tracking, chip counts +- Table management: configurable layouts, random seating, automatic rebalancing, final table merge +- Multi-tournament: run multiple simultaneous tournaments +- League and season management: configurable point formulas, season standings, league prizes +- Wireless display system: tournament clocks, rankings, seating charts — no HDMI cables +- Digital signage: event promos, drink specials, sponsor ads, league standings — WYSIWYG editor with AI assist +- Player mobile access: QR code → live clock, personal stats, rankings +- Export: CSV, JSON, HTML — tournament results, financial reports, player records +- Events engine: triggers (level change, final table, break), sounds, screen messages +- Offline-first: entire tournament runs without internet. Syncs when connected. + +**What makes this better than TDD:** +- Runs on a €100 device, not a Windows PC +- Wireless displays, not HDMI cables +- Players on their phones, not staring at one screen +- Works on any network, not tied to a venue's infrastructure +- Cloud-synced for remote access and cross-venue features +- Modern, clean UI — not a 2002 Windows interface +- Digital signage built in — the screens work for the venue 24/7, not just during tournaments + +**Detailed specification:** See `felt_phase1_spec_v05.md` (2,000+ lines) + +--- + +### Phase 2: Cash Game Operations *(Planned)* + +**Replaces:** Whiteboards, spreadsheets, manual waitlist management + +Cash games are the daily bread of most poker rooms. Tournaments are events; cash games are the constant. Phase 2 brings cash game management into the same system. + +**Core capabilities:** +- **Waitlist management:** Players join waitlists by game type and stakes. List displayed on screens and accessible via mobile. Automated "your seat is ready" notifications via SMS/push. +- **Table management:** Open/close tables, assign game type and stakes, track seat availability in real-time. Visual table map showing which seats are occupied. +- **Game type registry:** Define all games the venue offers — No-Limit Hold'em, Pot-Limit Omaha, Limit Hold'em, Mixed games, etc. Each game type has configurable stakes, min/max buy-in, rake structure. +- **Session tracking:** Log player sessions — when they sat, when they left, duration, buy-in amounts, cash-out amounts (optional, privacy-respecting). Used for loyalty, analytics, and player history. +- **Table transfers:** Move players between tables for balancing, game changes, or seat preferences. Track the transfer in the player's session. +- **Player alerts:** Notify players when their preferred game opens, when a seat is available, or when their waitlist position is reached. Via mobile PWA push notification or SMS. +- **Rake tracking:** Configure rake structures per game type (percentage, cap, time rake). Track house revenue per table per session. Financial reporting for venue owners. +- **Table stakes display:** Display nodes show current games running, stakes, seat availability, and waitlist depth — updated in real-time. Players walking in can see what's available without asking. +- **Must-move tables:** When a main game is full and a must-move table is running, automate the progression. When a seat opens at the main game, the longest-waiting must-move player gets notified. +- **Seat change requests:** Players can request a seat change within the same game. System tracks the queue and notifies when the requested seat opens. + +**Integration with Phase 1:** +- Same Leaf device, same display nodes, same player database +- Tournament and cash game can run simultaneously — display nodes assigned to different views +- Player profiles show both tournament results and cash game session history +- Loyalty points accrue from both tournament and cash play +- Dealers assigned to cash tables use the same scheduling system (Phase 3) + +**Why this matters for venues:** +Cash games are where the rake comes from. Efficient waitlist management directly impacts revenue — a player who walks in, sees a full room, and leaves is lost revenue. A player who gets a text saying "your seat is ready in 5 minutes" stays for a drink and sits down. Felt turns that whiteboard into a system that actively keeps players in the building. + +**Detailed specification:** See `felt_phase2_spec.md` + +--- + +### Phase 3: Complete Venue Platform *(Vision)* + +**Replaces:** Separate scheduling tools, manual comp tracking, no loyalty system, Poker Atlas (operational side) + +Phase 3 turns Felt from a poker management system into a complete venue operating platform. This is the endgame — the reason a venue would never leave Felt. + +**Core capabilities:** + +#### Dealer Management +- **Scheduling & shift planning:** Weekly/monthly schedules. Shift templates (morning, afternoon, evening, tournament). Drag-and-drop scheduling interface. +- **Skill profiles:** Each dealer has tagged skills — game types they can deal (Hold'em, Omaha, PLO, Stud, Mixed, Short Deck, etc.), cash vs. tournament specialization, experience level. The system matches dealer skills to table needs. +- **Shift trading:** Dealers can post shifts for trade. Other qualified dealers (matching skill requirements) can pick them up. Manager approval optional. All logged and auditable. +- **Work hours tracking:** Clock-in/clock-out (via mobile or Leaf UI). Automatic calculation of hours worked, overtime, break compliance. Export for payroll integration. +- **Availability management:** Dealers set their availability windows. Scheduling respects these. Conflict detection when a shift is assigned to an unavailable dealer. +- **Performance & preferences:** Track which dealers are requested by players or preferred for high-stakes games. Optional player feedback integration. +- **Dealer rotation:** Automated dealer rotation for cash games (push after X minutes). Ensures fairness and prevents fatigue. Configurable per table/game type. + +#### Player Loyalty System +- **Points engine:** Configurable point accrual — per tournament entry, per cash game hour, per buy-in amount, per league participation. Different multipliers for different game types or time slots (e.g., 2x points on slow nights). +- **Tier system:** Bronze/Silver/Gold/Platinum (or custom names). Tier thresholds based on point accumulation. Each tier unlocks benefits. +- **Rewards catalog:** Venue-defined rewards — free tournament entries, food/drink comps, merchandise, seat upgrades, priority waitlist. Redeemable via mobile or at the bar. +- **Automated promotions:** Time-based promos (happy hour double points, birthday bonuses, seasonal events). Triggered automatically, no manual intervention. +- **Cross-venue loyalty (multi-venue operators):** A player's loyalty status carries across all venues under the same operator. Play at venue A, redeem at venue B. +- **Display integration:** Loyalty tier and points balance shown in player's mobile profile. Leaderboards on venue displays showing top loyalty earners. + +#### Private Venues & Memberships +- **Venue privacy modes:** Public (anyone can see schedule and join), Semi-private (schedule visible, registration requires approval), Private (invite-only, invisible to non-members). +- **Membership management:** Invite codes, application process, member approval workflows. Annual/monthly membership fees tracked through Felt. +- **Member tiers:** Different membership levels with different access — e.g., "Standard" members can play tournaments, "VIP" members get cash game priority and higher-stakes access. +- **Guest system:** Members can invite guests for specific events. Guest passes tracked and limited. +- **Member communications:** In-app messaging, event announcements, schedule changes pushed to members via mobile notification. + +#### Venue Analytics & Reporting +- **Revenue dashboards:** Real-time revenue by source (tournament fees, cash game rake, membership fees, food/beverage). Daily/weekly/monthly/yearly views. +- **Player analytics:** Player lifetime value, visit frequency, game preferences, spending patterns (privacy-respecting, aggregated where appropriate). +- **Operational analytics:** Table utilization rates, peak hours, waitlist conversion rates, dealer efficiency, tournament fill rates. +- **Benchmarking (multi-venue):** Compare performance across venues. Identify which venues are underperforming and why. +- **Export & integration:** CSV/PDF export for accounting. API for integration with existing POS, accounting, or casino management systems. + +#### Public Venue Presence +- **Venue profile page:** Public page at `venue-name.felt.io` showing schedule, upcoming events, game offerings, location, hours. SEO-optimized. +- **Event registration:** Players can register for upcoming tournaments online. Pre-paid or reserve-only options. +- **Schedule publishing:** Auto-publish weekly schedule to venue page, social media (API integration), and Poker Atlas/Hendon Mob (export formats). +- **Player reviews & ratings:** Optional. Public-facing venue reputation. + +**Integration with Phases 1 & 2:** +- All features share the same Leaf, displays, player database, and network +- Dealer scheduling integrates with tournament and cash game table assignments +- Loyalty points accrue from all activities — tournaments, cash games, memberships +- Analytics aggregate across all venue operations +- Displays cycle between tournament info, cash game status, loyalty leaderboards, and venue promos + +**Why this is the endgame:** +At Phase 3, a venue's entire operational history is in Felt. Years of player records, financial data, dealer schedules, loyalty balances, membership lists, league standings. This data is the moat. Switching costs are enormous — not because we make it hard to leave, but because there's nowhere to go that has all of this in one system. + +**Detailed specification:** See `felt_phase3_spec.md` + +--- + +### Phase 4: Native Apps & Platform Maturity *(Post-Revenue)* + +**Depends on:** Phases 1-3 complete, sustainable Pro subscription revenue + +Phase 4 is the polish layer. Everything in Phases 1-3 runs as responsive web apps and PWAs. Phase 4 adds native mobile apps for both players and venue managers, taking advantage of platform APIs that browsers can't fully access. + +**Player App (Native iOS + Android):** +- Everything from the PWA player experience, plus: +- Rich push notifications (tournament starting, your waitlist position, friend results) +- Apple/Google Wallet integration (loyalty cards, event tickets) +- Share Sheet integration (share results, invite friends) +- Biometric quick-login (Face ID, fingerprint) +- Offline stat caching (view your history without connectivity) +- Social features: private groups, group chat, activity feed, achievements + +**Venue Management App (Native iOS + Android):** +- Everything from the PWA operator UI, plus: +- Push notifications for operational alerts (display offline, dealer no-show, tournament level change) +- Biometric auth for quick unlock (manager grabs iPad, Face ID, they're in) +- Native haptic feedback on critical actions +- Background sync (Leaf status updates even when app is backgrounded) +- Siri/Google Assistant shortcuts ("Hey Siri, pause the tournament") + +**PWA-first strategy:** +All Phase 1-3 apps are already installable PWAs via SvelteKit. Phase 4 native apps are additive — they don't replace the web experience, they enhance it. Venues and players who don't want to install an app lose nothing. The PWA remains the primary onboarding path (scan QR → instant access, no App Store friction). + +**Investment required:** +- Apple Developer Program: €99/yr +- Google Play Developer: $25 one-time +- App Store review process and compliance +- Native codebase (likely React Native or Kotlin/Swift, wrapping the existing SvelteKit frontend for shared logic) +- By Phase 4, Pro subscription revenue should comfortably cover these costs + +**Why this is Phase 4, not earlier:** +App Store presence is expensive in time, not just money. Review cycles, compliance requirements, update cadence expectations. Doing this before the product is stable and revenue-generating risks burning resources on App Store maintenance instead of core features. PWAs give us 90% of the mobile experience for 10% of the cost. Phase 4 is the last 10%. + +--- + +## 5. The Ecosystem + +Felt isn't just a product — it's a platform connecting four distinct user groups. Critically, **users belong to Felt, not to venues.** + +### Felt Profiles: Platform-Level Identity + +This is the most important strategic decision in the entire product: **players and dealers have Felt profiles, not venue profiles.** + +A player's profile is their poker identity across the entire platform. Their tournament results, cash game sessions, league standings, loyalty points, achievements — all of this belongs to their Felt profile, not to any single venue. If a venue switches to a competitor (or shuts down), the player keeps everything. Their data travels with them. + +**Why this matters:** +- **Player lock-in is to Felt, not to the venue.** A venue can leave Felt, but they can't take the players' profiles with them. Players will pressure venues to stay on (or join) Felt because their history is here. +- **Network effects compound.** A player with results across 10 venues has more reason to stay than a player at 1 venue. Cross-venue visibility creates stickiness. +- **Competitor defense.** If a competitor shows up, a venue switching doesn't mean players lose their data or leave the platform. The player's profile, history, and relationships survive. The venue that switches loses access to the network — the player doesn't lose anything. +- **Dealer portability.** A dealer's work history, skills, hours, and shift records belong to their Felt profile. If they move to a different venue, their history comes with them. This makes Felt valuable for dealers personally, not just for venues. + +**Profile visibility:** +- Public venues: tournament results visible on profile (unless player opts out) +- Private venues: results only visible to venue members +- Players control what's publicly visible on their profile +- Aggregate stats (total tournaments, total hours, venues played) always available to the player + +### Player App (Phase 3/4): Gamification & Social + +The ultimate demand-generation tool. A dedicated mobile app that makes poker social and gamified — and puts pressure on venues from below. + +**Core features:** +- **Personal stats dashboard:** Lifetime results, win rate trends, ROI per game type, ranking among friends +- **Private groups:** Players create groups with friends. See each other's recent tournament results, league standings, upcoming events. Built-in group chat. "Hey, Friday's €50 tournament at Bar Poker — who's in?" +- **Achievements & badges:** "First final table," "5 in-the-money finishes in a row," "Played at 10 different venues," "100 tournaments played." Visible on profile, shareable. +- **Activity feed:** See what your friends have been doing — recent results, new venues visited, achievements unlocked. Light, non-intrusive, opt-in. +- **Venue discovery:** Find Felt venues near you. See what's running tonight. Join a waitlist from home. +- **Event notifications:** Get notified about upcoming tournaments at your regular venues, or at new venues your friends are playing at. + +**Why this creates venue pressure:** +Imagine a player group of 15 friends who all play regularly. They use the Felt app to coordinate, track results, and banter. One of their regular venues doesn't use Felt — so results from those tournaments don't appear in the app, don't count toward their stats, and their friends can't see them. That venue starts hearing "hey, when are you getting on Felt?" from their regulars. Player demand drives venue adoption. + +**Revenue model for the app:** Free for players. Always. The app is a demand generator, not a revenue source. Revenue comes from venues subscribing to Pro. + +### Venue Management App (Phase 4): No PC Required + +The other half of Phase 4. A venue owner or floor manager runs their entire operation from an iPad or tablet. No PC. No laptop. Just a tablet at the host stand or behind the bar. + +**Core features:** +- **Full tournament control:** Start, pause, advance levels, manage buy-ins, bust players — everything the operator UI does, but native touch-optimized +- **Cash game management:** Open/close tables, seat players, manage waitlists, track sessions — tap and swipe +- **Dealer scheduling:** Assign dealers, approve shift trades, view the week's schedule — all from the tablet +- **Display management:** See what every screen is showing. Reassign views. Push signage content. +- **Live dashboard:** Revenue today, tables running, players in the building, upcoming events — glanceable from across the room +- **Notifications:** Tournament level about to change, waitlist player not responding, dealer called in sick, display node offline — push alerts to the manager's device + +**Why this matters:** +The current operator UI is a responsive web app that works on tablets. But a dedicated app with native gestures, haptic feedback, push notifications, and offline-capable local caching is a different level of polish. The venue owner managing a busy Friday night from a tablet clipped to their belt — that's the image. + +**PWA first, native later:** +Phase 1-3 operator UI is already PWA-capable (SvelteKit, mobile-first). For Phase 4, the dedicated management app starts as an enhanced PWA with full offline capability and installable home screen experience. Native iOS/Android apps come once revenue justifies the Apple Developer Program (€99/yr) and Google Play Developer ($25 one-time) accounts, plus the ongoing App Store review process. The native apps add push notifications without browser permission prompts, biometric auth (Face ID / fingerprint for quick unlock), and smoother animations. + +**Same story for the player app:** PWA first (players scan QR → instant access, no install friction). Native apps in the store once we have the volume and revenue to justify the investment. Native adds rich push notifications, Apple/Google Wallet integration for loyalty cards, and Share Sheet integration for sharing results. + +**Revenue model:** The management app is included with Pro. It's the premium operator experience — another reason to subscribe. + +### Venue Operators +The primary customer. Free-tier venues run tournaments on our cloud — no hardware, no cost, full tournament engine. When they outgrow the free tier (need offline, cash games, or displays), they buy hardware from us and subscribe to Pro. They get a system that replaces 5+ separate tools with one. Their screens work 24/7 — tournaments during play, promos during breaks, ads after hours. + +### Players +Free users with platform-level profiles. They scan a QR code and they're in. No app required for basic access (view clock, view rankings). Optional account unlocks personal stats, cross-venue history, loyalty, and league standings. The dedicated app (Phase 3/4) adds social features, gamification, and friend groups. Over time, their Felt profile becomes their poker identity. + +### Dealers +Free users with platform-level profiles. Their skills, work history, schedule, and shift records belong to them. They manage their schedule, trade shifts, track hours, and see assignments via mobile. If they move to a different venue, their verified skills and work history travel with them. + +### Multi-Venue Operators / Networks +Enterprise customers. They manage multiple venues from a single dashboard. Cross-venue leagues, shared player databases, centralized analytics, fleet management of Leaf nodes. They push content to all venue screens at once. + +### Regional Tournament Organizers +Free users who create cross-venue tournaments and leagues. They organize regional championships with qualifying rounds at independent venues. Automatic result aggregation, unified leaderboards, finals management. This hooks new venues into the Felt ecosystem for free. + +### The Flywheel + +``` +Free tier hooks venues → Players create Felt profiles +Players build cross-venue history → More valuable to stay on Felt +Player app creates social groups → Friends pressure non-Felt venues +Regional organizers create cross-venue events → More venues join for free +More venues on platform → Cross-venue features become more valuable +More players with history → Venues can't leave without losing the network +``` + +The flywheel has three engines: the free tier (venue acquisition), the player app (demand generation from below), and regional organizers (network creation from above). All three drive adoption. None of them cost us subscription revenue. + +--- + +## 6. Architecture Philosophy + +### Principle 1: Leaf Sovereignty +The Leaf node is the single source of truth during operation. The cloud is a backup, a sync target, and a distribution layer — never a dependency. A venue must be able to run a complete tournament, cash game, and dealer schedule without touching the internet. + +### Principle 2: Network Agnosticism +Felt works on any network. All traffic flows through WireGuard tunnels via Netbird. No firewall rules, no port forwarding, no static IPs, no IT involvement. A venue needs power and any internet connection. That's it. + +### Principle 3: Screens Are a Platform +Display nodes aren't just tournament clocks. They're the venue's entire digital signage system. Tournaments during play, promos during breaks, ads after hours. A WYSIWYG editor with AI assist makes content creation trivial. The venue gets a screen management system for free with their poker management system. + +### Principle 4: Data Gravity as Moat +Every tournament, every cash game session, every player registration, every financial transaction deepens the venue's dependency on Felt. After 12 months, a venue has thousands of records they can't take elsewhere. This is organic lock-in through value, not artificial barriers. + +### Principle 5: Scale by Adding Instances +The Leaf runs everything for one venue. The Core aggregates across venues. Scaling means more Leaf instances behind Netbird's mesh, not bigger servers. Each venue is operationally independent. + +### Principle 6: No Third-Party MITM +Self-hosted Netbird. Self-hosted Authentik. Self-hosted Core. No Cloudflare, no third-party auth, no vendor dependency that could change terms, raise prices, or compromise data. We own the full stack. + +--- + +## 7. Business Model + +### The Free Tier Philosophy + +The free tier is not a crippled demo. It's a genuinely useful product that runs a poker venue's tournaments — for free, forever. + +**How it works:** A venue signs up at felt.io. We spin up a **virtual Leaf node** on our cloud infrastructure. The venue gets the full tournament engine — clock, blinds, buy-ins, payouts, seating, multi-table balancing, leagues, seasons. Players scan a QR code and get full mobile access — live clock, rankings, their personal stats. The venue gets the WYSIWYG signage editor with AI assist — they can create event promos, drink specials, sponsor ads and display them on any browser-connected screen. + +**What the free tier includes:** +- Full tournament engine (unlimited tournaments, unlimited players) +- Player mobile access (scan QR, view clock, rankings, personal stats) +- Player database and history +- League and season management +- Digital signage editor with AI assist +- Multi-tournament support +- Financial tracking (buy-ins, payouts, prize pools) +- Regional tournament participation + +**What the free tier does NOT include:** +- Offline operation (requires internet — runs on our cloud) +- Dedicated hardware (no Leaf node, no display nodes) +- Cash game management (Pro feature) +- Dealer scheduling (Pro feature) +- Player loyalty system (Pro feature) +- Membership management (Pro feature) +- Advanced analytics +- Remote admin access +- Priority support + +**Why this works:** The Copenhagen enthusiast club running 3 tournaments a week on 5 tables gets a world-class tournament system for free. Their players get hooked on checking standings from their phone. The venue starts creating event promos in the signage editor. After 6 months, they have hundreds of player records, league standings, and tournament history in Felt. + +Then one night the internet goes down during a tournament. Or they want to run cash games on Fridays. Or they want proper displays on their TVs instead of a browser tab. That's when they call us. + +### Making TDD Obsolete on Day 1 + +The single biggest barrier to adoption is migration. TDD venues have years of data — blind structures, tournament templates, player databases, league histories. If switching to Felt means starting from scratch, they won't switch. + +**TDD Data Import Tool:** +- Import blind structures and tournament templates from TDD's XML export +- Import player databases (names, contact info, tournament history) +- Import league standings and season data +- Conversion runs in minutes, preserves all historical data +- Venues keep their entire history — from day one on Felt, it looks like they've been on Felt forever + +This removes the last excuse. The tournament engine is better, the displays are wireless, the players get mobile access, the signage is built in — and they don't lose a single record. + +### Regional Tournament Organizer (Free Tier) + +Independent venues in the same region often want to collaborate — regional championships, multi-venue league seasons, grand tournaments with qualifying rounds at individual venues. Today this requires spreadsheets, emails, and someone manually aggregating results. + +**Felt offers this for free.** Anyone can create a regional tournament or league through Felt's cloud platform: +- Define qualifying events across participating venues +- Automatic point aggregation from each venue's qualifying tournaments +- Unified leaderboard across all venues +- Finals event management +- Player profiles carry across all participating venues + +**Why free?** Because it hooks venues from the outside. A regional organizer approaches 12 venues about a championship series. 8 of them aren't on Felt yet. To participate, they sign up for the free tier. Now they're running their qualifiers on Felt. Their players create Felt profiles. Six months later, they're wondering why they're still running their regular tournaments on TDD. + +This is viral adoption that costs us nothing beyond the virtual Leaf infrastructure we're already providing. + +### Hardware Model + +**No BYO. Felt hardware only.** + +Leaf nodes and display nodes are sold exclusively by Felt. They ship pre-configured, locked down, and secure. + +**Why no BYO:** +- **Security.** The Leaf stores player data, financial records, and venue operations. A BYO device could be cloned, extracted, or tampered with. Our hardware ships with encrypted storage, secure boot, and tamper detection. We control the full chain. +- **Reliability.** We test on our exact hardware. No "works on my Raspberry Pi but not yours" support tickets. +- **Support.** When something breaks, we ship a replacement. +- **Consistency.** Every Felt installation worldwide runs identical hardware. + +**Hardware pricing (cost-recovery, no recurring fees on displays):** + +| Device | Price | Notes | +|--------|-------|-------| +| Leaf Node | ~€120 | Venue brain — runs everything offline | +| Display Node | ~€30 each | Pi Zero W2 + case + power — no recurring fee | +| Display Node 4-pack | ~€110 | Slight discount on multi-buy | + +**Leaf hardware options:** +- Buy outright for €120 +- Free with 12-month Offline or Pro annual plan +- Casino tiers: hardware included in contract + +**Display nodes:** Priced at cost + shipping. No recurring fee — they're stateless render devices. Venues buy as many as they need (most want 2-6). + +### Custom Domains + +Venues can point their own domain to Felt. They configure a CNAME to their Felt subdomain — Netbird handles TLS termination and routing automatically. The Felt subdomain (`venue-name.felt.io`) works as a fallback. + +Example: `poker.copenhagenbar.dk` → `copenhagenbar.felt.io` → Netbird → Leaf + +Zero complexity for the venue. Professional presence without IT involvement. + +### Subscription Tiers + +#### Venue Tiers + +| Tier | Price | Target | Key Differentiator | +|------|-------|--------|-------------------| +| **Free** | €0/mo | Enthusiast clubs, small bars, anyone starting | Full tournament engine on virtual Leaf in our cloud. Requires internet. | +| **Offline** | €25/mo | Venues that need reliability and displays | Dedicated Leaf hardware + wireless display nodes + offline operation + custom domain + remote admin | +| **Pro** | €100/mo | Serious venues wanting the full platform | Everything in Offline + cash games + dealer scheduling + loyalty + memberships + analytics + TDD import + priority support | + +Pro is €25 (Offline) + €75 (Pro features). A venue on the Offline tier can upgrade to Pro at any time by adding €75/mo — no hardware changes needed. + +#### Casino & Enterprise Tiers + +| Tier | Price | Target | +|------|-------|--------| +| **Casino Starter** | €249/mo | Independent casino, 1 poker room, 5-15 tables. Adds multi-room support, floor manager roles, compliance audit trail, shift reporting, 8hr SLA. | +| **Casino Pro** | €499/mo per property | Small chain, 2-5 properties, 15-40 tables each. Adds multi-property dashboard, cross-property player tracking & loyalty, centralized dealer pool, API access for POS/CMS integration, white-label, 4hr SLA, dedicated onboarding. | +| **Casino Enterprise** | Custom (€999+/mo) | Large operators, 5+ properties, 40+ tables each. Adds unlimited properties, fleet management, regional analytics, casino CMS API integration (Bally's, IGT, L&W), custom feature development, white-label everything, 2hr SLA + dedicated account manager, on-site installation. | + +Casino tiers include Leaf hardware in the contract. Display nodes at cost. + +### The Casino Multiplier + +One Casino Pro deal (5 properties × €499/mo) = €2,495/mo = €29,940/yr. That single deal is worth 100 Offline venues or 25 Pro venues. Two Casino Pro deals plus modest venue growth gets you to €100k+/yr revenue from under 100 total paying venues. + +Casinos are where the margin is. Enthusiast venues build the platform, prove the product, and generate the player network. Casino contracts pay the bills. + +### Unit Economics + +**Infrastructure costs (from capacity analysis):** + +A single €100/mo Hetzner server (16c/32t, 64GB RAM, 2×1TB NVMe) handles ~1,300 Virtual Leafs or ~900 Pro venues on Core. For Year 1-2 with everything on one server, actual cost per free venue is ~€0.45/mo — far below the €4/mo we conservatively budget. + +| Tier | Revenue | Infra Cost | Net Margin | +|------|---------|-----------|------------| +| Free (virtual Leaf) | €0/mo | ~€0.45/mo | -€0.45/mo (CAC) | +| Offline | €25/mo | ~€0.15/mo | ~€24.85/mo | +| Pro | €100/mo | ~€0.15/mo | ~€99.85/mo | +| Casino Starter | €249/mo | ~€0.15/mo | ~€248.85/mo | +| Casino Pro (per property) | €499/mo | ~€0.15/mo | ~€498.85/mo | + +Infrastructure margins are absurd because Pro/Casino Leafs run on the venue's hardware. We're essentially selling software licenses with near-zero marginal cost. + +### Conversion Strategy + +The free → paid conversion is driven by natural needs, not artificial limitations: + +| Trigger | Conversion Path | +|---------|----------------| +| Internet goes down during tournament | → Offline (€25/mo) | +| Want proper TV displays (not browser tabs) | → Offline (€25/mo) — need display node hardware | +| Want custom domain | → Offline (€25/mo) | +| Start running cash games | → Pro (€100/mo) | +| Want dealer scheduling | → Pro (€100/mo) | +| Want player loyalty | → Pro (€100/mo) | +| Growing player base needs analytics | → Pro (€100/mo) | +| Multi-room or 15+ tables | → Casino Starter (€249/mo) | +| Multiple properties | → Casino Pro (€499/mo) | + +### What Venues Pay Today (for comparison) + +| Current Tool | Cost | What Felt Replaces It With | +|-------------|------|---------------------------| +| TDD License | $130 one-time (Windows PC required) | Offline at €25/mo — plus wireless displays, mobile, signage | +| BravoPokerLive | $200-500/mo (US, waitlist-focused) | Pro at €100/mo — plus tournaments, displays, analytics | +| Casino CMS poker module | $2,000-10,000/mo | Casino Starter at €249/mo — 1/10th the price, better product | +| Digital signage software | €30-80/mo (separate sub) | Included in Offline and above | +| Generic waitlist app | €20-50/mo | Included in Pro | +| Spreadsheets for leagues/stats | Free but hours of manual work | Included in Free tier | + +### Scaling Math + +| Milestone | Free | Offline | Pro | Casino | Monthly Revenue | Annual Net | +|-----------|------|---------|-----|--------|----------------|-----------| +| Year 1 (Denmark) | 80 | 8 | 5 | 0 | €700 | ~€6.7k | +| Year 2 (Nordics + first casino) | 250 | 20 | 15 | 2 CS | €2,498 | ~€27k | +| Year 3 (N. Europe + UK) | 500 | 40 | 35 | 5 CS + 1 CP | €6,244 | ~€70k | +| Year 4 (International) | 800 | 60 | 60 | 8 CS + 3 CP + 1 CE | €12,488 | ~€143k | + +CS = Casino Starter (€249/mo), CP = Casino Pro (€499/mo), CE = Casino Enterprise (€999+/mo) + +### Unit Economics + +**Free tier cost to us:** + +| Item | Cost | +|------|------| +| Virtual Leaf (cloud compute per venue) | ~€3/mo | +| Storage | ~€0.50/mo | +| Bandwidth | ~€0.50/mo | +| **Total per free venue** | **~€4/mo** | + +At 100 free venues, that's €400/mo in infrastructure cost. This is our customer acquisition cost — far cheaper than any ad campaign, and the venue is already locked in with data. + +**Pro tier economics:** + +| Cost Side | Amount | +|-----------|--------| +| Hardware (Leaf + 4 displays, at-cost) | ~€200 one-time | +| Cloud infrastructure per venue | ~€3/mo | +| Support & maintenance overhead | ~€3/mo | +| **Total ongoing cost per venue** | **~€6/mo** | + +| Revenue Side | Amount | +|--------------|--------| +| Hardware margin (small) | ~€30 one-time | +| Pro subscription | €49/mo | +| **Net margin per venue** | **~€43/mo → €516/yr** | + +### Conversion Strategy + +The free → Pro conversion is driven by natural needs, not artificial limitations: + +| Trigger | Why They Upgrade | +|---------|-----------------| +| Internet goes down during tournament | "We need offline operation" → need Leaf → need Pro | +| Want proper TV displays | "Browser tab on a laptop isn't cutting it" → need display nodes → need Pro | +| Start running cash games | Only available on Pro | +| Want dealer scheduling | Only available on Pro | +| Want player loyalty | Only available on Pro | +| Growing player base needs analytics | Only available on Pro | +| Want remote access for management | Only available on Pro | + +We estimate 20-30% conversion over 12 months based on the natural progression of a venue that starts taking poker seriously. + +### Scaling Math + +| Free Venues | Pro Venues | Hardware Revenue | Annual Recurring | Net Annual | +|------------|-----------|-----------------|-----------------|-----------| +| 50 | 5 (10%) | €1,000 | €3k | ~€2.5k (break-even approaching) | +| 200 | 40 (20%) | €8,000 | €24k | ~€20k | +| 500 | 125 (25%) | €25,000 | €75k | ~€65k | +| 1,000 | 250 (25%) | €50,000 | €150k | ~€130k | + +Plus enterprise contracts on top. The free tier costs us ~€4/venue/mo but drives all adoption. + +--- + +## 8. Competitive Moats + +### What's Hard to Replicate + +**Platform-level player identity.** Players and dealers belong to Felt, not to any venue. A player's tournament history, stats, achievements, and loyalty span every venue they've ever played at. This creates lock-in to the platform, not to individual venues — and it means a venue switching away from Felt loses access to the network without the players losing anything. No competitor can replicate this without first building the network. + +**The full-stack hardware+software+network integration.** Anyone can build a tournament timer app. No one has built a system where the hardware, software, networking, display management, and player ecosystem all work together out of the box. This is years of engineering. + +**Player-driven demand.** The dedicated player app with social groups, gamification, and cross-venue stats creates bottom-up pressure on venues to adopt Felt. Players asking "when are you getting on Felt?" is the most powerful sales force we could have — and it costs us nothing. + +**Network agnosticism.** The Netbird-based zero-config networking is a deep technical advantage. Competitors would need to either build this themselves or depend on a third party. We self-host the entire stack. + +**Data gravity.** After 12 months of operation, a venue has thousands of tournament records, player profiles, financial transactions, league standings, and loyalty data in Felt. There's nowhere to migrate this to because no competitor has the same data model. + +**Signage lock-in.** Even free-tier venues use Felt's signage editor to create event promos and drink specials. Once they upgrade to Pro with physical display nodes managing all their screens, removing Felt means losing their entire screen management system. + +**TDD migration path.** We're the only alternative that imports TDD data directly. A venue doesn't start from scratch — they bring their entire history. This removes the migration barrier that has kept TDD alive for 15 years. + +**The regional organizer network.** Free cross-venue tournaments and leagues create organic adoption. A regional organizer brings 12 venues onto the platform at once — all for free — and every one of those venues' players now has a Felt profile. + +### What's Not Hard to Replicate + +- The tournament clock engine itself (TDD already does this well) +- Basic cash game waitlist management +- Mobile-first UI (any competent frontend team can build this) + +The moat isn't any single feature. It's the combination of platform-level identity, player-driven demand, free-tier adoption, and full-stack integration that creates a network no competitor can replicate by building better software alone. + +--- + +## 9. Market Landscape + +### Current Tools + +| Tool | What It Does | What It Doesn't Do | +|------|-------------|-------------------| +| The Tournament Director (TDD) | Deep tournament management | Cash games, mobile, cloud, wireless displays, signage, modern UI | +| Blind Valet | Simple cloud timer | Offline, cash games, displays, player tracking, depth | +| Poker Atlas | Venue discovery, schedule | Any operational tooling | +| Hendon Mob | Player results database | Venue management, real-time | +| BravoPokerLive | Waitlist app (US-focused) | Tournament management, displays, full integration | +| Generic digital signage | Screen management | Poker-specific features | + +### Market Segments + +**Small bars & clubs (1-5 tables):** The biggest market by count. Usually one person runs everything. They need simplicity above all. Felt's free tier gets them started; the €100 hardware investment is trivial compared to a Windows PC + monitors + cables. + +**Dedicated poker rooms (5-20 tables):** The sweet spot. These venues are serious enough to need proper tooling but small enough that enterprise casino management systems are overkill. They'll pay €49-79/mo happily if it replaces 3+ separate tools. + +**Casino poker rooms (20+ tables):** Enterprise opportunity. These venues already have casino management systems (from companies like Bally's, Light & Wonder, IGT) but those systems have weak poker-specific features. Felt could integrate as a poker module within their ecosystem — or replace their poker tooling entirely. + +**Amateur leagues:** Growing segment. League organizers run multi-venue seasons across bars and clubs. They need cross-venue league management, consistent player tracking, and a unified platform. Felt's multi-venue tier is built for this. + +--- + +## 10. Long-Term Value & Natural Exits + +Felt is designed to be valuable whether it exits or not. + +### Scenario A: Cash Cow + +200+ venues paying monthly subscriptions with minimal churn. Low infrastructure costs (self-hosted, no per-venue cloud resources beyond sync). Deep venue lock-in through years of data. Minimal support burden once the product is stable. This is a comfortable, profitable business that generates recurring revenue indefinitely. + +### Scenario B: Acquisition by Casino Management Platforms + +**Most likely acquirers:** Companies like Bally's Corporation, Light & Wonder (formerly Scientific Games), IGT, or Aristocrat. These companies spend billions on casino floor technology — slot management, table game tracking, player loyalty (think "players club" cards), regulatory compliance. But their poker room software is uniformly poor. Poker is a small part of their business and gets minimal R&D attention. + +**What Felt gives them:** +- Modern player tracking that actually works for poker (not adapted from slot machine logic) +- Cross-property poker analytics (how do poker players behave across multiple casino properties?) +- A mobile-first player experience their current systems can't provide +- Digital signage that integrates with their existing property management +- A ready-made platform they don't have to build + +**Why this makes sense for us:** +- These companies have distribution. They're already in hundreds of casinos. +- They have the sales force to onboard venues. We don't. +- Felt's clean API boundaries and multi-tenant architecture make integration straightforward. +- The acquisition price reflects both the technology and the venue/player data. + +### Scenario C: Acquisition by Online Poker Operators + +**Potential acquirers:** PokerStars (Flutter Entertainment), GGPoker, WPT (joining with Zynga Poker), 888poker. + +Online poker operators are all investing in live poker experiences. They sponsor live tours, run branded events, and want to bridge their online player base with live venues. Felt gives them an instant physical-venue platform with player data that bridges online and live play — a gap no one has closed. + +### What Makes Us Attractive Regardless of Path + +- Clean API boundaries (acquirer integrates, doesn't rebuild) +- Multi-tenant from day one (all venues in one platform) +- Data portability (full export — builds trust, reduces churn fear) +- Security posture that survives due diligence +- No technical debt from "we'll fix it later" shortcuts + +--- + +## 11. Risks & Mitigations + +### Product Risk: TDD Is Good Enough + +**Risk:** Venue operators are used to TDD's quirks and don't want to switch. +**Mitigation:** We don't need to replace TDD everywhere. We need to win new venues and venues that are upgrading. TDD doesn't do cash games, displays, signage, or mobile. Every venue that wants *any* of those features is our customer. + +### Market Risk: Small Market Size + +**Risk:** Live poker venues are a niche market. +**Mitigation:** The global live poker market is larger than it appears. US alone has 800+ poker rooms. Europe, Asia-Pacific, Latin America add thousands more. Amateur leagues (pub poker) are a massive undercounted segment. And Felt's complete-venue approach means higher revenue per customer than a single-feature tool. + +### Technical Risk: Hardware Reliability + +**Risk:** SBCs fail, SD cards corrupt, displays disconnect. +**Mitigation:** NVMe storage (not SD cards) on Leaf nodes. Display nodes are stateless and replaceable. Automatic cloud backup of all venue data. Remote diagnostics via Netbird SSH. Hardware is commodity — replace a failed Leaf in 30 minutes. + +### Competitive Risk: Someone Builds This + +**Risk:** A well-funded competitor builds an integrated venue platform. +**Mitigation:** First-mover advantage is real in B2B SaaS. Switching costs after 12 months of data accumulation are high. The full-stack integration (hardware + software + networking + signage) is 2+ years of engineering. And we're not waiting — we're shipping Phase 1 now. + +### Regulatory Risk: Gambling Regulations + +**Risk:** Some jurisdictions have regulations around poker management software, player tracking, or financial reporting. +**Mitigation:** Felt tracks buy-ins and payouts for venue operators — it doesn't process gambling transactions. We're a management tool, not a payment processor. Data export and audit trail features support compliance requirements. We'll engage legal counsel per jurisdiction as we expand. + +--- + +*"Plug in. Power on. Deal."* diff --git a/docs/felt_phase1_spec_v05.md b/docs/felt_phase1_spec_v05.md new file mode 100644 index 0000000..ebd2585 --- /dev/null +++ b/docs/felt_phase1_spec_v05.md @@ -0,0 +1,2106 @@ +# Project Felt — Phase 1 Product Specification + +## Live Tournament Management System + +**Working Title:** Felt +**Version:** 0.5 Draft +**Date:** 2026-02-27 + +--- + +## Table of Contents + +1. [Vision & Strategy](#1-vision--strategy) +2. [System Architecture](#2-system-architecture) +3. [Hardware Architecture](#3-hardware-architecture) +4. [Core Infrastructure](#4-core-infrastructure) +5. [Data Architecture](#5-data-architecture) +6. [Sync & Message Queue Architecture](#6-sync--message-queue-architecture) +7. [Feature Specification](#7-feature-specification) +8. [UI/UX Design System](#8-uiux-design-system) +9. [Tech Stack](#9-tech-stack) +10. [Display Node Protocol](#10-display-node-protocol) +11. [API Design](#11-api-design) +12. [Security & Threat Model](#12-security--threat-model) +13. [Deployment & Operations](#13-deployment--operations) +14. [Roadmap](#14-roadmap) +15. [Appendix: TDD Feature Parity Matrix](#15-appendix-tdd-feature-parity-matrix) + +--- + +## 1. Vision & Strategy + +### The Problem + +Live poker venue management is fragmented across aging, siloed tools: + +- **The Tournament Director (TDD):** Feature-rich but Windows-only desktop software from 2002. Requires HDMI cabling for displays, no mobile access, no cloud, no multi-device operation. Powerful under the hood but dated and intimidating to configure. +- **Blind Valet:** Modern cloud-based timer, but feature-shallow. Dead without internet. +- **Poker Atlas:** Venue discovery and schedule publishing, but zero operational tooling. +- **Spreadsheets & Paper:** Most venues still track cash games, waitlists, comps, and league points manually. + +No single system handles the full lifecycle of a poker venue while working reliably when the internet goes down. + +### The Vision + +**Felt** is an all-in-one poker venue operating system built on a resilient edge-cloud architecture. An ARM64 SBC "Leaf Node" runs the venue autonomously with NVMe storage. Ultra-cheap display nodes replace HDMI cables. Players interact via their phones — on venue WiFi or remotely via the cloud. The cloud layer handles cross-venue leagues, player profiles, public scheduling, and remote player access. + +### Strategic Phasing + +| Phase | Scope | Replaces | +|-------|-------|----------| +| **Phase 1** | Live Tournament Management | TDD, Blind Valet | +| **Phase 2** | Cash Game Management | Manual waitlists, spreadsheets | +| **Phase 3** | Full Venue System | Poker Atlas (operational side), comp tracking, analytics | +| **Phase 4** | Native Apps & Platform Maturity | PWA limitations, App Store presence, social/gamification | + +### Business Model & Exit Strategy + +Felt is designed for long-term value creation, not short-term revenue optimization. + +**Free tier:** Full tournament engine running on a virtual Leaf node in our cloud. Includes player mobile access, signage editor, leagues, seasons, and financial tracking. Requires internet — no offline capability. Costs us ~€0.45/mo per venue in infrastructure. This is our customer acquisition engine. + +**Offline tier (€25/mo):** Dedicated Leaf hardware + wireless display nodes + full offline operation + custom domain + remote admin access via Netbird. Leaf hardware purchased separately (~€120) or free with 12-month annual plan. + +**Pro tier (€100/mo = €25 offline + €75 features):** Everything in Offline plus cash game management, dealer scheduling, loyalty system, memberships, advanced analytics, TDD data import, and priority support. All Phase 2 and Phase 3 features included. + +**Casino tiers:** Casino Starter (€249/mo, independent casino 5-15 tables), Casino Pro (€499/mo per property, small chain 15-40 tables), Casino Enterprise (€999+/mo custom, large operators 40+ tables). Progressive features for multi-room, cross-property tracking, CMS integration, SLA. + +**Hardware model:** Leaf nodes and display nodes sold exclusively by Felt. Pre-configured, encrypted storage, secure boot. No BYO. Display nodes priced at cost + shipping with no recurring fee. + +**Exit strategy:** Acquisition by casino management platforms (Bally's, Light & Wonder, IGT) who need modern poker room software, by online poker operators (PokerStars, GGPoker, WPT) expanding into live venues, or growth to passive cash cow with deep venue lock-in through data gravity. + +**Architectural implications for exit:** +- Clean API boundaries (acquirer can integrate Felt's backend into their ecosystem) +- Multi-tenant from day one (acquirer gets all venues in one platform) +- Data portability (full export capability — builds trust, reduces churn) +- Open-source friendly licensing (Apache 2.0 on client-side, proprietary on Core — standard dual-license SaaS model) +- Security posture that survives due diligence (no shortcuts, no "we'll fix it later") + +### Design Philosophy + +**"TDD's brain, Linear's face."** + +Felt must be as powerful and logically organized as TDD — the tab structure (Game, Rounds, Players, Tables, Prizes, Events) is genuinely well thought out and tournament directors understand it. But it must look and feel like a modern premium product. Clean typography, generous whitespace, information density without clutter, smooth animations, and a color palette that works in a dim poker room. + +The operator should feel like they're running a professional operation. The players should feel like they're at a serious venue. The venue owner should feel like they have a competitive edge. + +--- + +## 2. System Architecture + +### Three-Tier Edge-Cloud Model + +``` ++--------------------------------------------------------------------+ +| CORE (Cloud / Hetzner PVE) | +| +------------+ +--------+ +----------+ +----------+ | +| | PostgreSQL | | NATS | | Go API | | Authentik| | +| | (master of | | (cloud | | Service | | (IdP) | | +| | record) | | hub) | | (admin) | | | | +| +------------+ +---+----+ +----------+ +----------+ | +| | | +| +------------------+-------------------------------------------+ | +| | Netbird (unified server + reverse proxy) | | +| | | | +| | Mesh: Leaf <-> Core, Leaf <-> Display, Admin <-> any | | +| | Proxy: *.felt.io -> Leaf services via WireGuard tunnel | | +| | DNS: felt.internal zone (internal service discovery) | | +| | SSH: Identity-aware via Authentik OIDC | | +| | Auth: SSO / PIN / password at proxy layer per service | | +| +------------------+-------------------------------------------+ | ++---------------------+----------------------------------------------+ + | + WireGuard mesh (encrypted, zero-trust, lazy connections) + NATS sync (async, queued locally on Leaf when offline) + Reverse proxy (HTTPS -> WireGuard tunnel -> Leaf HTTP/WS) + | + +-------------+---------------------------+ + | LEAF NODE (SBC) | + | | + | +----------+ +----------+ +----------+ | + | | Go | | LibSQL | | NATS | | + | | Backend | | (NVMe) | | (embed) | | + | +----+-----+ +----------+ +----------+ | + | | WebSocket Hub | + +-------+--+-----+-------+----------------+ + | | | | + +----+ +---+ +----+ +----------------------------+ + | | | | + +---------+ +---------+ +--------+ +---------------------+ + |Display | |Display | |Operator| |Player Phones | + |Node 1 | |Node 2 | |Tablet | | | + |(Clock) | |(Rank) | |(Touch) | | On venue WiFi: | + +---------+ +---------+ +--------+ | -> direct to Leaf | + | Off venue (4G/home): | + | -> Netbird proxy | + | -> WG -> Leaf | + | (same URL, same data,| + | no VPN, no app) | + +----------------------+ +``` + +### Netbird as Infrastructure Backbone + +Netbird (v0.65+) is the foundational layer that makes Felt network-agnostic. Every Felt device communicates exclusively through encrypted WireGuard tunnels — the Leaf, display nodes, Core services, admin devices — all traffic rides on the Netbird mesh regardless of what underlying network it sits on. + +**What this means for venues:** No firewall rules, no port forwarding, no static IPs, no VLANs, no IT department involvement. The Leaf and display nodes need one thing: outbound internet access. Netbird handles NAT traversal, encryption, and routing automatically. A venue can run Felt on a consumer-grade WiFi router with zero configuration. The traffic is invisible to anyone sniffing the local network — every byte is WireGuard-encrypted, even between the Leaf and display nodes sitting on the same LAN. + +**What this means for us:** One networking model regardless of deployment. A venue in a Copenhagen bar, a hotel ballroom in Las Vegas, and a community center in rural Denmark all work identically. No venue-specific networking troubleshooting, no "works on my network" bugs, no customer support calls about firewalls. + +Rather than building separate solutions for networking, service exposure, DNS, SSH access, and proxy authentication, Netbird provides all of these in a single self-hosted platform: + +| Capability | Replaces | Benefit | +|-----------|----------|---------| +| **WireGuard mesh** | Manual WireGuard, Tailscale, venue network dependency | All Felt traffic encrypted, NAT-traversed, network-agnostic. No ports, no firewall rules, no sniffing. | +| **Built-in reverse proxy** | Custom Core relay system, nginx public proxy | Expose Leaf services to internet with SSO/PIN auth, automatic TLS. Players access from anywhere. | +| **Custom DNS zones** | Hardcoded IPs in config files | `felt.internal` zone for service discovery, auto-distributed to peers | +| **Identity-aware SSH** | SSH key distribution, manual access management | OIDC auth via Authentik, per-user OS mapping, browser-based SSH | +| **Lazy connections** | Always-on tunnels to hundreds of Leaves | On-demand WireGuard tunnels, critical at 500+ venues | +| **Firewall policies** | Manual nftables per device | Drop-all-inbound on display nodes, zero-trust ACLs | +| **Auto-updates** | Manual Netbird agent updates across fleet | Fleet-wide agent updates from management dashboard | +| **Traffic logging** | Custom monitoring for mesh traffic | Real-time connection logging for security audit | + +### Key Principles + +1. **Leaf is sovereign.** All tournament logic runs locally. Cloud is never required for operation. +2. **Network agnostic.** Every device — Leaf, display, admin — communicates exclusively through encrypted WireGuard tunnels (Netbird). We don't care about the venue's network topology, firewall rules, NAT type, ISP, or router. All we need is outbound internet access. No ports to open, no static IPs, no IT support from the venue. Plug in, connect to any internet, done. All traffic is encrypted end-to-end regardless of what network it traverses — coffee shop WiFi, hotel LAN, cellular hotspot, it doesn't matter. +3. **Everything is a browser.** Operator, displays, players — all connect via HTTP/WebSocket. +4. **Real-time first.** State changes propagate to all clients within 100ms via WebSocket. +5. **Display nodes are dumb.** They render what the Leaf tells them. The Leaf owns routing. +6. **Data flows up via messages.** NATS JetStream queues events locally and forwards to Core when online. +7. **Core is the master of record.** Once synced, PostgreSQL is the canonical historical data store. +8. **Beautiful by default.** Every screen — operator, display, player — should look premium out of the box. +9. **Players connect from anywhere.** On venue WiFi → direct to Leaf. Off-venue → Netbird reverse proxy tunnels directly to Leaf. Same URL, same data, no VPN client, no relay service. +10. **Netbird is the infrastructure layer.** Mesh networking, service exposure, DNS, SSH, auth, traffic logging — one self-hosted platform, fully under our control. No Cloudflare, no third-party MITM, no vendor dependency for core networking. + +--- + +## 3. Hardware Architecture + +### Leaf Node — Requirements + +The Leaf Node is the venue brain. It must be an ARM64 SBC with native M.2 NVMe, sufficient RAM for Go + LibSQL + NATS + serving 50+ WebSocket clients, and reliable EU/DK sourcing. + +**Minimum Requirements:** + +| Component | Requirement | Notes | +|-----------|-------------|-------| +| **CPU** | ARM64, quad-core A76 or better | Must handle Go concurrency + WebSocket hub | +| **RAM** | 4GB minimum, 8GB recommended | Go + NATS + LibSQL + OS | +| **Storage** | M.2 NVMe (built-in, no hat) | Write durability critical for tournament data | +| **Network** | Gigabit Ethernet + WiFi 5/6 | Ethernet primary, WiFi fallback | +| **HDMI** | At least 1× (for initial setup) | Not used in production | +| **Audio** | 3.5mm or USB audio out | Level change sounds, announcements | +| **Power** | USB-C with UPS battery backup | Must survive brief power blips | +| **EU Sourcing** | Available from EU distributors | Amazon.de, Allnet.de, EU warehouses | + +**Reference Board: Orange Pi 5 Plus (~€90-110)** + +| Spec | Value | +|------|-------| +| SoC | Rockchip RK3588 (4× A76 @ 2.4GHz + 4× A55) | +| RAM | 8GB / 16GB / 32GB LPDDR5 | +| Storage | M.2 2280 NVMe via PCIe 3.0 ×4 (up to 3500 MB/s) + eMMC | +| Network | Dual 2.5GbE + WiFi 6 + BT 5.0 | +| Video | Dual HDMI (8K + 4K) | +| Power | USB-C, ~3W idle | +| OS | Armbian / Ubuntu / Debian (well-supported) | + +**Alternative Board: Radxa Rock 5B+ (~€85-100)** + +| Spec | Value | +|------|-------| +| SoC | RK3588 (same as Orange Pi 5 Plus) | +| RAM | Up to 32GB LPDDR5 | +| Storage | 3× M.2 slots (2× NVMe + 1× cellular modem) | +| Network | 2.5GbE + WiFi 6 | +| Bonus | Cellular modem M.2 slot (future: 4G fallback connectivity) | + +**Fallback Board: Raspberry Pi 5 (4GB/8GB)** + +Acceptable but not recommended. Requires M.2 HAT for NVMe, PCIe 2.0 ×1 only (~400 MB/s), single GbE. Use only if other boards unavailable. + +**Enclosure:** Compact aluminum case with passive cooling, mounted in equipment closet or behind a display. + +### Display Node + +| Component | Spec | Notes | +|-----------|------|-------| +| **Board** | Raspberry Pi Zero 2 W (~€15) | ARM64, 512MB RAM, WiFi, mini-HDMI | +| **Output** | Mini-HDMI to TV/monitor | Plugs directly into any display | +| **Network** | WiFi to venue network | Connects to Leaf via LAN / Netbird | +| **Software** | Minimal Linux + Chromium kiosk | Boot → fullscreen browser → Leaf URL | +| **Power** | USB from TV or wall adapter | Most TVs can power a Pi Zero | +| **Cost** | €20-30 all-in per unit | Replaces €50+ HDMI runs | + +No better alternative exists for display nodes at this price point. The Pi Zero 2 W is the reference and recommended board. + +### Display Node Boot Sequence + +``` +1. Power on → Linux boots (< 15s target) +2. WiFi connects (pre-configured or AP setup mode) +3. Netbird mesh establishes (if cross-network) +4. Chromium kiosk → http://leaf.local/display/{node-id} +5. WebSocket handshake → Leaf assigns content view +6. Render loop: state updates → re-render +7. Heartbeat every 5s → Leaf tracks status +``` + +### Display Node Discovery + +New display nodes auto-appear as "Unassigned" in the operator's Display Management panel. The operator names it, assigns it to a view, and the assignment persists across reboots. + +--- + +## 4. Core Infrastructure + +### OS Layer: Proxmox VE + +The Core runs on Proxmox VE on Hetzner dedicated servers. PVE provides LXC containers for lightweight services and KVM VMs for stateful workloads requiring kernel isolation, with a clear path from single-server dev to multi-node production cluster. + +**Why PVE over raw Linux + Docker/K8s:** +- Kubernetes on 1-2 servers is pure overhead — K8s shines at 5+ nodes, and we won't need that until 200+ venues +- Docker Compose on bare metal loses live migration, snapshots, granular resource control +- PVE gives container-level isolation, web management, API automation, and PBS backup integration for free +- Familiar operational model (existing homelab expertise) +- Horizontal scaling by adding nodes to the PVE cluster — zero re-architecture + +### Scaling Phases + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ DEV PHASE (now) │ +│ 1× Hetzner Dedicated (existing AX41/AX52) │ +│ │ +│ PVE Host │ +│ ├── LXC: felt-core-api (Go backend, 2 vCPU, 2GB) │ +│ ├── LXC: felt-nats (NATS + JetStream, 1 vCPU, 1GB) │ +│ ├── LXC: felt-authentik (Docker-in-LXC, 2 vCPU, 2GB) │ +│ ├── LXC: felt-netbird (unified server + reverse proxy + │ +│ │ Traefik, 2 vCPU, 1GB) │ +│ └── VM: felt-postgres (PostgreSQL 16, 2 vCPU, 4GB) │ +│ │ +│ Total: ~9 vCPU, ~11GB RAM — fits comfortably on AX41 │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ GROWTH PHASE (10-50 venues) │ +│ 2× Hetzner Dedicated (AX52 or EX44) │ +│ PVE Cluster (Hetzner vSwitch for private network) │ +│ │ +│ Node 1 (Application) │ +│ ├── LXC: felt-core-api ×2 (load balanced) │ +│ ├── LXC: felt-nats-1 (NATS cluster node 1) │ +│ ├── LXC: felt-authentik │ +│ └── LXC: felt-netbird (unified + proxy + Traefik) │ +│ │ +│ Node 2 (Data) │ +│ ├── VM: felt-postgres-primary │ +│ ├── VM: felt-postgres-replica (streaming replication) │ +│ ├── LXC: felt-nats-2 (NATS cluster node 2) │ +│ └── LXC: felt-nats-3 (NATS cluster node 3) │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ SCALE PHASE (50-500 venues) │ +│ 3-5× Hetzner AX/EX servers │ +│ PVE Cluster + Ceph distributed storage │ +│ │ +│ Nodes 1-2: API tier (4-8× felt-core-api behind LB) │ +│ Node 3: NATS cluster (3-node for HA + JetStream replication) │ +│ Nodes 4-5: PostgreSQL (primary + sync replica + async replica) │ +│ All nodes: Ceph OSD for distributed storage │ +│ │ +│ Key: API is stateless → scale by adding LXCs behind Netbird LB │ +│ Netbird proxy instances cluster automatically (same domain=HA) │ +│ NATS JetStream → 3-node cluster handles thousands of streams │ +│ PostgreSQL → read replicas for query scaling │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Design Rules for Horizontal Scaling + +1. **Core API is stateless.** All state lives in PostgreSQL and NATS. Any API instance can handle any request. Scale by running more instances behind Netbird's reverse proxy / load balancer. +2. **NATS is the backbone.** All inter-service communication goes through NATS subjects. Services don't know or care which server they're running on. +3. **PostgreSQL is the only shared state.** One primary, streaming replicas for reads. Connection pooling via PgBouncer when needed. +4. **No service has local state.** LXC containers can be live-migrated, destroyed, recreated. Nothing is lost because state is always in PostgreSQL or NATS JetStream. +5. **Configuration as code.** Every LXC/VM is reproducible from Ansible playbooks or PVE API calls. No snowflake servers. + +### Backup Architecture + +``` +┌────────────────────────────────────────────────────────┐ +│ Hetzner PVE Cluster │ +│ │ +│ All LXCs + VMs │ +│ │ │ +│ ├─→ PBS Tier 1 (on-site / same DC) │ +│ │ • Hourly snapshots (retain 24h) │ +│ │ • Daily backups (retain 30 days) │ +│ │ • Fast restore: < 5 min for any LXC/VM │ +│ │ │ +│ └─→ PBS Tier 2 (off-site / home lab) │ +│ • Daily sync from Tier 1 (PBS replication) │ +│ • Weekly full backups (retain 90 days) │ +│ • Connected via Netbird mesh (encrypted, no ports) │ +│ • Dedicated 1Gbit fiber (flat rate) │ +│ • Geographic separation = disaster recovery │ +│ │ +│ PostgreSQL additionally: │ +│ ├─→ Continuous WAL archiving to S3/Hetzner Object │ +│ └─→ Point-in-time recovery (PITR) to any second │ +└────────────────────────────────────────────────────────┘ + +PBS Off-Site (Home Lab): +├── Dedicated Proxmox Backup Server +├── ZFS pool (mirrored, scrubbed weekly) +├── Retention: 90d daily, 12mo monthly +├── Encrypted at rest (PBS encryption) +├── Netbird peer in 'backup-targets' group +└── Accessible only from PVE cluster (Netbird ACL) +``` + +**Recovery scenarios:** + +| Scenario | Recovery Method | RTO | +|----------|----------------|-----| +| LXC corruption | Restore from PBS Tier 1 snapshot | < 5 min | +| Full server failure | Failover to second node, restore from PBS | < 30 min | +| Datacenter outage | Rebuild from PBS Tier 2 (off-site) | < 2 hours | +| Database corruption | PostgreSQL PITR from WAL archive | < 15 min | +| Ransomware/compromise | PBS Tier 2 is air-gapped (pull-only via Netbird) | < 2 hours | +| Leaf SBC failure | New SBC + Felt OS + restore data from Core | < 30 min | + +--- + +## 5. Data Architecture + +### Leaf: LibSQL (Embedded) + +LibSQL provides SQLite compatibility with built-in replication support. The Leaf's database is the live operational store — every tournament action hits this DB first. + +**Why LibSQL over plain SQLite:** +- SQLite-compatible (same SQL, same tooling) +- Built-in WAL-based replication (can stream to remote) +- Supports embedded mode (no server process needed) +- Better concurrent write handling than vanilla SQLite +- Maintained actively by the Turso team +- Can run as embedded library within the Go binary + +### Core: PostgreSQL + +PostgreSQL is the canonical master of record. It stores: +- Complete historical data from all venues +- Player profiles (cross-venue) +- League standings (cross-venue) +- Public-facing tournament schedules and results +- Analytics and reporting data + +### Data Flow + +``` +Tournament action occurs (e.g., player busts out) + │ + ├─→ Written to LibSQL immediately (local, fast, reliable) + ├─→ Broadcast to all WebSocket clients (real-time display update) + └─→ Published to local NATS JetStream (queued for Core sync) + │ + └─→ When online: NATS forwards to Core + │ + └─→ Core ingests into PostgreSQL +``` + +### Core Schema (PostgreSQL) + +```sql +-- Venues (one per Leaf node) +venues ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT UNIQUE, + timezone TEXT DEFAULT 'UTC', + branding JSON, -- logo_url, colors, theme + config JSON, -- default settings + leaf_node_id TEXT UNIQUE, -- hardware ID of the Leaf + last_sync_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Players (cross-venue, canonical profiles) +players ( + id UUID PRIMARY KEY, + first_name TEXT, + last_name TEXT, + nickname TEXT, + email TEXT, + phone TEXT, + photo_url TEXT, + notes TEXT, -- private to venue staff + custom_fields JSONB, + home_venue_id UUID REFERENCES venues(id), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Tournaments (synced from Leaf after completion or periodically) +tournaments ( + id UUID PRIMARY KEY, + venue_id UUID REFERENCES venues(id), + name TEXT NOT NULL, + description TEXT, + league_id UUID REFERENCES leagues(id), + season_id UUID REFERENCES seasons(id), + status TEXT CHECK(status IN ('setup','running','paused','completed','cancelled')), + config JSONB, -- full tournament configuration snapshot + blind_structure JSONB, + started_at TIMESTAMPTZ, + ended_at TIMESTAMPTZ, + final_results JSONB, -- denormalized final standings + synced_from_leaf_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Tournament Entries (each player's participation in a tournament) +tournament_entries ( + id UUID PRIMARY KEY, + tournament_id UUID REFERENCES tournaments(id), + player_id UUID REFERENCES players(id), + status TEXT CHECK(status IN ('registered','active','busted','winner')), + finish_position INTEGER, + buy_in_amount NUMERIC(10,2), + total_cost NUMERIC(10,2), -- buy-in + rebuys + addons + bounty + rebuys INTEGER DEFAULT 0, + addons INTEGER DEFAULT 0, + bounties_collected INTEGER DEFAULT 0, + busted_by UUID REFERENCES players(id), + prize_won NUMERIC(10,2) DEFAULT 0, + bounty_won NUMERIC(10,2) DEFAULT 0, + points_earned NUMERIC(10,4) DEFAULT 0, + playing_time_seconds INTEGER, + bought_in_at TIMESTAMPTZ, + busted_at TIMESTAMPTZ, + UNIQUE(tournament_id, player_id) +) + +-- Transactions (full financial audit trail) +transactions ( + id UUID PRIMARY KEY, + tournament_id UUID REFERENCES tournaments(id), + player_id UUID REFERENCES players(id), + type TEXT CHECK(type IN ('buy_in','rebuy','addon','bounty_collect','prize','refund')), + amount NUMERIC(10,2), + chips INTEGER, + rake NUMERIC(10,2), + receipt_number INTEGER, + operator_id UUID, + voided BOOLEAN DEFAULT FALSE, + voided_at TIMESTAMPTZ, + voided_by UUID, + created_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Action History (event log — the queue replay) +action_history ( + id UUID PRIMARY KEY, + venue_id UUID REFERENCES venues(id), + tournament_id UUID REFERENCES tournaments(id), + player_id UUID, + action TEXT NOT NULL, + details JSONB, + level INTEGER, + clock_time_ms BIGINT, + operator_id UUID, + created_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Leagues +leagues ( + id UUID PRIMARY KEY, + venue_id UUID, -- NULL = cross-venue league + name TEXT NOT NULL, + points_formula TEXT, + scoring_config JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +) + +-- Seasons +seasons ( + id UUID PRIMARY KEY, + league_id UUID REFERENCES leagues(id), + name TEXT NOT NULL, + start_date DATE, + end_date DATE, + status TEXT CHECK(status IN ('active','completed','archived')) +) + +-- Season Standings (materialized/cached) +season_standings ( + season_id UUID REFERENCES seasons(id), + player_id UUID REFERENCES players(id), + total_points NUMERIC(10,4), + tournaments_played INTEGER, + best_finish INTEGER, + total_winnings NUMERIC(10,2), + avg_finish NUMERIC(5,2), + bounties_total INTEGER, + last_updated TIMESTAMPTZ, + PRIMARY KEY(season_id, player_id) +) +``` + +### Leaf Schema (LibSQL) + +The Leaf schema mirrors the Core schema structurally but adds operational fields: + +```sql +-- Same tables as Core, plus: + +-- Display Nodes (Leaf-only, not synced to Core) +display_nodes ( + id TEXT PRIMARY KEY, -- hardware-derived ID + name TEXT, + group_name TEXT, + resolution_w INTEGER, + resolution_h INTEGER, + assigned_view TEXT, + assigned_tournament_id TEXT, + screen_cycle JSON, + last_heartbeat_at TEXT, -- ISO datetime + config JSON +) + +-- Event Rules (can be global or per-tournament) +event_rules ( + id TEXT PRIMARY KEY, + tournament_id TEXT, -- NULL = global defaults + trigger TEXT NOT NULL, + conditions JSON, + actions JSON, + enabled INTEGER DEFAULT 1, + sort_order INTEGER +) + +-- Sync Queue Metadata (tracks what's been synced) +sync_state ( + entity_type TEXT, + entity_id TEXT, + last_synced_at TEXT, + sync_version INTEGER, + PRIMARY KEY(entity_type, entity_id) +) + +-- Clock state stored in tournament record as JSON: +-- { +-- "remaining_ms": 847000, +-- "is_paused": true, +-- "paused_at": "2026-02-27T21:30:00Z", +-- "level_started_at": "2026-02-27T21:15:00Z", +-- "total_elapsed_ms": 5400000 +-- } +``` + +--- + +## 6. Sync & Message Queue Architecture + +### Why NATS JetStream + +| Requirement | NATS JetStream | +|-------------|---------------| +| Runs on Pi | ✅ Single Go binary, ~10MB RAM | +| Persistent queue | ✅ JetStream stores messages to disk | +| Survives offline | ✅ Queues locally, forwards when connected | +| At-least-once delivery | ✅ Ack-based consumer model | +| Ordered replay | ✅ Stream sequences are ordered | +| Lightweight | ✅ No JVM, no Erlang runtime | + +### Message Flow + +``` +┌─────────────────────────────────────────────────┐ +│ LEAF NODE │ +│ │ +│ Go Backend │ +│ │ │ +│ ├─→ LibSQL write (immediate) │ +│ ├─→ WebSocket broadcast (immediate) │ +│ └─→ NATS Publish to local stream (immediate) │ +│ │ │ +│ ▼ │ +│ NATS JetStream (embedded or sidecar) │ +│ Stream: "venue.{venue_id}.events" │ +│ │ │ +│ └─→ When online: NATS leaf connects to │ +│ Core NATS cluster and replicates stream │ +└──────────────────────────────────────────────────┘ + │ + │ NATS leaf-to-hub connection + │ (auto-reconnect, TLS) + ▼ +┌──────────────────────────────────────────────────┐ +│ CORE │ +│ │ +│ NATS Server (hub) │ +│ │ │ +│ └─→ Consumer: sync-worker │ +│ │ │ +│ └─→ Process message → Write to PostgreSQL │ +│ (idempotent upserts keyed on event ID) │ +└───────────────────────────────────────────────────┘ +``` + +### Message Schema + +Every action on the Leaf produces a message: + +```json +{ + "id": "evt_01HQ3J5K7M...", + "venue_id": "venue_copenhagen_01", + "tournament_id": "tourn_friday_50", + "type": "player.busted", + "timestamp": "2026-02-27T21:32:15.847Z", + "sequence": 1247, + "data": { + "player_id": "plr_mikkel", + "busted_by": "plr_thomas", + "finish_position": 12, + "bounty_transferred": true, + "level": 8, + "clock_ms": 423000 + }, + "operator_id": "op_floor_jane" +} +``` + +### NATS Subjects (Topics) + +``` +venue.{id}.tournament.{id}.action -- tournament actions (bust, rebuy, etc.) +venue.{id}.tournament.{id}.clock -- clock state changes (start, pause, advance) +venue.{id}.tournament.{id}.financial -- transactions (buy-in, prize, refund) +venue.{id}.player.updated -- player profile changes +venue.{id}.display.status -- display node health +venue.{id}.system.health -- leaf node health metrics +``` + +### Sync Guarantees + +| Scenario | Behavior | +|----------|----------| +| Internet down during tournament | Events queue in NATS JetStream on local disk. Tournament unaffected. | +| Internet restored | NATS leaf auto-reconnects to hub, replays queued messages in order. | +| Leaf reboots | NATS JetStream recovers from disk. Un-acked messages re-delivered. | +| Core receives duplicate | Idempotent upsert using event `id`. Safe to replay. | +| Core is down | Leaf doesn't care. NATS leaf can't connect, messages stay queued locally. | +| SD card fails | Restore LibSQL from Core PostgreSQL (reverse sync for disaster recovery). | + +### Reverse Sync (Core → Leaf) + +Limited and controlled. The Core can push to the Leaf: + +- Updated player profiles (edited in cloud admin) +- League configuration changes +- New player registrations (from online signup) +- Venue branding/config changes + +These arrive via a separate NATS subject: `core.venue.{id}.sync` and are processed by the Leaf's sync consumer. + +**Rule: During a running tournament, Core never overrides Leaf data for that tournament.** + +--- + +## 7. Feature Specification + +### 6.1 Tournament Engine + +#### 6.1.1 Tournament Clock + +**Core:** +- Countdown timer per level (second-level granularity, millisecond-precision internally) +- Separate break durations +- Pause / resume with visual indicator across all displays +- Manual advance forward / backward +- Jump to any level by number +- Total elapsed time +- Time remaining in current level + +**Alerts:** +- Warning thresholds: configurable (e.g., 60s, 30s, 10s) +- Audio alerts: level change, break start/end, warnings (custom sound files) +- Visual: screen flash/pulse, color transitions per level + +**Sync:** +- Clock state authoritative on the Leaf +- Clients receive ticks via WebSocket (1/sec normal, 10/sec in final 10 seconds) +- Clients interpolate locally for smooth display +- Reconnecting clients receive full clock state immediately + +#### 6.1.2 Blind Structure + +**Schedule Builder:** +- Unlimited levels, each configurable: + - Type: Round or Break + - Game Type: No-Limit / Pot-Limit / Limit + - Game Name: freeform with presets (Hold'em, Omaha, Stud, Razz, HORSE, 8-Game...) + - Small Blind / Big Blind + - Ante (standard or Big Blind Ante) + - Up to 4 additional limit fields + - Duration (minutes) + - Chip-Up designation + - Notes (shown to players) + +**Structure Wizard:** +- Inputs: player count, starting chips, desired duration, chip denominations +- Output: suggested structure with level durations and chip-up timing +- Accounts for stack-to-blind ratio curve, break frequency + +**Templates:** +- Save/load as reusable templates +- Built-in: Turbo (~2hr), Standard (~3-4hr), Deep Stack (~5-6hr), WSOP-style +- Mixed game rotation support (HORSE, 8-Game round definitions) + +#### 6.1.3 Chip Management + +- Define denominations with colors (hex) and values +- Chip-Up (Color-Up) tracking per break +- Visual indicator on displays: "Color Up: Remove $25 chips" +- Total chips in play calculation +- Average stack display + +### 6.2 Financial Engine + +#### 6.2.1 Buy-In Configuration + +| Field | Description | +|-------|-------------| +| Buy-in Amount | Cost to enter | +| Starting Chips | Chips received | +| Per-Player Rake | Amount removed per player (house revenue) | +| Fixed Rake | Flat fee from total pool | +| House Contribution | Amount house adds to pool | +| Bounty Cost | Separate bounty fee (if enabled) | +| Points | League points for buying in | + +- Multiple rake categories (staff fund, league fund, house) +- Late registration cutoff (by level or time) +- Re-entry support (distinct from rebuy — new entry after busting) + +#### 6.2.2 Rebuys + +- Configurable: cost, chips, rake, points +- Limits: max per player, level/time cutoff, chip threshold requirement +- Points can be negative (rebuy penalty in league scoring) + +#### 6.2.3 Add-Ons + +- Configurable: cost, chips, rake, points +- Availability window (typically at first break) + +#### 6.2.4 Bounties + +**Fixed Bounty (Chip Model):** +- Bounty cost added to buy-in, chip issued +- Hitman tracking: who eliminated whom (full chain) +- Bounty chips cashed out at tournament end +- "Restrict bounties" option + +**Progressive Bounty (v1.1):** +- Half collected on elimination, half added to own head +- Mystery bounty variant + +#### 6.2.5 Prize Pool & Payouts + +- Auto-calculation from all financial inputs +- Guaranteed pot support (house covers shortfall) +- Payout structures: percentage, fixed, or custom table +- Rounding: to nearest $1, $5, $10, $100 +- Chop/deal support: ICM calculator, chip-chop, even-chop, custom +- End-of-season withholding: optionally reserve rake portion for season prizes + +#### 6.2.6 Receipts & Transactions + +- Every financial action generates a receipt +- Full transaction log with search/filter +- Transaction editing with audit trail +- Receipt reprint capability + +### 6.3 Player System + +#### 6.3.1 Player Database + +Persistent on Leaf (LibSQL), synced to Core (PostgreSQL). + +- UUID-based IDs (globally unique for cross-venue) +- Fields: name, nickname, photo, email, phone, notes, custom fields +- League memberships +- Import from CSV +- Merge duplicates +- Search with typeahead +- QR code generation per player (for self-check-in) + +#### 6.3.2 Tournament Operations + +**Buy-In Flow:** Search/select player → confirm details → optional auto-seat → receipt → displays update + +**Bust-Out Flow:** Select player → select hitman → bounty transfer → auto-rank → rebalance trigger → displays update + +**Undo:** Bust-out (with re-ranking), rebuy, add-on, buy-in — full action history per player + +**Per-Player Tracking:** Chip count, playing time, seat, moves, rebuys, add-ons, bounties, prize, points, net take, full timestamped action history + +#### 6.3.3 Player Mobile (PWA) + +Access via QR code scan. No login required. + +**Views:** +- Live clock (blinds, timer, next level) +- Full blind schedule (current highlighted) +- Rankings (live bust-out order) +- Prize pool and payout structure +- Personal status (seat, points — after PIN claim) +- League standings +- Upcoming tournaments + +**Technical:** +- WebSocket for real-time updates +- Auto-reconnect with exponential backoff +- Fallback to 5s polling if WS fails +- "Add to Home Screen" PWA prompt + +### 6.4 Table & Seating + +#### 6.4.1 Configuration + +- Tables with configurable seat counts (6-max to 10-max) +- Table names/labels +- Seat availability marking +- Table blueprints (save venue layout) +- Dealer button tracking + +#### 6.4.2 Seating Management + +- Random initial seating on buy-in (fills tables evenly) +- Automatic balancing algorithm: + - Table size difference threshold (configurable) + - Move count fairness (minimize repeat moves) + - Dealer button awareness + - Locked players (player/dealers) + - Break short tables first +- Drag-and-drop manual moves on touch interface +- "Break Table" action (dissolve and distribute) +- Shootout mode (no balancing until 1 per table) + +#### 6.4.3 Seating Display + +- Visual top-down table layout (player names in seats) +- List view (sortable) +- Movement screen (pending moves) +- Dealer button indicator + +### 6.5 Display Management + +#### 6.5.1 Display Node Registry + +Operator panel showing all connected nodes: + +| Column | Info | +|--------|------| +| Name | Operator-assigned | +| Status | Online / Offline / Stale | +| Resolution | Auto-detected | +| View | Current assignment | +| Group | Optional grouping | + +Actions: assign view, rename, group, reboot, remove + +#### 6.5.2 View Types + +**Tournament:** +- Tournament Clock (large countdown, blinds, entries, avg stack) +- Player Rankings (bust-out order, prizes, bounties) +- Seating Chart (visual table layout) +- Blind Schedule (full schedule, current highlighted) +- Final Table (featured player display with chip counts) +- Player Movement (pending moves) +- Prize Pool (payout structure) +- Tournament Lobby (multi-tournament overview) + +**General:** +- Welcome / Promo (venue branding, schedule, announcements) +- League Standings (season leaderboard) +- Upcoming Events (calendar) + +**Cash Game (Phase 2 placeholder):** +- Cash Waitlist +- Cash Game Info + +#### 6.5.3 Customization + +- Theme system (not raw HTML — clean theme picker) +- Pre-built themes (dark and light variants) +- Custom theme builder (colors, fonts, logo, background) +- Sponsor banner areas (configurable) +- Content toggles per view +- Auto font-scaling to resolution + +#### 6.5.4 Screen Cycling & Routing + +- Rotation config: Clock (30s) → Rankings (15s) → Clock... +- Conditional: show Rankings when player busts, return to Clock after 20s +- Override: force view on all/selected screens +- Lock: prevent cycling +- Multi-tournament routing: assign displays to specific tournaments or lobby + +### 6.6 Events & Automation + +#### 6.6.1 Triggers + +`tournament.started`, `tournament.ended`, `level.ended`, `level.started`, `break.started`, `break.ended`, `player.busted`, `player.bought_in`, `player.rebought`, `tables.consolidated`, `final_table.reached`, `bubble.reached`, `timer.warning`, `timer.{N}_remaining` + +#### 6.6.2 Actions + +`play_sound`, `show_message` (overlay with duration/style), `change_view`, `flash_screen`, `change_theme`, `announce` (TTS), `webhook` (HTTP POST, when online), `run_command` + +#### 6.6.3 Rule Builder + +Visual builder: select trigger → set conditions → add actions. No code required. + +``` +WHEN level.ended + AND next_level.type == 'break' + AND next_level.chip_up == true +THEN + play_sound("chip_up.mp3") + show_message("Color Up: Remove {chip_up_denomination} chips", 30s) +``` + +### 6.7 League & Points + +#### 6.7.1 Structure + +- Leagues (named groups, cross-season) +- Seasons (time-bounded within a league) +- Players can belong to multiple leagues +- Tournaments assigned to league + season + +#### 6.7.2 Points Formula Engine + +Custom mathematical formulas with tournament variables: + +**Variables:** `TOTAL_PLAYERS`, `UNIQUE_PLAYERS`, `PLAYER_PLACE`, `REBUYS`, `ADDONS`, `BOUNTIES`, `BUY_IN`, `PRIZE_WON`, `PLAYING_TIME`, `IS_WINNER`, `MADE_FINAL_TABLE`, `IN_THE_MONEY` + +**Functions:** `sqrt()`, `pow()`, `max()`, `min()`, `round()`, `floor()`, `ceil()`, `abs()`, `log()`, `if(condition, true_val, false_val)` + +**Testing:** Input test values, preview all placements, graph distribution curve + +#### 6.7.3 Season Standings + +- Cumulative points across tournaments +- Configurable: count all or best N of M, minimum attendance +- Historical archives +- Available as display node view + +### 6.8 Export + +- CSV (configurable columns) +- JSON (full tournament data) +- HTML (themed export with venue branding) +- Hendon Mob format (v1.2) +- Print (direct from operator UI) + +### 6.9 TDD Data Import + +A critical adoption tool. Venues migrating from The Tournament Director can import their entire history. + +**Supported imports:** +- Blind structures and tournament templates (from TDD XML export) +- Player database (names, contact info, aliases, notes) +- Tournament history (results, payouts, bust-out order) +- League standings and season data +- Custom payout structures + +**Import process:** +1. Venue exports data from TDD (File → Export → XML) +2. Felt import wizard reads the XML, shows preview of what will be imported +3. Venue confirms mapping (player name matching, template naming) +4. Import runs — typically under 2 minutes for years of data +5. Venue's Felt instance now shows their complete history from day one + +**Design goal:** Zero data loss on migration. A venue switching from TDD to Felt should never feel like they're starting over. + +--- + +## 8. UI/UX Design System + +### 7.1 Design Principles + +| Principle | Meaning | +|-----------|---------| +| **Glanceable** | Critical info visible instantly. No hunting. Large type for key numbers. | +| **Touch-native** | 48px minimum tap targets. Swipe gestures. No hover-dependent UI. | +| **Information-dense, not cluttered** | Show a lot of data, but with clear hierarchy and whitespace. | +| **Dark-room ready** | Default dark theme designed for dim poker rooms. Low-glare, high contrast. | +| **Progressively complex** | Simple tasks are simple. Advanced config is available but not in your face. | +| **Consistent vocabulary** | Use poker terminology throughout. "Bust" not "eliminate". "Blinds" not "stakes". | +| **Instant feedback** | Every action shows immediate visual confirmation. Toasts, animations, state changes. | + +### 7.2 Color System + +Two built-in themes inspired by Catppuccin Mocha and Tokyo Night palettes, adapted for poker: + +#### Felt Dark (Default — based on Catppuccin Mocha) + +``` +Background: + Base: #1e1e2e (main background) + Surface 0: #313244 (cards, panels) + Surface 1: #45475a (elevated elements, hover states) + Surface 2: #585b70 (borders, subtle dividers) + +Text: + Primary: #cdd6f4 (main text — Catppuccin "Text") + Secondary: #a6adc8 (subdued labels — Catppuccin "Subtext 0") + Muted: #6c7086 (disabled, hints — Catppuccin "Overlay 0") + +Accent: + Primary: #89b4fa (Catppuccin Blue — buttons, links, active states) + Success: #a6e3a1 (Catppuccin Green — confirmations, chip-up, bought-in) + Warning: #f9e2af (Catppuccin Yellow — timer warnings, approaching limits) + Danger: #f38ba8 (Catppuccin Red — bust-outs, errors, critical) + Info: #89dceb (Catppuccin Sky — informational, neutral actions) + Bounty: #f5c2e7 (Catppuccin Pink — bounty-related highlights) + Prize: #f9e2af (Catppuccin Yellow — money, winnings, prize pool) + +Poker-specific: + Felt Green: #74c7b8 (table felt accent, used sparingly for poker context) + Card White: #f5f5f5 (card faces, receipt backgrounds) +``` + +#### Felt Light (Alternative) + +``` +Background: + Base: #eff1f5 (Catppuccin Latte Base) + Surface 0: #ffffff + Surface 1: #e6e9ef + Surface 2: #ccd0da + +Text: + Primary: #4c4f69 (Catppuccin Latte Text) + Secondary: #6c6f85 + Muted: #9ca0b0 + +Accent: + (Same hues as dark, slightly adjusted saturation for readability on light) + Primary: #1e66f5 + Success: #40a02b + Warning: #df8e1d + Danger: #d20f39 +``` + +#### Venue Customization + +Venues can override: +- Primary accent color (their brand color) +- Logo (placed in header/footer of displays) +- Background image (for display nodes — subtle, dimmed overlay) + +The theme system generates appropriate contrast ratios automatically. + +### 7.3 Typography + +``` +Headings: Inter (clean, professional, excellent at large sizes) +Body: Inter +Monospace: JetBrains Mono (timers, chip counts, blind values) + +Timer display: JetBrains Mono, tabular figures, weight 700 +Blind values: JetBrains Mono, tabular figures, weight 600 +Player names: Inter, weight 500 +Labels: Inter, weight 400, uppercase tracking +0.05em +``` + +**Scale:** +``` +Display XL: 72px (tournament clock countdown on big screens) +Display L: 48px (blind values on display nodes) +Display M: 36px (next level blinds, player count) +H1: 28px (section headers on operator UI) +H2: 22px (subsection headers) +H3: 18px (card titles) +Body: 16px (default text) +Caption: 14px (labels, metadata) +Micro: 12px (timestamps, IDs) +``` + +### 7.4 Component System + +#### Cards +Primary container for grouped information. Rounded corners (12px), subtle border (Surface 2), Surface 0 background. + +``` +┌─────────────────────────────────┐ +│ Tournament: Friday $50 NL │ ← Card title (H3, Secondary color) +│ │ +│ ▶ RUNNING Level 8/24 │ ← Status badge + progress +│ │ +│ 150 / 300 (25) │ ← Blinds in monospace, large +│ │ +│ 12:47 remaining │ ← Timer in monospace, Primary accent +│ │ +│ 24 players │ Avg: 22,500 │ ← Stats row +└─────────────────────────────────┘ +``` + +#### Badges / Status Pills +Rounded pill shapes with semantic colors: +- `▶ RUNNING` — Success green background +- `⏸ PAUSED` — Warning yellow background +- `✓ COMPLETE` — Muted background +- `SETUP` — Info blue background + +#### Action Buttons +- **Primary:** Filled with Primary accent, rounded (8px), 48px min height +- **Secondary:** Outlined with Primary accent border +- **Danger:** Filled with Danger red (bust-out, delete, void) +- **Ghost:** Text-only, used for less important actions + +All buttons have: +- 48px minimum touch target +- Press-state animation (subtle scale + darken) +- Loading state (spinner replacing text) +- Disabled state (muted, no interaction) + +#### Toast Notifications +Slide in from top-right (desktop) or top-center (mobile): +- Success: "Player X bought in — Table 3, Seat 7" +- Info: "Level 9 starting — 200/400 (50)" +- Warning: "Rebuys close after this level" +- Error: "Action failed — please retry" + +Auto-dismiss after 4s, manually dismissible, stackable. + +#### Data Tables +Used for player lists, transaction logs, standings: +- Alternating row backgrounds (Base / Surface 0) +- Sortable columns (tap header to sort) +- Sticky header on scroll +- Row actions via swipe (mobile) or hover menu (desktop) +- Search/filter bar above table + +### 7.5 Operator UI Layout + +#### Navigation + +**Mobile/Tablet (primary):** +Bottom tab bar with 5 primary destinations + floating action button (FAB): + +``` +┌──────────────────────────────────────────┐ +│ [Persistent Header: Clock + Status] │ +│ │ +│ [Content Area — scrollable] │ +│ │ +│ │ +│ ┌───┐ │ +│ │ + │ FAB │ +│ └───┘ │ +├──────────────────────────────────────────┤ +│ Overview │ Players │ Tables │ $ │ ⚙️ │ +└──────────────────────────────────────────┘ +``` + +**Bottom Tabs:** +1. **Overview** — dashboard with live stats +2. **Players** — player list with actions +3. **Tables** — seating chart +4. **Financials** ($) — prize pool, transactions +5. **More** (⚙️) — displays, settings, events, export + +**FAB (Floating Action Button):** +Tap to expand quick actions: +- Bust Player +- Buy In +- Rebuy +- Add-On +- Pause/Resume + +**Persistent Header:** +Always visible, shows: +``` +┌─────────────────────────────────────────────┐ +│ ▶ 12:47 │ Lvl 8 NL Hold'em │ 150/300 (25)│ +│ │ Break in 2 levels │ 24/36 plrs │ +└─────────────────────────────────────────────┘ +``` + +Tapping the header opens the full clock control panel. + +#### Desktop/Laptop (secondary) + +Sidebar navigation (collapsible) + wider content area. Same information architecture, more horizontal space used for side-by-side panels. + +### 7.6 Display Node UI + +Display nodes show purpose-built views optimized for large screens viewed from 3-15 feet away. They serve dual purposes: **tournament displays** during active tournaments and **digital signage** at all other times (or on screens not assigned to a tournament). + +#### Design Rules for Display Views + +1. **Minimum readable distance: 10 feet.** Timer digits must be legible from across the room. +2. **No interactivity.** These are pure read-only displays. +3. **No scrolling.** Everything must fit on one screen (content adapts to resolution). +4. **High contrast.** Even in a well-lit room, text must be immediately readable. +5. **Smooth transitions.** Level changes, bust-outs, and content rotations animate smoothly (no jarring state jumps). +6. **Venue branding.** Logo always present (configurable size/position) — applied automatically to both tournament and info screen views. +7. **Tournament override.** When a tournament starts, assigned screens switch automatically. When it ends or breaks, they revert to their info screen playlist. + +#### Tournament Clock (Primary Display View) + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [Venue Logo] Friday $50 NL Hold'em [Promo] │ +│ │ +│ LEVEL 8 of 24 │ +│ No-Limit Hold'em │ +│ │ +│ │ +│ ░░ 12 : 47 ░░ │ +│ │ +│ │ +│ BLINDS ANTE NEXT LEVEL │ +│ 150 / 300 25 200 / 400 (50) │ +│ │ +│ ┌──────────┬──────────┬──────────┬──────────────────────┐ │ +│ │ ENTRIES │ REMAIN │AVG STACK │ PRIZE POOL │ │ +│ │ 36 │ 24 │ 22,500 │ $1,800 │ │ +│ └──────────┴──────────┴──────────┴──────────────────────┘ │ +│ │ +│ CHIPS IN PLAY: ● $25 ● $100 ● $500 ● $1000 │ +│ │ +│ Next break in 2 levels │ Color up $25 chips at break │ +└──────────────────────────────────────────────────────────────┘ +``` + +**Timer behavior:** +- Normal: white text on dark background +- Warning (configurable, e.g., <60s): text pulses amber +- Final 10s: text pulses red, size increases slightly +- Level change: smooth crossfade animation to new values +- Break: different visual treatment (e.g., tinted background, "BREAK" label replaces timer) + +### 7.7 Player Mobile UI + +Optimized for phones (320px - 428px width). Clean, fast, minimal. + +``` +┌─────────────────────────┐ +│ Friday $50 NL Hold'em │ +│ ▶ RUNNING │ +│ │ +│ 12 : 47 │ ← large, centered +│ │ +│ ┌───────┬────────────┐ │ +│ │ BLINDS│ 150 / 300 │ │ +│ │ ANTE │ 25 │ │ +│ │ NEXT │ 200 / 400 │ │ +│ └───────┴────────────┘ │ +│ │ +│ 24 remain │ Avg 22.5k │ +│ $1,800 prize pool │ +│ │ +│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ +│ 📋 Schedule 📊 Rank │ ← tab links +│ 💰 Payouts 🏆 League│ +└─────────────────────────┘ +``` + +--- + +## 9. Tech Stack + +### Leaf Node + +| Layer | Technology | Rationale | +|-------|------------|-----------| +| **Backend** | Go 1.22+ | Single binary, ARM cross-compile, goroutine concurrency | +| **Database** | LibSQL (embedded via `go-libsql`) | SQLite-compatible, replication-ready, no server process | +| **Message Queue** | NATS + JetStream (embedded) | Go-native, persistent queuing, ~10MB RAM | +| **WebSocket** | `nhooyr.io/websocket` | Clean API, context-aware, works well with Go stdlib | +| **HTTP** | `chi` router | Lightweight, idiomatic, composable middleware | +| **Operator UI** | SvelteKit → static SPA | Small bundle, fast on Pi, excellent mobile/touch | +| **Display Views** | Vanilla HTML/CSS/JS | Zero framework overhead, instant load | +| **Player PWA** | SvelteKit (shared codebase) | PWA manifest, responsive, WebSocket integration | +| **Audio** | `mpv` subprocess or Go audio | Sound file playback on Leaf audio out | +| **Mesh** | Netbird (v0.65+) | WireGuard mesh, reverse proxy ingress, identity-aware SSH | +| **OS** | Armbian (Orange Pi) / Pi OS Lite (RPi) 64-bit | Minimal, well-supported for target SBCs | +| **Process Mgmt** | systemd | Auto-start, restart on crash, logging | + +### Display Node + +| Layer | Technology | +|-------|------------| +| **OS** | Raspberry Pi OS Lite 64-bit (Pi Zero only) | +| **Browser** | Chromium `--kiosk --noerrdialogs --disable-infobars` | +| **Network** | wpa_supplicant + Netbird | +| **Boot** | systemd → Chromium → `http://leaf.local/display/{id}` | + +### Core (Cloud — Hetzner) + +| Layer | Technology | Rationale | +|-------|------------|-----------| +| **Backend** | Go (shared codebase, different build) | Same language as Leaf, shared types/logic | +| **Database** | PostgreSQL 16 | Master of record, shared with Authentik | +| **Identity** | Authentik (self-hosted) | OIDC/OAuth2 IdP, shared by Netbird + Felt | +| **Message Hub** | NATS Server (clustered) | Receives Leaf sync streams | +| **API** | REST + WebSocket | Admin dashboard, venue management, public API | +| **Frontend** | SvelteKit (SSR for public, SPA for admin) | Shared codebase with Leaf operator UI | +| **Infrastructure** | Netbird unified server (v0.65+) | WireGuard mesh, reverse proxy (replaces nginx), DNS zones, SSH, traffic logging | +| **Hosting** | Hetzner dedicated server (Proxmox VE) | LXC containers + PostgreSQL VM | + +--- + +## 10. Display Node Protocol + +### WebSocket Connection + +Display nodes connect: `ws://leaf.local/ws/display/{node-id}` + +**State Update (Leaf → Node):** +```json +{ + "type": "state", + "view": "tournament_clock", + "tournament_id": "abc123", + "ts": 1709042847000, + "data": { + "clock": { "remaining_ms": 847000, "paused": false, "level": 8 }, + "blinds": { "sb": 150, "bb": 300, "ante": 25, "game": "No-Limit Hold'em" }, + "next": { "sb": 200, "bb": 400, "ante": 50, "type": "round" }, + "stats": { "entries": 36, "remaining": 24, "avg_stack": 22500, "prize_pool": 1800 }, + "chips": [{ "value": 25, "color": "#2ecc71" }, { "value": 100, "color": "#000" }], + "chip_up": { "active": false, "denomination": 25, "at_next_break": true } + } +} +``` + +**View Assignment (Leaf → Node):** +```json +{ + "type": "assign", + "view": "player_rankings", + "tournament_id": "abc123", + "theme": "felt_dark", + "config": { "show_bounties": true, "max_rows": 20 }, + "cycle": { "next_view": "tournament_clock", "after_seconds": 15 } +} +``` + +**Heartbeat (Node → Leaf):** +```json +{ + "type": "heartbeat", + "node_id": "dn-001", + "resolution": { "w": 1920, "h": 1080 }, + "uptime": 3847 +} +``` + +**Update Frequency:** +- Normal: 1/sec +- Final 10 seconds: 10/sec +- Paused: on-change only +- Player action: immediate push +- Reconnect: full state dump + +### Digital Signage & Info Screens + +Display nodes are not just tournament tools — they are the venue's entire digital signage system. Between tournaments (or on screens not assigned to tournament views), displays show venue-managed content: upcoming events, drink specials, sponsor ads, league standings, custom announcements, or any content the venue creates. + +**Content Types:** + +| Type | Description | Example | +|------|-------------|---------| +| `info_card` | Full-screen rich content card | "Friday Night €50 Freezeout — Starts at 8PM" | +| `event_promo` | Upcoming event with countdown | "PLO Night in 3 days — 12 seats remaining" | +| `sponsor_ad` | Sponsor/partner branding | Logo + message, time-limited display | +| `menu_board` | Drink/food specials | "Happy Hour 6-8PM — €3 Draft Beer" | +| `league_table` | Current league standings | Top 20 players, points, events played | +| `custom_html` | Freeform HTML/CSS content | Anything the venue wants to show | +| `media` | Image or video playback | Venue branding video, photo slideshow | + +**WYSIWYG Editor with AI Assist:** + +The operator UI includes a visual content editor for creating and managing info screen content. This is not a code editor — it's a drag-and-drop canvas with templates, venue branding auto-applied, and AI assistance for generating professional-looking content from simple prompts. + +Key capabilities: +- **Template gallery:** Pre-designed layouts for events, promos, menus, announcements — all customizable +- **Venue branding:** Logo, colors, fonts auto-applied from venue profile. Every screen looks on-brand without effort. +- **AI content generation:** Operator types "Friday PLO night, €50 buy-in, 8PM start, max 40 players" → AI generates a polished promo card with layout, typography, and imagery. Operator tweaks and publishes. +- **AI image generation:** Generate thematic background images, poker-themed graphics, event artwork from text prompts +- **Rich text editing:** Headlines, body text, images, QR codes, countdowns, live data widgets (current league leader, next event countdown) +- **Scheduling:** Content assigned to time slots. "Show drink specials 6-8PM, then switch to tomorrow's tournament promo." Automated playlists with priority and fallback content. +- **Live data widgets:** Embed live data in info screens — league standings update automatically, event seat counts reflect real registrations, countdowns tick in real-time +- **Per-screen assignment:** Different screens can show different content simultaneously. Bar TV shows drink specials while lobby TV shows event schedule. + +**Content Delivery:** + +Info screen content is stored on the Leaf as static HTML/CSS/JS bundles. The WYSIWYG editor outputs a content bundle that the display node renders in its Chromium kiosk — same pipeline as tournament views, zero additional infrastructure. + +```json +{ + "type": "assign", + "view": "info_screen", + "content_id": "promo_friday_plo", + "config": { + "duration_seconds": 30, + "transition": "fade" + }, + "cycle": { + "playlist": ["promo_friday_plo", "drinks_happy_hour", "league_standings_q1"], + "loop": true, + "override_on_tournament": true + } +} +``` + +**Tournament Override:** When a tournament is active, screens assigned to tournament views automatically switch. When the tournament ends or breaks, they revert to their info screen playlist. Zero operator intervention needed. + +**Sync to Core:** Info screen content bundles sync to Core like any other venue data. A multi-venue operator can create a promo on Core and push it to all venues simultaneously. Sponsors can have their ads deployed across the network from a single dashboard. + +--- + +## 11. API Design + +### REST (Operator) + +Prefix: `/api/v1` + +**Tournaments:** +``` +POST /tournaments Create +GET /tournaments List (active + recent) +GET /tournaments/:id Get full state +PUT /tournaments/:id Update config +POST /tournaments/:id/start Start +POST /tournaments/:id/pause Pause +POST /tournaments/:id/resume Resume +POST /tournaments/:id/advance Next level +POST /tournaments/:id/rewind Previous level +POST /tournaments/:id/jump/:level Jump to level +POST /tournaments/:id/end End +``` + +**Players (in tournament):** +``` +POST /tournaments/:id/players/buyin Buy in +POST /tournaments/:id/players/:pid/bust Bust out +POST /tournaments/:id/players/:pid/rebuy Rebuy +POST /tournaments/:id/players/:pid/addon Add-on +POST /tournaments/:id/players/:pid/undo Undo last action +PUT /tournaments/:id/players/:pid/chips Update chip count +PUT /tournaments/:id/players/:pid/seat Move seat +GET /tournaments/:id/players List +GET /tournaments/:id/rankings Rankings +``` + +**Tables:** +``` +GET /tournaments/:id/tables Tables + seating +POST /tournaments/:id/tables/suggest Balancing suggestions +POST /tournaments/:id/tables/move Execute move +POST /tournaments/:id/tables/break/:tid Break a table +``` + +**Display Nodes:** +``` +GET /displays List +PUT /displays/:id Update assignment +POST /displays/:id/reboot Reboot +DELETE /displays/:id Remove +``` + +**Content / Info Screens:** +``` +GET /content List all content items +POST /content Create (from WYSIWYG editor output) +GET /content/:id Get content item + bundle +PUT /content/:id Update +DELETE /content/:id Delete +POST /content/:id/publish Publish to assigned displays +POST /content/generate AI-assisted content generation +GET /content/templates List available templates +GET /playlists List playlists +POST /playlists Create playlist +PUT /playlists/:id Update playlist (items, schedule, priority) +DELETE /playlists/:id Delete playlist +PUT /displays/:id/playlist Assign playlist to display +``` + +**Player Database:** +``` +GET /players List (search, filter, paginate) +POST /players Create +PUT /players/:id Update +DELETE /players/:id Delete +POST /players/import CSV import +``` + +**TDD Import:** +``` +POST /import/tdd Upload TDD XML export +GET /import/tdd/:id/preview Preview import (show what will be imported) +POST /import/tdd/:id/confirm Confirm and execute import +GET /import/tdd/:id/status Import progress/status +``` + +**Leagues:** +``` +GET /leagues List +POST /leagues Create +GET /leagues/:id/standings Season standings +GET /leagues/:id/seasons Seasons +``` + +### WebSocket Channels + +``` +/ws/operator Operator (all tournaments, full control) +/ws/display/{node-id} Display node (assigned view data) +/ws/tournament/{tournament-id} Player mobile (read-only tournament data) +``` + +--- + +## 12. Security & Threat Model + +**Security is a foundational design constraint, not a feature.** One breach — one leaked player database, one manipulated payout, one compromised Leaf — kills the project before it gets traction. Every architectural decision in this document has been evaluated against the threat model below. + +### 12.1 Threat Model + +#### Assets to Protect + +| Asset | Sensitivity | Impact if Compromised | +|-------|-------------|----------------------| +| Player PII (name, email, phone) | **HIGH** — GDPR Article 4 personal data | Mandatory breach notification, regulatory fines, loss of trust | +| Financial records (buy-ins, payouts, rake) | **HIGH** — real money flows | Fraud allegations, venue liability, loss of all venues | +| Tournament results & standings | **MEDIUM** — competitive integrity | Player disputes, league credibility destroyed | +| Operator credentials | **HIGH** — control access | Unauthorized tournament manipulation | +| Leaf node (physical device) | **MEDIUM** — venue hardware | Data extraction, impersonation | +| Core infrastructure | **CRITICAL** — all venues | Total platform compromise | +| API keys & sync credentials | **HIGH** — inter-system trust | Unauthorized data access, injection | + +#### Threat Actors + +| Actor | Motivation | Capability | +|-------|-----------|------------| +| Disgruntled player | Manipulate standings, access others' data | Low — network-level, social engineering | +| Venue insider (rogue operator) | Skim rake, alter payouts | Medium — physical access to Leaf, valid credentials | +| Competitor | Data theft, service disruption | Medium — targeted attacks | +| Opportunistic attacker | Data theft, ransomware | Medium — automated scanning, known CVEs | +| State actor | Not a realistic threat for this domain | N/A | + +#### Attack Surfaces + +``` +1. Leaf local network (venue WiFi / LAN) + - Player phones on same network as Leaf + - Display nodes on same network + - Potential rogue devices + +2. Core cloud infrastructure (Hetzner) + - Public API endpoints (player PWA, venue pages) + - NATS sync ingress from Leaf nodes + - Authentik login endpoints + +3. Physical devices (Leaf SBC, display nodes) + - SD card / NVMe extraction + - USB port access + - Network cable interception + +4. Supply chain + - Felt OS image integrity + - Dependency vulnerabilities (Go modules, npm packages) + - Update mechanism compromise +``` + +### 12.2 Identity & Authentication Architecture + +#### Authentik (Centralized IdP) + +Authentik is the single source of identity truth, serving both infrastructure (Netbird) and application (Felt) through OIDC. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Authentik (Self-Hosted IdP) │ +│ PostgreSQL-backed, Docker │ +│ https://auth.felt.io │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────────┐ │ +│ │ OIDC Provider: │ │ OIDC Provider: │ │ +│ │ Netbird │ │ Felt Application │ │ +│ │ (infra mesh) │ │ (operator + admin + API) │ │ +│ └────────┬─────────┘ └──────────────┬───────────────┘ │ +└───────────┼──────────────────────────────┼───────────────────┘ + │ │ + ▼ ▼ +┌───────────────────────┐ ┌──────────────────────────────────┐ +│ Netbird Mesh │ │ Felt Application Auth │ +│ │ │ │ +│ • Leaf ↔ Core │ │ Operator (venue): │ +│ • Leaf ↔ Display Nodes│ │ PIN login → local JWT │ +│ • Admin ↔ any Leaf │ │ (works offline, no IdP needed) │ +│ │ │ │ +│ Setup keys for nodes │ │ Core Admin Dashboard: │ +│ OIDC for operators │ │ OIDC via Authentik │ +│ │ │ (SSO, MFA mandatory) │ +│ │ │ │ +│ │ │ API (Leaf ↔ Core sync): │ +│ │ │ mTLS + API key │ +│ │ │ validated with JWKS │ +│ │ │ │ +│ │ │ Player Mobile: │ +│ │ │ Public (no auth for views) │ +│ │ │ PIN claim for personal data │ +└───────────────────────┘ └──────────────────────────────────┘ +``` + +**Why Authentik:** + +| Requirement | Authentik | +|-------------|-----------| +| Self-hosted, open source | ✅ Apache 2.0, Docker-native | +| OIDC provider for Netbird | ✅ First-class integration, documented | +| OIDC provider for app auth | ✅ Full OAuth2/OIDC/SAML | +| Custom auth flows | ✅ Visual flow editor (login, enrollment, recovery) | +| MFA | ✅ TOTP, WebAuthn/FIDO2, SMS | +| PostgreSQL backend | ✅ Shares existing Core PostgreSQL | +| Lightweight | ✅ ~200MB RAM (vs Keycloak's 512MB+ JVM) | +| License | ✅ Apache 2.0 (vs Zitadel's AGPL-3.0) | + +**Rejected:** Keycloak (Java/heavy), Zitadel (AGPL license friction for commercial distribution), Authelia (not a full IdP). + +#### Auth by Context + +| Context | Auth Method | Offline? | MFA? | +|---------|------------|----------|------| +| **Operator on Leaf** | PIN → local JWT (bcrypt hash in LibSQL) | ✅ Yes | Optional (TOTP when online) | +| **Operator SSO** | OIDC via Authentik (when Leaf has internet) | ❌ No | ✅ Required for Admin role | +| **Core Admin** | OIDC via Authentik (mandatory) | ❌ No | ✅ Required | +| **Leaf ↔ Core sync** | mTLS certificate + API key per venue | ✅ Queues offline | N/A | +| **Display nodes** | Netbird setup key (auto-enrolled) | ✅ Connects to Leaf | N/A | +| **Player mobile (public views)** | None — read-only tournament data | ✅ Direct to Leaf | N/A | +| **Player mobile (personal data)** | 6-digit PIN claim | ✅ Direct to Leaf | N/A | + +#### Operator Roles + +| Role | Scope | Permissions | +|------|-------|------------| +| **Admin** | Venue | Full control: config, tournaments, players, financials, displays, settings | +| **Floor** | Tournament | Runtime actions: bust, rebuy, add-on, seat moves, clock control | +| **Viewer** | Tournament | Read-only: see all data, take no actions | +| **Platform Admin** | All venues (Core only) | Full control of all venues, user management, system config | +| **Venue Owner** | Own venue (Core only) | View own venue data, reports, analytics | +| **League Admin** | League (Core only) | League/season config, cross-venue standings | + +### 12.3 Network Security + +#### Zero-Trust Mesh (Netbird v0.65+) + +**Principle:** Deny-by-default. Every connection must be explicitly permitted by policy. + +**Peer Groups & Access Policies:** + +| Group | Members | Can Reach | Protocol | +|-------|---------|-----------|----------| +| `leaf-nodes` | All venue Leaf SBCs | `core-services` (NATS, API) | TCP 4222, 443 | +| `leaf-nodes` | All venue Leaf SBCs | Own `display-nodes` | TCP 80, 443 | +| `display-nodes` | All Pi Zero display endpoints | Own venue `leaf-node` only | TCP 80 | +| `core-services` | Core API, NATS, PostgreSQL | `leaf-nodes` (for push/response) | TCP 4222, 443 | +| `admin-devices` | Operator devices (OIDC auth) | Any `leaf-node`, `core-services` | TCP 80, 443, 22 | +| `backup-targets` | Home lab PBS | Reachable by `core-services` only | TCP 8007 | + +**Network isolation enforced:** +- Display nodes: can ONLY reach their own venue's Leaf. Not the internet. Not other display nodes. Not the Core. +- Display nodes: **Firewall drop-all-inbound** enabled (Netbird setting) — outbound-only posture, no one connects *to* a display node. +- Leaf nodes: can reach Core (sync) and own display nodes. Nothing else. +- Backup: Core can push to home PBS. PBS cannot initiate connections to anything. +- All traffic: WireGuard encrypted (Netbird transport layer). +- **Lazy connections** enabled for Leaf ↔ Core — tunnels activate on-demand, reducing idle resource usage at scale (critical for 500+ venues). + +**Enrollment:** +- Leaf nodes: single-use setup key, auto-assigns `leaf-nodes` group + venue tag +- Display nodes: reusable venue-scoped setup key, auto-assigns `display-nodes` + venue tag +- Admin devices: interactive OIDC login via Authentik +- **Auto-updates:** Netbird agent on all peers updated from management dashboard (Windows/macOS automatic, Linux via fleet management) + +**Custom DNS Zone (felt.internal):** + +Service discovery within the mesh is managed by Netbird's custom DNS zones (v0.63+), eliminating hardcoded IPs: + +| Record | Target | Distributed To | +|--------|--------|----------------| +| `core-api.felt.internal` | Core API LXC Netbird IP | `felt-infra` group | +| `nats.felt.internal` | NATS LXC Netbird IP | `felt-infra` group | +| `postgres.felt.internal` | PostgreSQL VM Netbird IP | `felt-infra` group | +| `auth.felt.internal` | Authentik LXC Netbird IP | `felt-infra` group | +| `pbs.felt.internal` | Home PBS Netbird IP | `felt-infra` group | + +Search domain enabled — services reference `nats.felt.internal` (or just `nats`) instead of IPs. Service migration = DNS record update, zero config changes on consumers. + +**Identity-Aware SSH (v0.60+):** + +Admin SSH to Leaves uses Netbird's native OpenSSH integration with OIDC authentication: + +| IdP Group (Authentik) | SSH Target | Local OS User | Access | +|----------------------|------------|---------------|--------| +| `felt-platform-admins` | Any `leaf-node`, `core-services` | `felt` | Full admin | +| `venue-operators` | Own venue `leaf-node` only | `felt-readonly` | Read-only (logs, status) | +| All others | Denied | — | — | + +Benefits: No SSH key distribution, no `.authorized_keys` management. Onboarding = add to Authentik group. Offboarding = remove from group (instant revocation). Browser-based SSH from Netbird dashboard for emergency maintenance (no laptop needed). + +**Traffic Events Logging:** + +All connections between Netbird peers are logged with source, destination, protocol, and timestamp. This feeds into the security audit system for anomaly detection (e.g., a Leaf suddenly connecting to unusual peers, a display node initiating unexpected outbound connections). + +#### Leaf Local Network Hardening + +The Leaf sits on a venue's LAN alongside player phones and other devices. It must be hardened: + +- **Firewall (nftables):** Only allow inbound on ports 80 (HTTP), 443 (HTTPS/WSS), and Netbird's UDP port. Drop everything else. +- **No SSH by default.** SSH only accessible via Netbird mesh (admin-devices group, identity-aware OIDC auth). Never exposed on LAN. +- **API rate limiting:** Per-IP rate limits on all endpoints. Player PWA endpoints limited to 60 req/min. Operator endpoints limited to 300 req/min. +- **WebSocket origin validation:** Only accept WS connections from known origins (felt.local, felt.io, *.felt.io). +- **mDNS only for discovery.** Leaf advertises `felt.local` via mDNS for convenience but does not trust mDNS for authentication. + +#### Core Public Endpoints + +The Core exposes public HTTPS endpoints through Netbird's built-in reverse proxy: + +- **TLS managed by Netbird proxy.** Automatic Let's Encrypt certificates per subdomain, or static certs with hot-reload. +- **HSTS with preload.** Force HTTPS everywhere. +- **Authentication at proxy layer.** SSO (Authentik), PIN, password — per-service, configurable in Netbird dashboard. +- **Rate limiting:** Configurable per proxy service. +- **DDoS:** Hetzner's built-in DDoS protection + Netbird proxy connection limits. +- **No public database access.** PostgreSQL listens only on Netbird mesh IP (`postgres.felt.internal`), never on public interface. +- **No direct Leaf exposure.** Leaves are only reachable through Netbird mesh — the reverse proxy tunnels traffic via WireGuard, the Leaf has no public IP and no open ports. + +### 12.4 Data Security + +#### Encryption + +| Layer | Method | +|-------|--------| +| Data in transit (Leaf ↔ Core) | WireGuard (Netbird) + TLS 1.3 | +| Data in transit (Player ↔ Leaf via proxy) | TLS 1.3 (HTTPS to Netbird proxy) + WireGuard (proxy to Leaf) | +| Data in transit (Player ↔ Leaf on LAN) | HTTP (acceptable on local network, upgrade to HTTPS for sensitive ops) | +| Data in transit (Admin SSH ↔ Leaf) | WireGuard (Netbird) + SSH encryption (OIDC auth) | +| Data at rest (Leaf NVMe) | LUKS full-disk encryption (key sealed to TPM if SBC supports, otherwise passphrase at boot) | +| Data at rest (Core PostgreSQL) | LUKS volume encryption on VM disk | +| Data at rest (PBS backups) | PBS built-in AES-256-GCM encryption (key not stored on backup server) | +| Secrets in config | Age-encrypted or SOPS, never plaintext in repo | + +#### Multi-Tenancy Isolation + +Every data query on the Core is scoped by `venue_id`. This is enforced at multiple layers: + +1. **API middleware:** Extract venue_id from authenticated context (JWT claim). Inject into every database query. +2. **Database:** Row-Level Security (RLS) policies on PostgreSQL tables. Even if application code has a bug, the database rejects cross-venue queries. +3. **NATS:** Subject namespacing. `venue.{id}.*` — a Leaf can only publish/subscribe to its own venue's subjects. Enforced by NATS authorization. +4. **Audit:** Every cross-venue data access attempt is logged and alerted. + +```sql +-- PostgreSQL RLS example +ALTER TABLE tournaments ENABLE ROW LEVEL SECURITY; + +CREATE POLICY venue_isolation ON tournaments + USING (venue_id = current_setting('app.current_venue_id')::uuid); + +-- Application sets context per request: +-- SET LOCAL app.current_venue_id = '{venue-uuid}'; +``` + +#### PII Handling (GDPR) + +- **Minimization:** Only collect what's needed. Photo is optional. Phone is optional. +- **Purpose limitation:** Player data used only for tournament operations and league standings. +- **Right to erasure:** Player deletion cascades: anonymize tournament entries (keep aggregate stats, remove PII), delete from player database, propagate deletion via NATS to Core. +- **Right to export:** Player can request full data export (all tournaments, standings, transactions) in JSON. +- **Consent:** Explicit opt-in for cloud sync of player data. Local-only operation requires no consent beyond venue's own policies. +- **Data retention:** Configurable per venue. Default: active data indefinitely, soft-deleted data purged after 90 days. +- **Breach notification:** If detected, notify all affected venues within 24 hours (regulatory requirement is 72 hours to DPA). + +### 12.5 Physical Security + +#### Leaf Node Theft/Tampering + +If someone steals the Leaf SBC from a venue: + +- **LUKS encryption:** NVMe is encrypted at rest. Without the passphrase, data is inaccessible. +- **Netbird key revocation:** Operator (or platform admin) revokes the Leaf's Netbird peer. It can no longer reach Core or any display nodes. +- **API key rotation:** Rotate the venue's API key in Core. Old Leaf can't sync. +- **Data on Core is unaffected.** Core has the complete sync'd copy. New Leaf can be provisioned and restored from Core data. + +#### Display Node Theft + +Display nodes contain zero sensitive data. They're stateless browsers. Steal one, get a Pi Zero that tries to connect to a Leaf it can no longer reach. Revoke the Netbird setup key for the venue, re-enroll remaining nodes. + +### 12.6 Application Security + +#### Input Validation + +- All API inputs validated with strict schemas (Go struct tags + custom validators) +- SQL injection: impossible — parameterized queries only, LibSQL and PostgreSQL prepared statements +- XSS: SvelteKit auto-escapes by default. Display views use textContent, never innerHTML. +- CSRF: SameSite cookies + CSRF tokens on state-changing requests +- Formula engine (league points): sandboxed evaluator with whitelist of functions. No eval(). No arbitrary code execution. + +#### Dependency Management + +- Go: `go.sum` for integrity verification. Dependabot or Renovate for update alerts. +- Node (SvelteKit): `package-lock.json` pinned. Audit on every build. +- OS: Unattended security updates on all LXC/VM. Leaf OS has read-only root with overlay for config. +- Container images: pinned to digest, not tag. Rebuilt on CVE alerts. + +#### Audit Trail + +Every action that modifies state produces an audit record: + +```json +{ + "id": "audit_01HQ...", + "timestamp": "2026-02-27T21:32:15.847Z", + "venue_id": "venue_cph_01", + "tournament_id": "tourn_friday_50", + "operator_id": "op_jane", + "operator_role": "floor", + "action": "player.bust", + "target": { "player_id": "plr_mikkel", "busted_by": "plr_thomas" }, + "previous_state": { "status": "active", "seat": "T3-S7" }, + "new_state": { "status": "busted", "finish_position": 12 }, + "ip_address": "192.168.1.42", + "user_agent": "Mozilla/5.0 (iPad; ...)" +} +``` + +Audit records are: +- Append-only (no delete, no update) +- Synced to Core via NATS (immutable once synced) +- Retained indefinitely +- Available for export (compliance, disputes) + +#### Incident Response + +- All security events (failed auth, rate limit hits, unusual access patterns) logged to structured logs +- Alerting via webhook (NATS publish to monitoring subject → Core forwards to notification channel) +- Runbook for common scenarios: compromised Leaf, leaked API key, unauthorized access, GDPR request + +### 12.7 Player Access via Netbird Reverse Proxy + +Players access tournament data from any network. No VPN client, no app install, no relay service. + +**How it works:** + +Netbird's built-in reverse proxy (v0.65+) exposes each Leaf's player-facing endpoints to the internet through an encrypted WireGuard tunnel. Players hit a public HTTPS URL → Netbird proxy terminates TLS → tunnels through WireGuard → arrives at the Leaf's local HTTP/WebSocket server. The player never knows they're going through a tunnel. + +``` +Player scans QR code or opens bookmark + | + https://tournament.venue-name.felt.io/t/{tournament-id} + | + Netbird reverse proxy (on Core, automatic TLS via Let's Encrypt) + | + WireGuard tunnel (encrypted, direct to Leaf peer) + | + Leaf Go backend serves HTTP + WebSocket directly + | + Player sees live clock, blinds, rankings in real-time +``` + +**Netbird proxy configuration per venue (dashboard or API):** + +| Service | Subdomain | Target | Auth | +|---------|-----------|--------|------| +| Player tournament views | `play.venue-name.felt.io` | Leaf peer, port 80, path `/t/*` | None (public) | +| Player personal data | `play.venue-name.felt.io` | Leaf peer, port 80, path `/me/*` | PIN | +| Operator remote access | `admin.venue-name.felt.io` | Leaf peer, port 80, path `/*` | SSO (Authentik) | +| Core admin dashboard | `dashboard.felt.io` | Core API LXC, port 80 | SSO (Authentik, MFA) | + +**QR Code Strategy:** +- QR codes always encode: `https://play.venue-name.felt.io/t/{tournament-id}` +- This URL always works — from anywhere in the world +- On venue WiFi, DNS may optionally resolve `felt.local` for lowest latency (direct to Leaf, no proxy) +- Player PWA can still try local-first: attempt `ws://felt.local/ws/tournament/{id}` with 2s timeout, fall back to the proxy URL +- **No custom relay code.** Netbird proxy handles TLS, tunneling, and auth. The Leaf serves the same HTTP/WebSocket it always does. + +**What this eliminates from the codebase:** +- `internal/sync/relay.go` — no Core relay service +- Core WebSocket hub for player connections — not needed +- NATS consumer for state mirroring to Core — not needed for player access (still needed for data sync) +- Smart routing JavaScript in player PWA — simplified to single URL with optional local-first optimization + +**Security:** +- Netbird proxy enforces auth per service (SSO, PIN, password — configurable in dashboard) +- Traffic is end-to-end encrypted (TLS to proxy, WireGuard to Leaf) +- Rate limiting configurable at the proxy layer +- Leaf never exposed directly to the internet — only reachable through Netbird mesh +- If Leaf is offline, proxy returns 502 — appropriate because tournament isn't running anyway + +**Scaling:** +- Multiple Netbird proxy instances with the same `NB_PROXY_DOMAIN` form a cluster automatically +- Static cert hot-reload supported (no restart needed for cert rotation) +- Proxy is stateless — sessions managed via JWT + +--- + +## 13. Deployment & Operations + +### Initial Setup + +**Core (one-time):** +``` +1. Deploy Authentik on Hetzner (Docker Compose, shares PostgreSQL) +2. Configure Authentik: create Felt OIDC provider + Netbird OIDC provider +3. Deploy Netbird unified server (connected to Authentik for SSO) + - Enable built-in reverse proxy (Traefik TLS passthrough) + - Configure wildcard DNS: *.felt.io → Netbird server + - Configure wildcard DNS: *.proxy.felt.io → Netbird server (if separate) +4. Create Netbird peer groups and zero-trust access policies +5. Create Netbird custom DNS zone: felt.internal + - core-api.felt.internal, nats.felt.internal, postgres.felt.internal, etc. +6. Configure Netbird reverse proxy services: + - dashboard.felt.io → Core API (SSO + MFA) + - auth.felt.io → Authentik (public) +7. Deploy Core Go service + NATS server +8. Generate venue setup key (Netbird) + API key (Felt) per venue +``` + +**Venue Leaf (per venue):** +``` +1. Flash NVMe SSD with Felt OS image (Armbian-based) +2. Insert SSD into Orange Pi 5 Plus (or compatible SBC) +3. Boot, connect Ethernet +4. Access http://felt.local → Setup Wizard: + - Venue name + branding (slug used for subdomain) + - WiFi credentials (for display nodes) + - Netbird setup key (enrolls Leaf into mesh) + - Felt API key (connects to Core sync) + - Operator PINs (local auth) +5. Leaf auto-connects to Core via Netbird mesh +6. Core auto-creates Netbird reverse proxy services for this venue: + - play.venue-name.felt.io → Leaf (public, player views) + - admin.venue-name.felt.io → Leaf (SSO, operator remote access) +7. Venue is live — players can access from anywhere immediately +``` + +**Display Nodes (per venue):** +``` +1. Flash microSD with Felt Display image +2. Insert into Pi Zero 2 W, plug HDMI into TV +3. On first boot: connects to venue WiFi +4. Netbird enrolls via setup key → joins venue display group +5. Chromium kiosk opens → connects to Leaf +6. Appears as "Unassigned" in operator Display panel +7. Operator names it and assigns a view +``` + +### Updates + +- Leaf: auto-check + OTA update (with rollback) +- Display Nodes: updated from Leaf push +- One-tap rollback to previous version + +### Monitoring + +- Leaf health: CPU, RAM, disk, connected clients +- Display node status: online/offline, heartbeat, uptime +- Full operation log: all actions, all operators, timestamped + +--- + +## 14. Roadmap + +### Phase 1: Live Tournament System + +**v1.0 — Core** +- Tournament engine (clock, blinds, chips) +- Financial engine (buy-in, rebuy, add-on, bounties, payouts) +- Player management (database, tournament operations) +- Table and seating (config, random seat, auto-balance) +- Operator UI (mobile-first, Felt Dark theme) +- Display management (node registry, view assignment, real-time) +- Digital signage system (info screens, event promos, sponsor ads, playlists, auto-scheduling) +- WYSIWYG content editor with AI assist (template gallery, AI-generated promo cards and imagery) +- Player mobile PWA (clock, blinds, rankings) +- Player access via Netbird reverse proxy (public HTTPS → WireGuard → Leaf, zero custom relay) +- Multi-tournament support +- League and season management with formula engine +- Events engine (triggers, sounds, messages) +- Export (CSV, JSON, HTML) +- NATS-based sync queue (Leaf → Core when online) +- Authentik IdP (shared by Netbird mesh + Core admin) +- Netbird infrastructure layer: + - WireGuard mesh (Leaf ↔ Core ↔ Display Nodes, zero-trust) + - Built-in reverse proxy (player + operator access to Leaf via HTTPS) + - Custom DNS zone (felt.internal for service discovery) + - Identity-aware SSH (admin access to Leaves via Authentik) + - Firewall policies (drop-inbound on display nodes) + - Lazy connections (on-demand tunnels at scale) + +**v1.1 — Polish** +- Progressive / mystery bounties +- ICM calculator for chops +- Advanced view theming + custom theme builder +- Tournament templates and profiles +- Player self-check-in via QR +- Blind structure wizard +- Display node screen cycling with conditions +- Achievement/badge system + +**v1.2 — Integrations** +- Hendon Mob export +- Cloud admin dashboard +- Remote venue management +- Public venue page + +### Phase 2: Cash Game Management +### Phase 3: Full Venue System +### Phase 4: Native Apps & Platform Maturity + +--- + +## 15. Appendix: TDD Feature Parity Matrix + +| TDD Feature | Felt | Delta | +|-------------|------|-------| +| Tournament Clock | ✅ Parity+ | Multi-device sync, ms precision | +| Blind Structure | ✅ Parity+ | Wizard, templates, mixed games | +| Buy-in / Rebuy / Add-on | ✅ Parity | Full financial model | +| Bounty System | ✅ Parity+ | Fixed + progressive | +| Prize Pool / Payouts | ✅ Parity+ | ICM, chop, season withholding | +| Receipts | ✅ Parity | Digital with audit trail | +| Player Database | ✅ Parity+ | Cloud sync, photos, QR | +| Bust-Out / Undo | ✅ Parity | Full action history | +| Table Balancing | ✅ Parity | Same algorithm factors | +| Seating Chart | ✅ Parity+ | Multi-screen, touch drag-drop | +| Display System | ✅ Surpasses | Multi-screen, per-display routing, no HDMI | +| Events Engine | ✅ Parity+ | Visual builder, more triggers | +| Sounds | ✅ Parity | Audio on Leaf output | +| League/Points | ✅ Parity+ | Same formulas + `if()` + achievements | +| Stats/Export | ✅ Parity | CSV, JSON, HTML, Hendon Mob | +| Multi-Display | ✅ Surpasses | Unlimited displays, no cables | +| Hotkeys | 🔄 Different | Touch UI replaces keyboard shortcuts | +| HTML Layout Editor | 🔄 Different | Theme system replaces raw HTML | +| Windows Desktop | ❌ N/A | Web-based, Pi — different platform | +| Player Mobile | ✅ New | TDD has nothing comparable | +| Multi-Tournament | ✅ New | TDD is single-tournament | +| Offline Resilience | ✅ New | Local-first with async cloud sync | +| Cash Game | 📅 Phase 2 | Not in TDD | +| Venue Mgmt | 📅 Phase 3 | Not in TDD | + +--- + +*Living document. Project built with Claude Code.* diff --git a/docs/felt_phase2_spec.md b/docs/felt_phase2_spec.md new file mode 100644 index 0000000..75b3717 --- /dev/null +++ b/docs/felt_phase2_spec.md @@ -0,0 +1,552 @@ +# Project Felt — Phase 2 Product Specification + +## Cash Game Operations + +**Version:** 0.1 Draft +**Date:** 2026-02-28 +**Depends on:** Phase 1 (Tournament Management) — shared Leaf, displays, player DB, network + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Data Architecture](#2-data-architecture) +3. [Feature Specification](#3-feature-specification) +4. [Display Integration](#4-display-integration) +5. [Player Mobile Experience](#5-player-mobile-experience) +6. [API Design](#6-api-design) +7. [Sync & Cloud Features](#7-sync--cloud-features) +8. [Roadmap](#8-roadmap) + +--- + +## 1. Overview + +### What Phase 2 Adds + +Phase 1 handles tournaments — structured events with a beginning and end. Phase 2 adds cash games — continuous, open-ended sessions that are the daily revenue driver for most poker rooms. + +Cash games have fundamentally different management needs: waitlists instead of registrations, seat availability instead of seating assignments, rake tracking instead of prize pool calculation, session duration instead of bust-out order. + +### Design Principles + +1. **Coexistence.** Tournaments and cash games run simultaneously on the same Leaf. Display nodes can show tournament clocks on some screens and cash game status on others. +2. **Shared player database.** A player who plays tournaments and cash games has one profile. Their history includes both. +3. **Shared dealer pool.** Dealers are assigned to cash tables or tournament tables from the same scheduling system (Phase 3 completes this, but the data model supports it from Phase 2). +4. **Real-time everywhere.** Waitlist updates, seat availability, and table status push to displays and mobile devices in real-time via the same WebSocket infrastructure used for tournaments. +5. **Offline-first.** Cash game management works fully offline. Cloud sync happens when available. + +--- + +## 2. Data Architecture + +### New Tables (Leaf SQLite + Core PostgreSQL) + +```sql +-- Game Type Registry +CREATE TABLE game_types ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "No-Limit Hold'em" + short_name TEXT NOT NULL, -- "NLH" + variant TEXT NOT NULL, -- holdem, omaha, omaha_hilo, stud, stud_hilo, draw, mixed, short_deck, other + betting TEXT NOT NULL, -- no_limit, pot_limit, fixed_limit, spread_limit + max_players INTEGER NOT NULL DEFAULT 10, -- Max seats per table for this game + is_active BOOLEAN NOT NULL DEFAULT true, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Rake Structures +CREATE TABLE rake_structures ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "Standard 5% cap €10" + type TEXT NOT NULL, -- percentage, time_rake, flat + percentage REAL, -- 0.05 for 5% + cap INTEGER, -- Max rake per pot (cents) + time_amount INTEGER, -- Time rake amount per interval (cents) + time_interval INTEGER, -- Time rake interval (minutes) + flat_amount INTEGER, -- Flat rake per hand (cents) + no_flop_no_drop BOOLEAN NOT NULL DEFAULT true, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Cash Tables +CREATE TABLE cash_tables ( + id TEXT PRIMARY KEY, + table_number INTEGER NOT NULL, -- Physical table number + game_type_id TEXT NOT NULL REFERENCES game_types(id), + stakes TEXT NOT NULL, -- "1/2", "2/5", "5/10" + small_blind INTEGER NOT NULL, -- Cents + big_blind INTEGER NOT NULL, -- Cents + straddle BOOLEAN NOT NULL DEFAULT false, + min_buyin INTEGER NOT NULL, -- Cents (e.g., 10000 = €100) + max_buyin INTEGER, -- Cents (NULL = uncapped) + max_players INTEGER NOT NULL DEFAULT 10, + rake_id TEXT REFERENCES rake_structures(id), + status TEXT NOT NULL DEFAULT 'closed', -- closed, open, breaking + dealer_id TEXT REFERENCES players(id), -- Current dealer (Phase 3) + opened_at DATETIME, + closed_at DATETIME, + must_move_for TEXT REFERENCES cash_tables(id), -- If this is a must-move table, which main game + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Seats (real-time seat tracking) +CREATE TABLE cash_seats ( + id TEXT PRIMARY KEY, + table_id TEXT NOT NULL REFERENCES cash_tables(id), + seat_number INTEGER NOT NULL, + player_id TEXT REFERENCES players(id), -- NULL = empty + session_id TEXT REFERENCES cash_sessions(id), + status TEXT NOT NULL DEFAULT 'empty', -- empty, occupied, reserved, away + occupied_at DATETIME, + UNIQUE(table_id, seat_number) +); + +-- Player Sessions +CREATE TABLE cash_sessions ( + id TEXT PRIMARY KEY, + player_id TEXT NOT NULL REFERENCES players(id), + table_id TEXT NOT NULL REFERENCES cash_tables(id), + seat_number INTEGER NOT NULL, + game_type_id TEXT NOT NULL REFERENCES game_types(id), + stakes TEXT NOT NULL, + buyin_total INTEGER NOT NULL DEFAULT 0, -- Total bought in (cents), accumulates with rebuys + cashout_amount INTEGER, -- Final cashout (cents), NULL = still playing + started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + ended_at DATETIME, + duration_minutes INTEGER, -- Calculated on session close + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Session Buy-ins (track each individual buy-in/rebuy) +CREATE TABLE cash_session_buyins ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES cash_sessions(id), + amount INTEGER NOT NULL, -- Cents + type TEXT NOT NULL DEFAULT 'buyin', -- buyin, rebuy, addon + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Waitlists +CREATE TABLE waitlists ( + id TEXT PRIMARY KEY, + game_type_id TEXT NOT NULL REFERENCES game_types(id), + stakes TEXT NOT NULL, -- "1/2", "2/5" — or "any" for game-type-only + is_active BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Waitlist Entries +CREATE TABLE waitlist_entries ( + id TEXT PRIMARY KEY, + waitlist_id TEXT NOT NULL REFERENCES waitlists(id), + player_id TEXT NOT NULL REFERENCES players(id), + position INTEGER NOT NULL, -- Queue position + status TEXT NOT NULL DEFAULT 'waiting', -- waiting, called, seated, expired, removed + called_at DATETIME, -- When player was notified + response_deadline DATETIME, -- Must respond by + notification_sent BOOLEAN NOT NULL DEFAULT false, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Seat Change Requests +CREATE TABLE seat_change_requests ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES cash_sessions(id), + player_id TEXT NOT NULL REFERENCES players(id), + table_id TEXT NOT NULL REFERENCES cash_tables(id), + requested_seat INTEGER, -- NULL = any seat, specific number = that seat + reason TEXT, -- "Prefer seat 1", "Want to move tables" + status TEXT NOT NULL DEFAULT 'pending', -- pending, fulfilled, cancelled + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + fulfilled_at DATETIME +); + +-- Rake Log (per table per time period) +CREATE TABLE rake_log ( + id TEXT PRIMARY KEY, + table_id TEXT NOT NULL REFERENCES cash_tables(id), + period_start DATETIME NOT NULL, + period_end DATETIME NOT NULL, + total_rake INTEGER NOT NULL, -- Cents + hands_dealt INTEGER, -- If tracked + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +### Extended Player Table + +Phase 2 adds columns to the existing `players` table: + +```sql +ALTER TABLE players ADD COLUMN preferred_games TEXT; -- JSON array of game_type_ids +ALTER TABLE players ADD COLUMN preferred_stakes TEXT; -- JSON array of stakes strings +ALTER TABLE players ADD COLUMN total_cash_hours REAL DEFAULT 0; -- Lifetime cash game hours +ALTER TABLE players ADD COLUMN total_cash_sessions INTEGER DEFAULT 0; +``` + +--- + +## 3. Feature Specification + +### 3.1 Game Type Registry + +The venue defines all games they spread. This is the foundation for waitlists, table assignments, dealer skills (Phase 3), and analytics. + +**Built-in game types (pre-populated, editable):** + +| Variant | Betting Structures | +|---------|-------------------| +| Hold'em | No-Limit, Pot-Limit, Fixed-Limit, Spread-Limit | +| Omaha | Pot-Limit, No-Limit, Fixed-Limit | +| Omaha Hi-Lo | Pot-Limit, Fixed-Limit | +| Stud | Fixed-Limit | +| Stud Hi-Lo | Fixed-Limit | +| Razz | Fixed-Limit | +| Draw (2-7, 5-Card) | Fixed-Limit, No-Limit | +| Short Deck (6+) | No-Limit, Ante-only | +| Mixed (H.O.R.S.E., 8-Game, Dealer's Choice) | Rotation | + +Venues can add custom game types. Each game type has a max players setting (e.g., Hold'em = 10, Short Deck = 6). + +### 3.2 Table Management + +**Table lifecycle:** Closed → Open → Breaking → Closed + +**Opening a table:** +1. Operator selects game type and stakes from dropdown +2. System applies default rake structure (editable) +3. Table status changes to "open" +4. Display nodes immediately show the table in the "Games Running" view +5. Waitlist players for this game/stakes are notified + +**Table status board (operator view):** +``` +┌─────────────────────────────────────────────────────────────┐ +│ TABLE MANAGEMENT │ +│ │ +│ Table 1 │ NLH 1/2 │ 8/10 seats │ ● OPEN │ [Manage] │ +│ Table 2 │ PLO 2/5 │ 6/6 seats │ ● OPEN │ [Manage] │ +│ Table 3 │ NLH 1/2 │ 9/10 seats │ ● OPEN │ [Manage] │ +│ Table 4 │ NLH 2/5 │ 5/10 seats │ ● OPEN │ [Manage] │ +│ Table 5 │ — │ — │ ○ CLOSED │ [Open] │ +│ Table 6 │ Tournament │ Tournament │ ● TOURNY │ │ +│ │ +│ [Open New Table] [Close Table] [Break Table] │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Seat map (per table):** +``` +┌──────────────────────────────────────────┐ +│ TABLE 1 — NLH 1/2 — 8/10 occupied │ +│ │ +│ [8] [9] [10] │ +│ [7] [1] │ +│ [6] [2] │ +│ [5] [4] [3] │ +│ │ +│ ● = Occupied ○ = Empty ⊘ = Reserved │ +│ │ +│ Seat 1: Mike S. (sat 2h15m, in €200) │ +│ Seat 2: — EMPTY — [Seat Player] │ +│ Seat 3: Anna K. (sat 45m, in €100) │ +│ ... │ +└──────────────────────────────────────────┘ +``` + +### 3.3 Waitlist Management + +**Waitlist structure:** One waitlist per game-type + stakes combination. If a venue runs NLH 1/2 and NLH 2/5, those are separate waitlists. + +**Player joins waitlist:** +1. Player requests via mobile PWA or operator adds them +2. System assigns queue position +3. Player sees their position on mobile in real-time +4. When a seat opens, the top player is called + +**Calling a player:** +1. Seat opens at a table matching the waitlist +2. System notifies the first player (mobile push + SMS optional) +3. Player has a configurable response window (default: 5 minutes) +4. If player responds "on my way" → seat is reserved +5. If player doesn't respond → marked as expired, next player called +6. Operator can manually override (skip players, call out of order) + +**Waitlist display (shown on venue TVs):** + +``` +┌──────────────────────────────────────────────────┐ +│ WAITLIST │ +│ │ +│ NLH 1/2 │ NLH 2/5 │ PLO 2/5 │ +│ 1. Mike S. │ 1. David R. │ No waitlist │ +│ 2. Anna K. │ 2. Tom H. │ │ +│ 3. Chris L. │ │ │ +│ 4. Sarah M. │ │ │ +│ │ │ │ +│ Tables: 2 open │ Tables: 1 │ Tables: 1 │ +│ Seats avail: 1 │ Seats: FULL │ Seats: 2 │ +└──────────────────────────────────────────────────┘ +``` + +### 3.4 Session Tracking + +When a player sits down at a cash table, a session begins. The session tracks: + +- When they sat down +- All buy-ins and rebuys (amounts and timestamps) +- When they stood up +- Cashout amount (optional — not all venues track this) +- Total duration + +**Privacy considerations:** +- Buy-in and cashout tracking is **venue-configurable.** Some venues want full financial tracking for analytics. Some just want to know who's playing. +- Individual session financials are never visible to other players +- Aggregated data (average buy-in, average session length) available for analytics +- Players can see their own session history in their mobile profile + +### 3.5 Must-Move Tables + +When a main game is full and demand is high, venues open a "must-move" table. Players at the must-move table get automatically moved to the main table when a seat opens (longest-waiting first). + +**How it works:** +1. Operator opens Table 3 as a must-move for Table 1 (both NLH 1/2) +2. System tracks must-move players' time at Table 3 +3. When a seat opens at Table 1, the player who has been at Table 3 the longest is notified +4. Player moves to Table 1. Their session updates (new table, same game, continuous clock) +5. If the must-move table gets short-handed (≤4 players), operator can break it and move remaining players to the main game's waitlist + +### 3.6 Rake Tracking + +Configurable rake structures: + +| Type | How It Works | Example | +|------|-------------|---------| +| Percentage with cap | X% of each pot, capped at Y | 5% up to €10 per hand | +| Time rake | Fixed amount per time interval | €6 per 30 minutes per seat | +| Flat per hand | Fixed amount per hand dealt | €1 per hand | +| No rake | Used for private games | — | + +**No-flop-no-drop:** Configurable per rake structure. If no flop is dealt, no rake is taken. + +Rake is logged per table per period (configurable: per hour, per shift, per session). Venue owner sees daily/weekly/monthly rake revenue in analytics. + +### 3.7 Seat Change Requests + +Players can request a seat change within the same game or to a different table: + +1. Player requests via mobile or tells the floor (operator enters) +2. System queues the request +3. When the requested seat (or any seat at the target table) opens, the requesting player is offered it +4. Player accepts → moved. Player declines → request stays in queue. +5. Seat change requests have lower priority than new waitlist entries (configurable) + +--- + +## 4. Display Integration + +Cash game views use the same display node infrastructure as tournaments. A display node can be assigned to any of these views: + +### Cash Game Views + +**Games Running Board:** +Shows all active cash games — game type, stakes, seat availability, waitlist depth. Updated in real-time. This is the first thing a player sees when they walk in. + +**Table Detail View:** +Shows a specific table's seat map, current players (first name + last initial), and game info. Useful for large rooms where players can't easily see which seats are open. + +**Waitlist View:** +Shows all active waitlists, player names, and positions. Players can check their position without asking the floor. + +**Combined View (Auto-Cycle):** +Cycles between games running, waitlist, and tournament info. Configurable cycle timing. Most venues will use this on their main lobby TV. + +### Display Priority + +When a tournament and cash games are running simultaneously: + +1. Screens assigned to tournament → show tournament +2. Screens assigned to cash → show cash +3. Screens assigned to "auto" → show tournament during active levels, cash during breaks +4. Info screen playlists continue on screens not assigned to either + +--- + +## 5. Player Mobile Experience + +### Cash Game Features on Mobile PWA + +**View current games:** See all running games, stakes, seat availability without walking to the venue or calling. + +**Join waitlist remotely:** A player at home can join the 2/5 NLH waitlist, see their position, and head to the venue when they're close to the top. Game changer for player retention. + +**Session dashboard (during play):** +``` +┌─────────────────────────────┐ +│ YOUR SESSION │ +│ │ +│ Table 1 — NLH 1/2 │ +│ Seat 4 │ +│ Playing for: 2h 45m │ +│ Total in: €300 │ +│ │ +│ [Request Seat Change] │ +│ [Leave Table] │ +└─────────────────────────────┘ +``` + +**Cash game history:** See all past sessions — dates, games played, duration, buy-in/cashout (if tracked). Lifetime stats: total hours played, favorite game type, average session length. + +--- + +## 6. API Design + +### REST (Operator) + +Prefix: `/api/v1` + +**Cash Tables:** +``` +GET /cash/tables List all tables (with current status) +POST /cash/tables Create/configure a table +GET /cash/tables/:id Get table detail (seats, players) +PUT /cash/tables/:id Update table config +POST /cash/tables/:id/open Open table (set game, stakes) +POST /cash/tables/:id/close Close table (end all sessions) +POST /cash/tables/:id/break Start breaking table +``` + +**Seats:** +``` +POST /cash/tables/:id/seats/:num/sit Seat a player +POST /cash/tables/:id/seats/:num/stand Player stands up +POST /cash/tables/:id/seats/:num/reserve Reserve seat (waitlist call) +POST /cash/tables/:id/seats/:num/away Mark player away +POST /cash/tables/:id/transfer Transfer player between seats/tables +``` + +**Sessions:** +``` +GET /cash/sessions List sessions (active + recent) +GET /cash/sessions/:id Get session detail +POST /cash/sessions/:id/buyin Add buy-in/rebuy to session +POST /cash/sessions/:id/cashout Close session with cashout +``` + +**Waitlists:** +``` +GET /cash/waitlists List active waitlists +POST /cash/waitlists/:id/join Add player to waitlist +POST /cash/waitlists/:id/call Call next player +POST /cash/waitlists/:id/remove Remove player from waitlist +GET /cash/waitlists/:id/position Get player's position (used by mobile) +``` + +**Seat Changes:** +``` +POST /cash/seat-changes Request seat change +GET /cash/seat-changes List pending requests +POST /cash/seat-changes/:id/fulfill Fulfill request +POST /cash/seat-changes/:id/cancel Cancel request +``` + +**Game Types:** +``` +GET /cash/game-types List all game types +POST /cash/game-types Create custom game type +PUT /cash/game-types/:id Update +``` + +**Rake:** +``` +GET /cash/rake-structures List rake structures +POST /cash/rake-structures Create +GET /cash/rake/report Rake report (date range, per table/game) +POST /cash/rake/log Log rake for a period +``` + +### WebSocket (Real-Time) + +Cash game state updates use the same WebSocket infrastructure as tournaments: + +``` +ws://leaf.local/ws/cash/tables → Real-time table status updates +ws://leaf.local/ws/cash/waitlist → Waitlist position updates +ws://leaf.local/ws/cash/session/:id → Player's own session updates +``` + +### Player Mobile API + +``` +GET /api/v1/me/cash/sessions My cash session history +GET /api/v1/me/cash/sessions/stats My lifetime cash stats +GET /api/v1/me/cash/waitlist My current waitlist positions +POST /api/v1/me/cash/waitlist/join Join a waitlist +POST /api/v1/me/cash/waitlist/leave Leave a waitlist +POST /api/v1/me/cash/seat-change Request seat change +``` + +--- + +## 7. Sync & Cloud Features + +Cash game data syncs to Core using the same NATS JetStream infrastructure as tournaments. + +**What syncs:** +- Game type registry (Core → Leaf for multi-venue consistency, Leaf → Core for custom types) +- Session records (Leaf → Core for player history and analytics) +- Rake logs (Leaf → Core for financial reporting) +- Waitlist activity is NOT synced (ephemeral, local-only) +- Seat status is NOT synced in real-time (local-only, too volatile) + +**Cloud features (require subscription):** +- Cross-venue cash game history for players +- Remote waitlist joining (player at home joins via `play.venue.felt.io`) +- Cash game analytics (revenue per game type, table utilization, peak hours) +- Multi-venue game type standardization (operator defines games once, pushes to all venues) + +--- + +## 8. Roadmap + +### v2.0 — Core Cash Game + +- Game type registry with built-in types +- Cash table management (open, close, seat tracking) +- Waitlist management with player notification +- Session tracking (start, buy-in, rebuy, cashout, end) +- Display views: games running board, waitlist, table detail +- Player mobile: view games, join waitlist, session dashboard +- Basic rake tracking (percentage with cap) + +### v2.1 — Advanced Features + +- Must-move table automation +- Seat change request system +- All rake types (time rake, flat, no-flop-no-drop) +- Table transfer with continuous session tracking +- Waitlist SMS notifications (Twilio integration) +- Auto-open/close tables based on waitlist depth (configurable threshold) +- Cash game stats in player profiles + +### v2.2 — Analytics & Integration + +- Revenue dashboards (rake by game, table, period) +- Table utilization analytics (seats occupied vs. available over time) +- Peak hour analysis (when are games fullest?) +- Player analytics (session frequency, favorite games, average duration) +- Integration with Phase 3 dealer scheduling (assign dealers to cash tables) + +--- + +*Phase 2 builds on Phase 1's infrastructure — same Leaf, same displays, same network, same player database. Cash games are additive, not a separate product.* diff --git a/docs/felt_phase3_spec.md b/docs/felt_phase3_spec.md new file mode 100644 index 0000000..b7ced8c --- /dev/null +++ b/docs/felt_phase3_spec.md @@ -0,0 +1,894 @@ +# Project Felt — Phase 3 Product Specification + +## Complete Venue Platform + +**Version:** 0.1 Draft +**Date:** 2026-02-28 +**Depends on:** Phase 1 (Tournaments) + Phase 2 (Cash Games) + +--- + +## Table of Contents + +1. [Overview](#1-overview) +2. [Dealer Management](#2-dealer-management) +3. [Player Loyalty System](#3-player-loyalty-system) +4. [Private Venues & Memberships](#4-private-venues--memberships) +5. [Venue Analytics & Reporting](#5-venue-analytics--reporting) +6. [Public Venue Presence](#6-public-venue-presence) +7. [Data Architecture](#7-data-architecture) +8. [API Design](#8-api-design) +9. [Roadmap](#9-roadmap) + +--- + +## 1. Overview + +### What Phase 3 Adds + +Phase 1 manages tournaments. Phase 2 manages cash games. Phase 3 manages **everything else** — the people, the relationships, and the business intelligence that turn a poker room into a venue that players return to. + +Phase 3 has four pillars: +1. **Dealer Management** — Scheduling, skills, shifts, rotation +2. **Player Loyalty** — Points, tiers, rewards, promotions +3. **Private Venues & Memberships** — Access control, member management, invitations +4. **Analytics & Public Presence** — Business intelligence, venue profiles, event publishing + +### Design Principles + +1. **Everything connects.** Dealer scheduling integrates with tournament and cash table assignments. Loyalty points accrue from tournaments AND cash games. Analytics aggregate across all operations. +2. **Progressive adoption.** Venues can enable Phase 3 features individually. A small bar might only want dealer scheduling. A large room wants everything. No feature forces adoption of another. +3. **Player-visible value.** Players should see the value of Phase 3 — loyalty rewards, membership benefits, career stats. This drives word-of-mouth to other venues. +4. **Operator simplicity.** Every Phase 3 feature must be simpler to use than the spreadsheet/whiteboard it replaces. If it's not, we've failed. + +--- + +## 2. Dealer Management + +### 2.1 Dealer Profiles + +Each dealer is a user in the system (they may also be a player — dual roles supported). Their profile contains: + +**Identity:** Name, contact info, photo, employee ID (venue-assigned) + +**Skill Tags:** + +| Category | Tags (examples) | +|----------|----------------| +| Game variants | Hold'em, Omaha, PLO Hi-Lo, Stud, Razz, Draw, Short Deck, Mixed | +| Betting structures | No-Limit, Pot-Limit, Fixed-Limit | +| Format specialization | Cash games, Tournaments, Both | +| Experience level | Trainee, Standard, Experienced, Senior | +| Special skills | High-stakes tables, Final table dealing, Celebrity/VIP events | +| Languages | Danish, English, German, etc. | + +Skills are tagged by the venue manager. A dealer can request skill additions (e.g., after training on a new game), subject to manager approval. + +**Availability Windows:** +Dealers set weekly recurring availability (e.g., "Available Mon-Fri 4PM-12AM, Sat 12PM-12AM, not available Sunday"). They can override specific dates (e.g., "Unavailable Dec 24-26"). The scheduling system respects these. + +### 2.2 Schedule Management + +**Shift Templates:** +Venues define recurring shift templates: + +``` +Morning: 10:00 — 18:00 +Afternoon: 14:00 — 22:00 +Evening: 18:00 — 02:00 +Tournament: Based on tournament schedule (variable) +Split: 10:00 — 14:00, 18:00 — 22:00 +``` + +Templates are building blocks. The actual schedule is assembled from templates assigned to specific dealers on specific dates. + +**Schedule Builder (Operator UI):** + +Week view with dealers on the Y-axis and days on the X-axis. Drag-and-drop shift assignment. Color-coded by shift type. Conflict detection (double-booking, availability violations, skill mismatch) shown in real-time. + +``` +┌──────────────────────────────────────────────────────────────┐ +│ DEALER SCHEDULE — Week of Mar 2 │ +│ │ +│ Mon Tue Wed Thu Fri Sat Sun │ +│ Anna K. [Eve] [Eve] [---] [Eve] [Eve] [Aft] [---] │ +│ Mike S. [Mor] [Mor] [Mor] [Mor] [Mor] [---] [---] │ +│ Chris L. [Aft] [---] [Aft] [Aft] [Eve] [Eve] [Aft] │ +│ Sarah D. [---] [Aft] [Eve] [---] [Aft] [Eve] [Eve] │ +│ Tom H. [Mor] [Mor] [Mor] [Mor] [---] [Mor] [---] │ +│ │ +│ ⚠ Warning: Friday Evening needs 3 dealers, only 2 assigned │ +│ ⚠ Warning: Chris L. exceeds 40h this week │ +└──────────────────────────────────────────────────────────────┘ +``` + +**Scheduling constraints (auto-enforced):** +- Maximum hours per week (configurable, default 40) +- Minimum rest between shifts (configurable, default 10 hours) +- Availability windows respected +- Skill requirements per table/game met +- Minimum staffing levels per shift (configurable) + +### 2.3 Shift Trading + +Dealers can trade shifts with each other — the system facilitates this while enforcing skill requirements. + +**Trading flow:** +1. Dealer A posts a shift for trade ("Friday Evening available") +2. System shows the post to all dealers who: (a) are available that day, (b) have the required skills, (c) wouldn't exceed hour limits +3. Dealer B offers to take it +4. If manager approval is required (venue-configurable): manager approves/rejects +5. If no approval required: trade is instant +6. Both dealers notified. Schedule updated. Audit log recorded. + +**Drop without trade:** A dealer can drop a shift (with enough notice, venue-configurable). The system posts it as an open shift. If unclaimed by a threshold time, the manager is notified. + +### 2.4 Dealer Assignment to Tables + +Phase 3 connects dealer scheduling to Phase 1 (tournaments) and Phase 2 (cash games): + +**Cash game dealer assignment:** +- Operator assigns a dealer to a cash table from the on-shift dealer pool +- System shows only dealers who: (a) are on shift, (b) have the matching game skill +- Dealer rotation timer (configurable: push every 30/45/60 minutes) +- When the rotation timer hits, system suggests the next dealer and shows who's available + +**Tournament dealer assignment:** +- Tournament tables can have assigned dealers +- Multi-table tournaments: dealers rotate across tables per the rotation schedule +- Final table: system can flag dealers with "final table" skill for assignment + +### 2.5 Dealer Mobile Experience + +Dealers access their schedule and shift management via the same mobile PWA (role-based views): + +- **My Schedule:** See upcoming shifts, with venue details and game/table assignments +- **Trade Board:** See available shifts for trade. Offer to take shifts. Post own shifts. +- **Clock In/Out:** Tap to start/end shift. Automatic time tracking. +- **Notifications:** Shift reminders, trade offers, schedule changes, rotation alerts +- **Hours Summary:** Current week hours, pay period total, overtime status + +### 2.6 Work Hours & Reporting + +**Automatic tracking:** Clock-in/clock-out via mobile or Leaf UI. Break tracking (configurable: paid/unpaid breaks, minimum break duration compliance). + +**Reports (for venue manager):** +- Hours per dealer per period (week, month, pay period) +- Overtime alerts +- No-show / late arrival log +- Shift trade history +- Skill utilization (which game skills are most in-demand) + +**Export:** CSV for payroll integration. Standard format compatible with common payroll systems. + +--- + +## 3. Player Loyalty System + +### 3.1 Points Engine + +Points are the currency of loyalty. Venues configure how players earn them. + +**Point accrual rules (all configurable):** + +| Activity | Default Points | Example | +|----------|---------------|---------| +| Tournament entry | 1 point per €1 buy-in | €50 tournament = 50 points | +| Tournament finish (top 10%) | Bonus 50 points | ITM finish = bonus | +| Cash game play | 1 point per hour | 4-hour session = 4 points | +| Cash game buy-in | 0.5 points per €1 | €200 buy-in = 100 points | +| League participation | 10 points per event | Season regular = loyalty bonus | +| Referral | 25 points per new player | Bring a friend | +| Special events | Custom | Venue-defined promotions | + +**Multipliers (stackable):** +- Time-based: "2x points on Tuesday nights" (drive traffic on slow nights) +- Game-based: "1.5x points on PLO tables" (incentivize new games) +- Tier-based: "Gold members earn 1.5x base points" +- Event-based: "3x points during anniversary week" + +**Point expiry:** Configurable. Options: never expire, expire after X months of inactivity, expire after calendar year. Venues choose. + +### 3.2 Tier System + +Players progress through tiers based on point accumulation: + +| Tier | Threshold (example) | Benefits (example) | +|------|--------------------|--------------------| +| Bronze | 0 points | Base point earning, basic profile | +| Silver | 500 points/quarter | 1.25x point multiplier, priority waitlist | +| Gold | 2,000 points/quarter | 1.5x multiplier, free tournament entry/month, comp credits | +| Platinum | 5,000 points/quarter | 2x multiplier, VIP event access, dedicated floor contact | + +**Tier configuration:** +- Tier names, thresholds, and benefits are fully customizable per venue +- Evaluation period: monthly, quarterly, or annual +- Tier maintenance: "maintain or drop" vs. "earn once, keep forever" (venue choice) +- Grace period: configurable period before tier demotion (e.g., 1 month grace after missing threshold) + +### 3.3 Rewards Catalog + +Venues define what players can spend points on: + +| Category | Examples | +|----------|---------| +| Tournament entries | Free buy-in to weekly tournament (500 points) | +| Food & beverage | €10 bar credit (200 points), free coffee (50 points) | +| Merchandise | Venue t-shirt (1,000 points), branded card protector (300 points) | +| Seat upgrades | Priority seat selection in next tournament (100 points) | +| Cash game | Free hour of rake-free play (800 points) | +| Events | VIP event invitation (2,000 points) | +| Custom | Whatever the venue wants to offer | + +**Redemption flow:** +1. Player views rewards on mobile → selects reward → confirms +2. System deducts points, generates reward voucher with unique code +3. Player shows voucher to staff (or staff scans QR code) +4. Staff marks voucher as redeemed in the system +5. All tracked for analytics (which rewards are popular, cost per redemption) + +### 3.4 Automated Promotions + +Venues can create promotions that trigger automatically: + +**Promotion types:** +- **Time-based:** "Double points every Wednesday 2-6PM" +- **Birthday:** "100 bonus points on your birthday month" +- **Win-back:** "Been away 30+ days? Come back and earn 3x points this visit" +- **Achievement:** "Play your 100th tournament? 500 bonus points" +- **Seasonal:** "Holiday tournament series — 2x points on all December events" +- **Referral:** "Refer a friend who plays 3 events — both get 200 bonus points" + +Promotions are created in the operator UI with start/end dates, targeting rules, and automatic application. No manual tracking. + +### 3.5 Cross-Venue Loyalty (Multi-Venue Operators) + +For operators running multiple venues: +- Player loyalty tier is shared across all venues +- Points earned at any venue count toward tier status +- Rewards can be redeemed at any venue (or restricted to earning venue — operator choice) +- Leaderboards can span all venues or be venue-specific +- Corporate-level promotions push to all venues + +### 3.6 Display & Mobile Integration + +**Player mobile view:** +``` +┌─────────────────────────────┐ +│ MY LOYALTY │ +│ │ +│ ★ GOLD MEMBER │ +│ 2,450 / 5,000 to Platinum │ +│ ████████░░░░ 49% │ +│ │ +│ Available Points: 1,200 │ +│ Lifetime Points: 8,750 │ +│ │ +│ [View Rewards] [History] │ +│ │ +│ Active Promotions: │ +│ 🔥 2x points tonight! │ +│ 🎂 Birthday month bonus │ +└─────────────────────────────┘ +``` + +**Display node views:** +- Loyalty leaderboard (top earners this month/quarter) +- Current promotions ("2x Points Tonight!") +- Tier benefits overview +- All auto-cycle in signage playlists + +--- + +## 4. Private Venues & Memberships + +### 4.1 Venue Privacy Modes + +| Mode | Visibility | Registration | +|------|-----------|-------------| +| Public | Anyone sees schedule, events, games | Open — anyone can join | +| Semi-Private | Schedule visible, limited detail | Requires application + approval | +| Private | Invisible to non-members | Invite-only | + +Privacy mode affects: +- Public venue page visibility +- Whether the venue appears in search/discovery +- Whether events are listed publicly +- Whether non-members can join waitlists or register for tournaments + +### 4.2 Membership Management + +**Membership tiers (venue-defined):** + +| Tier | Example | Access | +|------|---------|--------| +| Standard | €20/month | Tournament access, cash game access | +| Premium | €50/month | + Priority waitlist, league eligibility | +| VIP | €100/month | + High-stakes games, private events, guest passes | +| Founding | By invitation | Full access, lifetime rate, governance input | + +**Member lifecycle:** +1. **Application:** Player applies via venue page or invitation link +2. **Approval:** Venue operator reviews application. Auto-approve rules (e.g., referred by existing member) available. +3. **Payment:** Monthly/annual fee (tracked in Felt — payment processing via Stripe or venue's own system) +4. **Active membership:** Access granted per tier +5. **Renewal:** Auto-reminder before expiry. Grace period configurable. +6. **Suspension/Cancellation:** Operator can suspend or cancel. Player can self-cancel with configurable notice period. + +**Invite system:** +- Members generate invite links (limited per member per month — configurable) +- Invited players get a pre-approved application +- Referral credit for the inviting member (loyalty points or other reward) +- Guest passes: temporary access for specific events (configurable per tier — e.g., VIP members get 2 guest passes per month) + +### 4.3 Member Communications + +**In-app messaging:** +- Venue → all members (announcements) +- Venue → tier-specific (e.g., VIP event invitations) +- Venue → individual member (private message) +- Delivered via mobile PWA notification + optional email/SMS + +**Automated communications:** +- Welcome message on membership approval +- Renewal reminders (7 days, 1 day before expiry) +- Event announcements (new tournaments, special events) +- Membership tier changes (upgrade/downgrade notifications) +- Win-back messages for lapsed members + +### 4.4 Member Directory (Optional) + +Venues can enable a member directory (privacy settings per member): +- Members can see who else is a member (first name + last initial) +- Opt-in: members choose what to share (photo, game preferences, bio) +- Useful for building community in private clubs +- Disabled by default — venue must explicitly enable + +--- + +## 5. Venue Analytics & Reporting + +### 5.1 Revenue Dashboard + +**Real-time revenue tracking:** +- Tournament revenue: entry fees collected, house take +- Cash game revenue: rake collected per table, per game type +- Membership revenue: fees collected +- Combined: total revenue by day/week/month/year + +**Visualizations:** +- Revenue trend line (last 30/90/365 days) +- Revenue by source (pie: tournaments vs. cash vs. memberships) +- Revenue by game type (which games make the most money?) +- Revenue by day of week (which nights are strongest?) +- Revenue by time of day (when are the peak revenue hours?) + +### 5.2 Player Analytics + +**Player engagement metrics:** +- Active players (played in last 7/30/90 days) +- New player acquisition rate +- Player retention rate (% returning within 30 days) +- Player lifetime value (total revenue attributed per player) +- Visit frequency distribution + +**Player segmentation:** +- By game preference (tournament vs. cash vs. both) +- By stakes level +- By loyalty tier +- By visit frequency (regular, occasional, lapsed) +- By value (high-value, medium, low) + +All player analytics are aggregated — individual player data is not exposed to other players. + +### 5.3 Operational Analytics + +**Table utilization:** +- Seats occupied vs. available over time (heatmap by hour of day / day of week) +- Average table utilization rate +- Table open/close patterns + +**Tournament analytics:** +- Fill rates (registered vs. capacity) +- Average entries per tournament +- Most popular tournament formats +- Late registration patterns +- Prize pool trends + +**Waitlist analytics:** +- Average wait time by game/stakes +- Waitlist conversion rate (joined → seated) +- Waitlist abandonment rate +- Peak demand times + +**Dealer analytics:** +- Hours worked per dealer +- Shift coverage rates +- Trade frequency +- Rotation compliance + +### 5.4 Multi-Venue Benchmarking + +For operators with multiple venues: +- Side-by-side venue comparison (revenue, utilization, player engagement) +- Identify best practices from top-performing venues +- Flag underperforming venues with specific metrics +- Cross-venue player flow (which players play at multiple venues?) + +### 5.5 Export & Integration + +- **CSV export:** All reports exportable for Excel/accounting +- **PDF reports:** Formatted reports for stakeholders +- **API access:** All analytics available via REST API for custom integrations +- **Scheduled reports:** Auto-generate and email weekly/monthly reports to venue owner + +--- + +## 6. Public Venue Presence + +### 6.1 Venue Profile Page + +Each venue gets a public page at `venue-name.felt.io` (or custom domain): + +- Venue name, logo, photos, description +- Location with map +- Hours of operation +- Current games running (live, for public venues) +- Upcoming events and tournaments +- Game offerings (types and stakes) +- Contact information + +SEO-optimized for "poker near [city]" searches. + +### 6.2 Event Publishing + +**Automated schedule publishing:** +- Weekly tournament schedule auto-published to venue page +- Upcoming special events with registration +- League schedules and current standings + +**External publishing (API integrations):** +- Poker Atlas format export (automatic submission) +- Hendon Mob format export (tournament results) +- Social media API integration (auto-post new events to venue's Facebook/Instagram) +- iCal feed for players to subscribe to venue schedule + +### 6.3 Online Registration + +Players can register for upcoming tournaments from the venue page: +- View tournament details (format, buy-in, blind structure, starting stack) +- Register (with or without pre-payment — venue configurable) +- See current registrant count and capacity +- Cancel registration (within venue's cancellation policy) +- Automatic reminders before the event + +--- + +## 7. Data Architecture + +### New Tables (extends Phase 1 + 2 schema) + +```sql +-- ===================== +-- DEALER MANAGEMENT +-- ===================== + +CREATE TABLE dealers ( + id TEXT PRIMARY KEY, + player_id TEXT REFERENCES players(id), -- Dealer may also be a player + name TEXT NOT NULL, + email TEXT, + phone TEXT, + photo_url TEXT, + employee_id TEXT, + status TEXT NOT NULL DEFAULT 'active', -- active, inactive, terminated + hire_date DATE, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE dealer_skills ( + id TEXT PRIMARY KEY, + dealer_id TEXT NOT NULL REFERENCES dealers(id), + skill_type TEXT NOT NULL, -- game_variant, betting, format, experience, special, language + skill_value TEXT NOT NULL, -- "holdem", "no_limit", "tournament", "senior", "high_stakes", "danish" + verified BOOLEAN NOT NULL DEFAULT false, + verified_by TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE shift_templates ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "Evening" + start_time TIME NOT NULL, -- "18:00" + end_time TIME NOT NULL, -- "02:00" + break_minutes INTEGER DEFAULT 0, + color TEXT, -- Hex color for schedule UI + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE dealer_schedules ( + id TEXT PRIMARY KEY, + dealer_id TEXT NOT NULL REFERENCES dealers(id), + date DATE NOT NULL, + template_id TEXT REFERENCES shift_templates(id), + start_time DATETIME NOT NULL, + end_time DATETIME NOT NULL, + status TEXT NOT NULL DEFAULT 'scheduled', -- scheduled, confirmed, in_progress, completed, no_show, traded + actual_start DATETIME, + actual_end DATETIME, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(dealer_id, date, template_id) +); + +CREATE TABLE dealer_availability ( + id TEXT PRIMARY KEY, + dealer_id TEXT NOT NULL REFERENCES dealers(id), + day_of_week INTEGER, -- 0=Mon, 6=Sun (NULL for specific date override) + specific_date DATE, -- For one-off availability changes + available_from TIME, -- NULL = not available + available_to TIME, + is_available BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE shift_trades ( + id TEXT PRIMARY KEY, + schedule_id TEXT NOT NULL REFERENCES dealer_schedules(id), + offering_dealer TEXT NOT NULL REFERENCES dealers(id), + taking_dealer TEXT REFERENCES dealers(id), -- NULL = open for offers + status TEXT NOT NULL DEFAULT 'posted', -- posted, offered, approved, completed, cancelled + requires_approval BOOLEAN NOT NULL DEFAULT false, + approved_by TEXT, + approved_at DATETIME, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE dealer_time_clock ( + id TEXT PRIMARY KEY, + dealer_id TEXT NOT NULL REFERENCES dealers(id), + schedule_id TEXT REFERENCES dealer_schedules(id), + clock_in DATETIME NOT NULL, + clock_out DATETIME, + break_start DATETIME, + break_end DATETIME, + total_hours REAL, -- Calculated on clock-out + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- ===================== +-- LOYALTY SYSTEM +-- ===================== + +CREATE TABLE loyalty_config ( + id TEXT PRIMARY KEY, + venue_id TEXT NOT NULL, + point_expiry_months INTEGER, -- NULL = never expire + evaluation_period TEXT DEFAULT 'quarterly', -- monthly, quarterly, annual + grace_period_days INTEGER DEFAULT 30, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_tiers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "Gold" + sort_order INTEGER NOT NULL, + threshold_points INTEGER NOT NULL, -- Points needed per evaluation period + point_multiplier REAL NOT NULL DEFAULT 1.0, + color TEXT, -- Hex color for UI + icon TEXT, -- Icon identifier + benefits_json TEXT, -- JSON description of benefits + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_accrual_rules ( + id TEXT PRIMARY KEY, + activity_type TEXT NOT NULL, -- tournament_entry, cash_hour, cash_buyin, league_event, referral, custom + points_per_unit REAL NOT NULL, -- Points per €1, per hour, per event, etc. + unit TEXT NOT NULL, -- "currency", "hour", "event", "flat" + conditions_json TEXT, -- Optional JSON conditions (game type, stakes, time window) + is_active BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_multipliers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "Tuesday Double Points" + multiplier REAL NOT NULL, -- 2.0 for double + conditions_json TEXT NOT NULL, -- Time windows, game types, tier requirements + start_date DATETIME, + end_date DATETIME, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE player_loyalty ( + id TEXT PRIMARY KEY, + player_id TEXT NOT NULL REFERENCES players(id) UNIQUE, + current_tier_id TEXT REFERENCES loyalty_tiers(id), + current_points INTEGER NOT NULL DEFAULT 0, + lifetime_points INTEGER NOT NULL DEFAULT 0, + period_points INTEGER NOT NULL DEFAULT 0, -- Points in current evaluation period + tier_locked_until DATETIME, -- Grace period end + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_transactions ( + id TEXT PRIMARY KEY, + player_id TEXT NOT NULL REFERENCES players(id), + type TEXT NOT NULL, -- earn, redeem, expire, adjust + points INTEGER NOT NULL, -- Positive for earn, negative for redeem/expire + source TEXT, -- tournament_id, session_id, promotion_id, reward_id + description TEXT, -- "Tournament entry: Friday €50 NLH" + balance_after INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_rewards ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "Free Tournament Entry" + description TEXT, + category TEXT NOT NULL, -- tournament, food_beverage, merchandise, seat, cash_game, event, custom + points_cost INTEGER NOT NULL, + min_tier_id TEXT REFERENCES loyalty_tiers(id), -- Minimum tier to access this reward + quantity_available INTEGER, -- NULL = unlimited + quantity_redeemed INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_redemptions ( + id TEXT PRIMARY KEY, + player_id TEXT NOT NULL REFERENCES players(id), + reward_id TEXT NOT NULL REFERENCES loyalty_rewards(id), + points_spent INTEGER NOT NULL, + voucher_code TEXT NOT NULL UNIQUE, + status TEXT NOT NULL DEFAULT 'issued', -- issued, redeemed, expired, cancelled + redeemed_at DATETIME, + redeemed_by TEXT, -- Staff member who confirmed redemption + expires_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE loyalty_promotions ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, -- time_based, birthday, winback, achievement, seasonal, referral + config_json TEXT NOT NULL, -- Trigger conditions, point amounts, multipliers + start_date DATETIME, + end_date DATETIME, + is_active BOOLEAN NOT NULL DEFAULT true, + auto_apply BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- ===================== +-- MEMBERSHIPS +-- ===================== + +CREATE TABLE membership_tiers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, -- "VIP" + description TEXT, + monthly_fee INTEGER, -- Cents (NULL = free tier) + annual_fee INTEGER, -- Cents (discounted annual option) + max_members INTEGER, -- NULL = unlimited + guest_passes_monthly INTEGER DEFAULT 0, + benefits_json TEXT, -- JSON description of access and benefits + sort_order INTEGER NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE memberships ( + id TEXT PRIMARY KEY, + player_id TEXT NOT NULL REFERENCES players(id), + tier_id TEXT NOT NULL REFERENCES membership_tiers(id), + status TEXT NOT NULL DEFAULT 'pending', -- pending, active, suspended, cancelled, expired + applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + approved_at DATETIME, + approved_by TEXT, + billing_cycle TEXT DEFAULT 'monthly', -- monthly, annual + current_period_start DATETIME, + current_period_end DATETIME, + auto_renew BOOLEAN NOT NULL DEFAULT true, + cancelled_at DATETIME, + cancel_reason TEXT, + invited_by TEXT REFERENCES players(id), + invite_code TEXT, + notes TEXT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE guest_passes ( + id TEXT PRIMARY KEY, + member_id TEXT NOT NULL REFERENCES memberships(id), + guest_name TEXT NOT NULL, + guest_email TEXT, + event_id TEXT, -- Specific event, or NULL for general access + valid_date DATE NOT NULL, + status TEXT NOT NULL DEFAULT 'issued', -- issued, used, expired, cancelled + used_at DATETIME, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE venue_settings_privacy ( + id TEXT PRIMARY KEY, + privacy_mode TEXT NOT NULL DEFAULT 'public', -- public, semi_private, private + show_schedule BOOLEAN NOT NULL DEFAULT true, + show_games BOOLEAN NOT NULL DEFAULT true, + show_waitlist BOOLEAN NOT NULL DEFAULT false, + require_membership BOOLEAN NOT NULL DEFAULT false, + member_directory_enabled BOOLEAN NOT NULL DEFAULT false, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## 8. API Design + +### Dealer Management + +``` +GET /dealers List dealers +POST /dealers Create dealer profile +GET /dealers/:id Get dealer detail +PUT /dealers/:id Update dealer +POST /dealers/:id/skills Add skill +DELETE /dealers/:id/skills/:skillId Remove skill + +GET /schedule Get schedule (date range, filters) +POST /schedule Assign shift +PUT /schedule/:id Update shift +DELETE /schedule/:id Remove shift + +GET /shifts/templates List shift templates +POST /shifts/templates Create template + +POST /shifts/trades Post shift for trade +GET /shifts/trades List available trades +POST /shifts/trades/:id/offer Offer to take shift +POST /shifts/trades/:id/approve Approve trade (manager) + +POST /dealers/:id/clock-in Clock in +POST /dealers/:id/clock-out Clock out +GET /dealers/:id/hours Hours report (date range) +``` + +### Loyalty System + +``` +GET /loyalty/config Get loyalty configuration +PUT /loyalty/config Update loyalty configuration +GET /loyalty/tiers List tiers +POST /loyalty/tiers Create tier +PUT /loyalty/tiers/:id Update tier + +GET /loyalty/rules List accrual rules +POST /loyalty/rules Create rule +GET /loyalty/multipliers List active multipliers +POST /loyalty/multipliers Create multiplier + +GET /loyalty/rewards List available rewards +POST /loyalty/rewards Create reward +POST /loyalty/redeem Redeem reward (player) +POST /loyalty/redemptions/:id/confirm Confirm redemption (staff) + +GET /loyalty/promotions List promotions +POST /loyalty/promotions Create promotion + +GET /loyalty/leaderboard Loyalty leaderboard +``` + +### Player Loyalty (Mobile) + +``` +GET /me/loyalty My loyalty status (tier, points, promotions) +GET /me/loyalty/history My point transaction history +GET /me/loyalty/rewards Available rewards for my tier +POST /me/loyalty/redeem Redeem a reward +GET /me/loyalty/vouchers My active vouchers +``` + +### Memberships + +``` +GET /memberships/tiers List membership tiers +POST /memberships/apply Apply for membership +GET /memberships List all memberships (operator) +PUT /memberships/:id Update membership (approve, suspend, etc.) +POST /memberships/:id/renew Trigger renewal + +POST /memberships/:id/guest-pass Issue guest pass +GET /memberships/:id/guest-passes List guest passes + +GET /me/membership My membership status +POST /me/membership/cancel Cancel my membership +POST /me/membership/invite Generate invite link +``` + +### Analytics + +``` +GET /analytics/revenue Revenue dashboard (date range, source filters) +GET /analytics/players Player engagement metrics +GET /analytics/tables Table utilization metrics +GET /analytics/tournaments Tournament performance metrics +GET /analytics/waitlist Waitlist analytics +GET /analytics/dealers Dealer metrics +GET /analytics/loyalty Loyalty program metrics +GET /analytics/export Export report (CSV/PDF) +``` + +### Public Venue + +``` +GET /venue/profile Public venue profile +PUT /venue/profile Update venue profile (operator) +GET /venue/schedule Public event schedule +GET /venue/games Current games (public venues) +POST /venue/events/:id/register Register for event (player) +GET /venue/events/:id/registrations List registrations (operator) +``` + +--- + +## 9. Roadmap + +### v3.0 — Dealer Management + +- Dealer profiles with skill tags +- Shift templates and schedule builder +- Shift trading system +- Clock-in/clock-out with hours tracking +- Dealer assignment to cash tables and tournaments +- Dealer mobile experience (schedule, trade board, clock) +- Basic reporting (hours, shifts, trades) + +### v3.1 — Player Loyalty + +- Points engine with configurable accrual rules +- Tier system with automated evaluation +- Rewards catalog and redemption flow +- Point multipliers (time-based, game-based, tier-based) +- Loyalty display on mobile and venue screens +- Basic promotions (birthday, time-based) + +### v3.2 — Memberships & Privacy + +- Venue privacy modes (public, semi-private, private) +- Membership tiers with fee tracking +- Application and approval workflow +- Invite system and guest passes +- Member communications +- Optional member directory + +### v3.3 — Analytics & Public Presence + +- Revenue dashboards +- Player engagement analytics +- Operational analytics (tables, waitlists, tournaments) +- Public venue profile page +- Event publishing with online registration +- External publishing (Poker Atlas, Hendon Mob, social) +- Scheduled reports + +### v3.4 — Advanced Features + +- Cross-venue loyalty (multi-venue operators) +- Multi-venue benchmarking analytics +- Dealer rotation automation +- Advanced promotions engine (winback, achievement, referral chains) +- Custom reporting builder +- API access for third-party integrations +- White-label options for enterprise + +--- + +*Phase 3 turns Felt from a poker management tool into the operating system for the venue. At this point, every tournament, every cash game, every dealer shift, every player visit, every dollar — it all flows through Felt. That's the endgame.* diff --git a/docs/felt_pitch_deck.pptx b/docs/felt_pitch_deck.pptx new file mode 100644 index 0000000000000000000000000000000000000000..f5bc4a8db7b2f179b624dd448494ce23536cd563 GIT binary patch literal 755752 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
  • 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

    AWh9-)?0SuRn-rhZ-}5NwP=~7 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 {'Sell Price':>12s} Notes") +print(f" {'─' * 65}") +for name, cost, price, notes in hardware: + margin = price - cost + print(f" {name:<25s} €{cost:>7} €{price:>9} {notes}") + +print(f""" + + Leaf hardware options: + • Buy outright: €120 + • Free with 12-month Offline or Pro annual plan + • Casino tiers: hardware included in contract + + Display nodes: + • Priced at cost + shipping + • No recurring fee — they're dumb render devices + • Venues buy as many as they need (most want 2-6) +""") + +# ================================================================ +# REVENUE MODEL WITH NEW PRICING +# ================================================================ +print(f"\n{'=' * 70}") +print("REVISED FINANCIAL MODEL") +print("=" * 70) + +# Infrastructure cost (from capacity analysis) +INFRA_PER_FREE = 0.45 # Actual cost per virtual Leaf (from capacity model) +INFRA_PER_PAID = 0.15 # Pro/Offline on Core (just sync) +INFRA_BASE = 100 # One Hetzner server + +TARGET_ANNUAL_DKK = 60000 * 2 * 12 # 2× 60k DKK/mo +TARGET_ANNUAL_EUR = TARGET_ANNUAL_DKK / EUR_TO_DKK + +print(f"\n Target: 2× {60000:,} DKK/mo = {TARGET_ANNUAL_DKK:,} DKK/yr = ~€{TARGET_ANNUAL_EUR:,.0f}/yr") +print(f" Infrastructure: €{INFRA_BASE}/mo base (real cost from capacity analysis)") + +scenarios = [ + # (name, free, offline, pro, casino_starter, casino_pro, casino_enterprise, casino_ent_price) + ("Year 1: Denmark", 80, 8, 5, 0, 0, 0, 0), + ("Year 2: Nordics + first casino", 250, 20, 15, 2, 0, 0, 0), + ("Year 3: N. Europe + UK", 500, 40, 35, 5, 1, 0, 0), + ("Year 4: International", 800, 60, 60, 8, 3, 1, 1499), +] + +print(f"\n {'─' * 66}") + +for name, free, offline, pro, cs, cp, ce, ce_price in scenarios: + # Revenue + rev_offline = offline * 25 + rev_pro = pro * 100 + rev_cs = cs * 249 + rev_cp = cp * 499 + rev_ce = ce * (ce_price if ce_price else 999) + total_rev = rev_offline + rev_pro + rev_cs + rev_cp + rev_ce + + # Costs + cost_free = free * INFRA_PER_FREE + cost_paid = (offline + pro + cs + cp + ce) * INFRA_PER_PAID + total_cost = INFRA_BASE + cost_free + cost_paid + + # Add second server if >400 free venues + if free > 400: + total_cost += 100 + + net = total_rev - total_cost + annual_net = net * 12 + dkk = annual_net * EUR_TO_DKK + per_person = dkk / 2 / 12 + pct = per_person / 60000 * 100 + + total_venues = free + offline + pro + cs + cp + ce + total_paying = offline + pro + cs + cp + ce + + print(f"\n {name}") + print(f" Total venues: {total_venues} ({free} free, {total_paying} paying)") + detail = f" Paying: {offline} offline, {pro} pro" + if cs: detail += f", {cs} casino-start" + if cp: detail += f", {cp} casino-pro" + if ce: detail += f", {ce} casino-ent" + print(detail) + print(f" Revenue: €{total_rev:>7,.0f}/mo Costs: €{total_cost:>6,.0f}/mo Net: €{net:>7,.0f}/mo") + print(f" Annual: €{annual_net:>8,.0f} → {dkk:>10,.0f} DKK/yr → {per_person:>7,.0f} DKK/mo per person", end="") + if pct >= 95: + print(f" ✅") + else: + print(f" ({pct:.0f}%)") + +# ================================================================ +# THE CASINO MULTIPLIER EFFECT +# ================================================================ +print(f"\n\n{'=' * 70}") +print("THE CASINO MULTIPLIER") +print("=" * 70) +print(f""" + One Casino Pro deal (5 properties × €499/mo) = €2,495/mo = €29,940/yr + + That single deal is worth: + • 100 Offline venues (€25/mo each) + • 25 Pro venues (€100/mo each) + • 10 Casino Starter venues (€249/mo each) + + Two Casino Pro deals + modest venue growth: + 2× Casino Pro: €4,990/mo + 30 Pro venues: €3,000/mo + 20 Offline venues: €500/mo + ──────────────────────────── + Total: €8,490/mo → €101,880/yr → ~759,000 DKK/yr + Per person: ~31,600 DKK/mo (53% of target) + + Add ONE Casino Enterprise deal and you're close to target + with under 100 total paying venues. + + The path: + 1. Saturate Denmark free tier (prove the product) + 2. Convert enthusiast venues to Offline/Pro (prove revenue) + 3. Use those as case studies to land first casino deal + 4. Casino revenue subsidizes everything else +""") + +# ================================================================ +# PRICING COMPARISON +# ================================================================ +print(f"{'=' * 70}") +print("WHAT VENUES PAY TODAY (for comparison)") +print(f"{'=' * 70}") +print(f""" + TDD License: $130 one-time (but stuck on Windows PC) + BravoPokerLive: $200-500/mo (US, waitlist-focused) + Casino CMS poker module: $2,000-10,000/mo (Bally's, IGT, L&W) + Digital signage software: €30-80/mo (separate subscription) + Generic waitlist apps: €20-50/mo + Spreadsheets: Free (but hours of manual work) + + Felt Offline at €25/mo replaces TDD + adds wireless displays + + mobile + signage. That's a no-brainer for any venue currently + on TDD who spends any money on their operation. + + Felt Pro at €100/mo replaces TDD + digital signage + waitlist + app + spreadsheets + manual comp tracking. Five tools for the + price of one. + + Casino Starter at €249/mo is 1/10th what they pay for their + current casino management poker module — and it's better. +""") + diff --git a/docs/felt_repo_structure_v02.md b/docs/felt_repo_structure_v02.md new file mode 100644 index 0000000..14d6d70 --- /dev/null +++ b/docs/felt_repo_structure_v02.md @@ -0,0 +1,485 @@ +# 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