Initial docs/project description including financial models
This commit is contained in:
commit
a254f79bd2
9 changed files with 5532 additions and 0 deletions
337
docs/felt_capacity_model.py
Normal file
337
docs/felt_capacity_model.py
Normal file
|
|
@ -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.
|
||||
""")
|
||||
|
||||
109
docs/felt_financials_v5.py
Normal file
109
docs/felt_financials_v5.py
Normal file
|
|
@ -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.
|
||||
""")
|
||||
|
||||
726
docs/felt_grand_vision.md
Normal file
726
docs/felt_grand_vision.md
Normal file
|
|
@ -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."*
|
||||
2106
docs/felt_phase1_spec_v05.md
Normal file
2106
docs/felt_phase1_spec_v05.md
Normal file
File diff suppressed because it is too large
Load diff
552
docs/felt_phase2_spec.md
Normal file
552
docs/felt_phase2_spec.md
Normal file
|
|
@ -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.*
|
||||
894
docs/felt_phase3_spec.md
Normal file
894
docs/felt_phase3_spec.md
Normal file
|
|
@ -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.*
|
||||
BIN
docs/felt_pitch_deck.pptx
Normal file
BIN
docs/felt_pitch_deck.pptx
Normal file
Binary file not shown.
323
docs/felt_pricing_model_v2.py
Normal file
323
docs/felt_pricing_model_v2.py
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Felt Pricing Model v2
|
||||
|
||||
Key insights from Mikkel:
|
||||
- Offline (Leaf hardware) is a separate value from Pro features
|
||||
- Display nodes: cost-recovery only, no recurring
|
||||
- Casinos need tiered pricing (independent, small chain, large)
|
||||
- €25/mo for offline capability alone
|
||||
- €75/mo additional for full Pro features (cash, dealers, loyalty etc)
|
||||
"""
|
||||
|
||||
EUR_TO_DKK = 7.45
|
||||
|
||||
print("=" * 70)
|
||||
print("FELT PRICING MODEL v2")
|
||||
print("=" * 70)
|
||||
|
||||
print("""
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PRICING PHILOSOPHY │
|
||||
│ │
|
||||
│ FREE → Hook them. Full tournament engine in cloud. │
|
||||
│ OFFLINE ADD-ON → They need reliability. Leaf hardware. │
|
||||
│ PRO → They need operations. Cash, dealers, loyalty. │
|
||||
│ CASINO TIERS → They need scale. Enterprise features. │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
""")
|
||||
|
||||
# ================================================================
|
||||
# TIER DEFINITIONS
|
||||
# ================================================================
|
||||
print("=" * 70)
|
||||
print("VENUE TIERS")
|
||||
print("=" * 70)
|
||||
|
||||
tiers = [
|
||||
{
|
||||
"name": "Free",
|
||||
"price": 0,
|
||||
"target": "Enthusiast clubs, small bars, anyone starting",
|
||||
"includes": [
|
||||
"Full tournament engine (unlimited)",
|
||||
"Player mobile access",
|
||||
"Signage editor with AI assist",
|
||||
"Leagues & seasons",
|
||||
"Player database & history",
|
||||
"Regional tournament participation",
|
||||
],
|
||||
"requires": "Internet connection (virtual Leaf in our cloud)",
|
||||
},
|
||||
{
|
||||
"name": "Offline",
|
||||
"price": 25,
|
||||
"target": "Any venue that needs reliability / wants displays",
|
||||
"includes": [
|
||||
"Everything in Free",
|
||||
"Dedicated Leaf node (hardware purchase required)",
|
||||
"Full offline operation — runs without internet",
|
||||
"Wireless display nodes (purchase separately)",
|
||||
"Custom domain support",
|
||||
"Remote admin access via Netbird",
|
||||
],
|
||||
"requires": "Leaf hardware (~€120) + display nodes (~€30 each)",
|
||||
},
|
||||
{
|
||||
"name": "Pro",
|
||||
"price": 100, # 25 offline + 75 pro features
|
||||
"target": "Serious venues wanting the full platform",
|
||||
"includes": [
|
||||
"Everything in Offline",
|
||||
"Cash game management (waitlists, sessions, rake)",
|
||||
"Dealer scheduling & shift management",
|
||||
"Player loyalty system",
|
||||
"Membership management",
|
||||
"Advanced analytics & reporting",
|
||||
"TDD data import wizard",
|
||||
"Priority support",
|
||||
],
|
||||
"requires": "Leaf hardware (included free with annual plan)",
|
||||
},
|
||||
]
|
||||
|
||||
for t in tiers:
|
||||
print(f"\n {'━' * 60}")
|
||||
if t['price'] == 0:
|
||||
print(f" {t['name'].upper():30s} €0/mo")
|
||||
elif t['name'] == 'Pro':
|
||||
print(f" {t['name'].upper():30s} €{t['price']}/mo (€25 offline + €75 pro)")
|
||||
else:
|
||||
print(f" {t['name'].upper():30s} €{t['price']}/mo")
|
||||
print(f" Target: {t['target']}")
|
||||
print(f" Requires: {t['requires']}")
|
||||
print(f" Includes:")
|
||||
for item in t['includes']:
|
||||
print(f" ✓ {item}")
|
||||
|
||||
# ================================================================
|
||||
# CASINO / ENTERPRISE TIERS
|
||||
# ================================================================
|
||||
print(f"\n\n{'=' * 70}")
|
||||
print("CASINO & ENTERPRISE TIERS")
|
||||
print("=" * 70)
|
||||
|
||||
casino_tiers = [
|
||||
{
|
||||
"name": "Casino Starter",
|
||||
"price": "€249/mo",
|
||||
"target": "Independent casino with 1 poker room (5-15 tables)",
|
||||
"tables": "Up to 15 tables",
|
||||
"includes": [
|
||||
"Everything in Pro",
|
||||
"Multi-room support (tournament room + cash room)",
|
||||
"Floor manager role with restricted permissions",
|
||||
"Shift reporting & payroll export",
|
||||
"Compliance-ready audit trail",
|
||||
"8hr response SLA",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Casino Pro",
|
||||
"price": "€499/mo per property",
|
||||
"target": "Small casino chain (2-5 properties, 15-40 tables each)",
|
||||
"tables": "Up to 40 tables per property",
|
||||
"includes": [
|
||||
"Everything in Casino Starter",
|
||||
"Multi-property dashboard",
|
||||
"Cross-property player tracking",
|
||||
"Cross-property loyalty (play at A, redeem at B)",
|
||||
"Centralized dealer pool management",
|
||||
"API access for POS/CMS integration",
|
||||
"Custom branding & white-label displays",
|
||||
"4hr response SLA",
|
||||
"Dedicated onboarding",
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Casino Enterprise",
|
||||
"price": "Custom (€999+/mo)",
|
||||
"target": "Large operators (5+ properties, 40+ tables each)",
|
||||
"tables": "Unlimited",
|
||||
"includes": [
|
||||
"Everything in Casino Pro",
|
||||
"Unlimited properties",
|
||||
"Fleet management (remote Leaf provisioning & updates)",
|
||||
"Regional analytics & benchmarking",
|
||||
"Casino management system API integration (Bally's, IGT, L&W)",
|
||||
"Custom feature development",
|
||||
"White-label everything (player app, venue pages)",
|
||||
"2hr response SLA + dedicated account manager",
|
||||
"On-site installation support",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for t in casino_tiers:
|
||||
print(f"\n {'━' * 60}")
|
||||
print(f" {t['name'].upper():30s} {t['price']}")
|
||||
print(f" Target: {t['target']}")
|
||||
print(f" Tables: {t['tables']}")
|
||||
print(f" Includes:")
|
||||
for item in t['includes']:
|
||||
print(f" ✓ {item}")
|
||||
|
||||
# ================================================================
|
||||
# HARDWARE PRICING
|
||||
# ================================================================
|
||||
print(f"\n\n{'=' * 70}")
|
||||
print("HARDWARE PRICING (cost-recovery, no margin on displays)")
|
||||
print("=" * 70)
|
||||
|
||||
hardware = [
|
||||
("Leaf Node", 120, 120, "Venue brain, NVMe, locked down"),
|
||||
("Display Node", 25, 30, "Pi Zero W2 + case + power, per unit"),
|
||||
("Display Node (4-pack)", 90, 110, "4× display nodes, slight discount"),
|
||||
]
|
||||
|
||||
print(f"\n {'Device':<25s} {'Our Cost':>10s} {'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.
|
||||
""")
|
||||
|
||||
485
docs/felt_repo_structure_v02.md
Normal file
485
docs/felt_repo_structure_v02.md
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue