# 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.*