23 KiB
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
- Overview
- Data Architecture
- Feature Specification
- Display Integration
- Player Mobile Experience
- API Design
- Sync & Cloud Features
- 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
- 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.
- Shared player database. A player who plays tournaments and cash games has one profile. Their history includes both.
- 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).
- 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.
- Offline-first. Cash game management works fully offline. Cloud sync happens when available.
2. Data Architecture
New Tables (Leaf SQLite + Core PostgreSQL)
-- 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:
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:
- Operator selects game type and stakes from dropdown
- System applies default rake structure (editable)
- Table status changes to "open"
- Display nodes immediately show the table in the "Games Running" view
- 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:
- Player requests via mobile PWA or operator adds them
- System assigns queue position
- Player sees their position on mobile in real-time
- When a seat opens, the top player is called
Calling a player:
- Seat opens at a table matching the waitlist
- System notifies the first player (mobile push + SMS optional)
- Player has a configurable response window (default: 5 minutes)
- If player responds "on my way" → seat is reserved
- If player doesn't respond → marked as expired, next player called
- 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:
- Operator opens Table 3 as a must-move for Table 1 (both NLH 1/2)
- System tracks must-move players' time at Table 3
- When a seat opens at Table 1, the player who has been at Table 3 the longest is notified
- Player moves to Table 1. Their session updates (new table, same game, continuous clock)
- 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:
- Player requests via mobile or tells the floor (operator enters)
- System queues the request
- When the requested seat (or any seat at the target table) opens, the requesting player is offered it
- Player accepts → moved. Player declines → request stays in queue.
- 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:
- Screens assigned to tournament → show tournament
- Screens assigned to cash → show cash
- Screens assigned to "auto" → show tournament during active levels, cash during breaks
- 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.