pokertrip/docs/2026-03-18-pokertrip-design.md
Mikkel Georgsen fef6f5318e Initial project docs — design spec, reference JSX, and GSD kickoff
Personal poker tracker: Go + React + PostgreSQL PWA for tracking
home sessions and poker trips with AI-powered schedule research.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:36:56 +00:00

21 KiB

PokerTrip — Personal Poker Tracker Design Spec

Overview

A personal poker tracker PWA for a tournament player based in Copenhagen. Tracks ad-hoc home sessions across local venues and structured poker trips abroad, each with isolated budgets. Mobile-first "cockpit" for fast input at the table, desktop "command center" for analysis and trip planning. Includes an AI-powered trip research agent that builds tournament schedules from web searches, URLs, and uploaded images (venue flyers, Facebook posts, etc.).

Designed as a personal tool with architectural choices that support a future SaaS launch (multi-user, shared tournament knowledge base, tiered AI access).

User Context

  • Danish developer in Copenhagen, plays tournaments ad-hoc (2 jobs + family = no fixed schedule)
  • Travels for poker a few times a year (upcoming Manila trip May 2026) — trips are where structured planning and budget tracking matter
  • Runs Proxmox on Hetzner with Nginx Proxy Manager, PBS with off-site backup
  • Prefers Catppuccin Mocha theming, JetBrains Mono font
  • Does not code — Claude builds everything

Core Concepts

Home vs. Trip

Everything is either Home or a Trip.

  • Home: Your default poker life. Ad-hoc sessions at local venues in Denmark. No fixed schedule. One ongoing budget/P&L. Rich filtering by venue(s), buyin range, date range, game type.
  • Trip: A planned poker trip with its own dates, location, budget, and tournament schedule. Budget is soft — you set an intended amount but can exceed it. Trips have a lifecycle: planning → active → completed.

Three Layers of Tournament Data

  1. Global Events (public, system-level)

    • Scraped/curated nightly from Hendon Mob, WPT, WSOP, PokerStars Live, APT, etc.
    • Series-level data: "EPT Barcelona, July 15-28"
    • Background River job, builds a baseline calendar of the poker world
    • Available to all users as a browsable calendar
  2. Location Knowledge (community-contributed, anonymized)

    • When a user runs AI research for a location/date range and approves results, the venue and schedule data enriches a shared pool
    • No attribution — can't see who contributed, just that data exists
    • Another user researching the same location gets this as context fed into their own research run (not free access — you must run your own research to benefit)
  3. My Trips (private, per-user)

    • Your trip: name, dates, budget, which events you're registered for, your results
    • Completely invisible to others unless explicitly shared via a friends feature (SaaS)

Venue Profiles (the "app learns" feature)

Users set up venue profiles: "Casino Copenhagen → Thursday → NLH, 500 DKK, 50K GTD, 10K starting stack". When logging a session at that venue on that day, the app pre-fills these defaults. User can always override. Pattern-based auto-detection from history is a future enhancement — manual profiles first.

Tournament Snapshots

During an active session, you can take a photo of the tournament screen (the TV showing clock/blinds/players). A cheap vision model (e.g. Gemini Flash, Haiku) extracts: level, blinds, ante, players remaining, average stack, total chips, next break, prize pool. You manually enter your own stack size. This creates a timestamped snapshot enabling:

  • Stack trajectory charts per session (your stack in BB over time)
  • Your stack vs. average stack overlay
  • "At what BB count do I tend to bust?" analysis
  • Smarter re-entry tilt guard (uses live data from last snapshot)

Re-entry Tilt Guard

When tapping "Re-entry", the app calculates and displays: how many BBs you'll get, total invested so far, break-even cash amount needed. Forces a moment of rational thought before emotional re-entry decisions. Uses blind structure from venue profiles or trip schedule data, enhanced by snapshot data if available.

Data Model

Users

Column Type Notes
id UUID PK
email TEXT unique
password_hash TEXT bcrypt
display_name TEXT
settings JSONB preferred currency, AI config, snapshot model, etc.
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Venues

Column Type Notes
id UUID PK
name TEXT
short_name TEXT
city TEXT
country TEXT
area TEXT neighborhood/district
type TEXT tournament / cash / both
notes TEXT
color TEXT hex color for UI
visibility TEXT public / private
created_by_user_id UUID FK → users
created_at TIMESTAMPTZ

Venue Profiles (per-user defaults)

Column Type Notes
id UUID PK
user_id UUID FK → users
venue_id UUID FK → venues
day_of_week INT 0=Sunday, 1=Monday, etc. (nullable for "any day" default)
default_game_type TEXT NLH, PLO, etc.
default_buyin INT in minor currency unit
default_currency TEXT DKK, PHP, USD, EUR, etc.
default_gtd INT nullable
starting_stack INT chips
blind_structure JSONB array of {level, small, big, ante, duration_min}
notes TEXT

Trips

Column Type Notes
id UUID PK
user_id UUID FK → users
name TEXT "Manila May 2026"
location TEXT "Manila"
country TEXT "Philippines"
start_date DATE
end_date DATE
planned_budget INT soft ceiling
currency TEXT
status TEXT planning / active / completed
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Global Events (scraped)

Column Type Notes
id UUID PK
name TEXT "EPT Barcelona"
series TEXT EPT, WPT, WSOP, etc.
location TEXT
country TEXT
start_date DATE
end_date DATE
source_url TEXT where it was scraped from
details JSONB flexible extra data
scraped_at TIMESTAMPTZ

Location Schedules (community-contributed, anonymized)

Column Type Notes
id UUID PK
location_key TEXT e.g. "manila-2026-05" for dedup/lookup
venue_id UUID FK → venues
date DATE
time TIME
event_code TEXT "#29A" etc.
name TEXT
buyin INT
currency TEXT
guarantee INT nullable
format TEXT NLH, PLO, NLH PKO, etc.
notes TEXT
multi_day BOOLEAN
multi_day_info TEXT "Day 2: May 9, Final: May 10"
starting_stack INT nullable
blind_structure JSONB nullable
source TEXT "ai_research" / "manual" / "scraped"
created_at TIMESTAMPTZ

Sessions (the core table — every tournament/cash game played)

Column Type Notes
id UUID PK
user_id UUID FK → users
venue_id UUID FK → venues
trip_id UUID FK → trips, nullable (null = Home)
schedule_entry_id UUID FK → location_schedules, nullable
date DATE
start_time TIMESTAMPTZ
end_time TIMESTAMPTZ nullable (set when session ends)
game_type TEXT NLH, PLO, etc.
session_type TEXT tournament / cash
buyin INT
currency TEXT
payout INT nullable
finish_position INT nullable
field_size INT nullable
status TEXT registered / playing / busted / cashed / day2 / skipped
late_reg BOOLEAN default false
entry_level INT nullable — what level you joined at
entry_blinds TEXT nullable — e.g. "100/200"
notes TEXT
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Session Re-entries

Column Type Notes
id UUID PK
session_id UUID FK → sessions
timestamp TIMESTAMPTZ when the re-entry happened
level INT nullable
blinds TEXT nullable — e.g. "200/400"
stack_received INT nullable — chips received
cost INT re-entry cost (may differ from original buyin)

Session Snapshots

Column Type Notes
id UUID PK
session_id UUID FK → sessions
timestamp TIMESTAMPTZ
my_stack INT user-entered
level INT extracted from image
blinds_small INT extracted
blinds_big INT extracted
ante INT nullable, extracted
players_remaining INT nullable, extracted
average_stack INT nullable, extracted
total_chips BIGINT nullable, extracted
next_break TEXT nullable, extracted
prize_pool INT nullable, extracted
bb_count NUMERIC calculated: my_stack / blinds_big
image_url TEXT path to stored image

Research Jobs

Column Type Notes
id UUID PK
user_id UUID FK → users
trip_id UUID FK → trips
status TEXT pending / running / completed / failed
prompt TEXT user's research request
model TEXT model ID used
provider_endpoint TEXT OpenRouter/Requesty URL
attachments JSONB array of {type: "image"/"url", value: "..."}
results JSONB structured venue + schedule data proposed by AI
conversation JSONB full tool-use conversation for refinement context
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ

Uploads

Column Type Notes
id UUID PK
user_id UUID FK → users
path TEXT filesystem path
content_type TEXT image/jpeg, image/png, etc.
size_bytes BIGINT
purpose TEXT research / snapshot
created_at TIMESTAMPTZ

Architecture

Phone/Desktop (React PWA)
  ├── Optimistic local state (in-memory + localStorage cache)
  ├── Background sync → API
  └── SSE connection for real-time updates (research progress, etc.)
        │
        ▼
Go API (single binary, embeds React frontend)
  ├── Chi router
  ├── Auth middleware (JWT + refresh tokens)
  ├── REST endpoints (/api/v1/*)
  ├── SSE endpoint for live updates
  ├── River job queue (in-process, Postgres-backed)
  │     ├── Sync/write jobs
  │     ├── AI research jobs (search → fetch → structure)
  │     ├── Snapshot OCR jobs
  │     └── Nightly global events scraper
  └── SearXNG client (for AI web search tool-use)
        │
        ▼
Postgres (10.5.0.109)
  ├── Application data
  └── River job queue tables

SearXNG (LXC on Proxmox)
  └── Metasearch engine, self-hosted, no API keys

Write Path (message queue pattern)

Every mutation from the client follows fire-and-forget:

  1. Client makes optimistic UI update (instant)
  2. POST/PATCH to API → returns 202 Accepted immediately
  3. River job processes the actual DB write
  4. If failure, client resyncs on next pull
  5. SSE pushes confirmations for critical operations

The UI is always ahead of the database. No lag, ever.

API Design

All endpoints versioned under /api/v1/. All write endpoints return 202 Accepted. All list endpoints support cursor pagination + query param filters.

/api/v1/auth
  POST   /register
  POST   /login             → JWT access token + refresh token (httpOnly cookie)
  POST   /refresh           → rotate tokens

/api/v1/me
  GET    /                  → user profile + settings
  PATCH  /                  → update settings

/api/v1/venues
  GET    /                  → list (public + user's private)
  POST   /
  PATCH  /:id

/api/v1/venue-profiles
  GET    /                  → user's venue defaults
  PUT    /:venue_id/:dow    → set/update default

/api/v1/sessions
  GET    /                  → filterable: venue, trip, date range, buyin range, game type, status
  POST   /
  PATCH  /:id              → status changes, payout, notes, end time
  POST   /:id/reentry      → add re-entry (triggers tilt guard response)
  POST   /:id/snapshot      → upload tournament screen photo + stack size

/api/v1/trips
  GET    /
  POST   /
  PATCH  /:id
  GET    /:id/schedule      → trip's tournament schedule

/api/v1/schedule
  GET    /                  → location schedules (query by location_key, date range)
  POST   /                  → manually add schedule entry

/api/v1/research
  POST   /                  → start AI research job
  GET    /:id               → job status + results
  POST   /:id/refine        → iterate with additional context/images
  POST   /:id/approve       → accept results → writes to venues + location_schedules

/api/v1/global-events
  GET    /                  → browse scraped series (filterable by date, location)

/api/v1/events
  GET    /stream            → SSE (research progress, sync confirmations)

/api/v1/settings/models
  GET    /                  → quick picks + full model list from provider API

AI Research System

Research Flow

  1. User creates a trip, hits "Research"
  2. Chat-like interface: text prompt + image uploads + URL links + model picker
  3. POST /research → River job queued
  4. Go backend calls chosen model via OpenRouter/Requesty (OpenAI-compatible endpoint)
  5. Model receives system prompt + tools:
    • search(query) → hits self-hosted SearXNG
    • fetch_url(url) → fetches and extracts page content
    • analyze_image(image) → model's own vision capability on uploaded images
    • create_venue(data) → proposes a new venue
    • create_schedule_entry(data) → proposes a tournament entry
  6. SSE streams progress to browser ("Searching... Found venue... Extracting schedule...")
  7. Results presented as proposed schedule cards with Accept/Edit/Remove per entry
  8. User can refine ("also check 9D Poker Club") → POST /research/:id/refine
  9. User approves → venues (public) + schedule entries written to DB

Shared Knowledge Context

Before starting research, system checks for existing location knowledge for the target location + date range. If found, it's provided as context to the AI ("We already know about these venues and events — build on this"). This requires the user to run their own research — no free access to the knowledge base without contributing.

Snapshot OCR Flow

  1. During active session, tap camera button
  2. Snap tournament screen → upload image
  3. POST /sessions/:id/snapshot with image + user's stack size
  4. River job sends image to snapshot model (cheap/fast vision model, configured separately in settings)
  5. Model extracts: level, blinds, ante, players, average stack, etc.
  6. Returns pre-filled fields → user confirms/corrects → saved

Model Management (Settings UI)

AI Settings:
  Provider:        [OpenRouter ▾]  (OpenRouter / Requesty / Custom)
  Endpoint URL:    [https://openrouter.ai/api/v1]
  API Key:         [••••••••••••]

  Research Model:  Quick picks: Sonnet, Opus, GPT-4o, Gemini Pro, Grok
                   [🔍 Search all models...]

  Snapshot Model:  Quick picks: Haiku, Gemini Flash, GPT-4o-mini
                   [🔍 Search all models...]

  🖼️ Vision support indicator per model

Quick picks populated from provider's /models endpoint, filtered to top models from Anthropic, OpenAI, Google, xAI. Fuzzy search for the full list.

Frontend

Shared Principles

  • Catppuccin Mocha color palette throughout
  • JetBrains Mono / Fira Code font
  • Dark mode only
  • PWA: manifest.json + service worker (Workbox) for offline caching
  • Optimistic UI: every action feels instant

Mobile Cockpit (< 768px)

The in-the-moment tool. Speed and touch-friendliness above all.

Top toggle: Home | Trip (Trip only visible when an active trip exists)

Home mode:

  • "Log a session" prominent button
  • Today's venue suggestion (from venue profiles for this day of week)
  • Last 3-5 sessions as quick history
  • Tap "Log" → pick venue (favorites first, search others) → pre-filled from venue profile → confirm → live session
  • Active session: timer, big thumb-friendly action buttons (Rebuy, Add-on, Bust, Cash, Made Day 2, Snapshot)
  • Re-entry tilt guard modal before confirming re-entry
  • "Cash" → input payout + finish position → end session

Trip mode:

  • Today's schedule (card-style, like Manila JSX)
  • Filter by venue
  • Status badges and action buttons per tournament
  • Budget bar: spent vs. planned, current P&L
  • Swipe between days

Session entry captures:

  • Venue, date, game type, buyin, currency
  • Start time (auto), end time (when you tap "done")
  • Late registration flag + entry level/blinds (optional)
  • Re-entries (tracked individually with timestamp, level, blinds, cost)
  • Payout, finish position, field size (on completion)
  • Notes
  • Snapshots (photos of tournament screen + your stack)

Desktop Dashboard (≥ 768px)

The analysis and planning tool.

Sidebar navigation: Home, Trips, Stats, Venues, Settings

Home view:

  • Session history table with powerful filtering: date range, venue(s), buyin range, game type, result
  • Summary stats bar: total sessions, P&L, ROI, avg buyin, avg cash, ITM%
  • Filters update everything live

Trip view:

  • Trip list → drill into trip → full schedule explorer
  • Side panel: budget tracker (planned vs. actual), session results, daily breakdown
  • Research tab: start/refine AI research, review proposed schedules, approve

Stats view:

  • P&L over time (line chart)
  • By venue (bar chart)
  • By game type, by buyin range
  • Home vs. Trips breakdown
  • Monthly/quarterly/yearly views
  • Late reg vs. on-time performance comparison
  • Stack trajectory charts (from snapshots)
  • Re-entry analysis (ROI with vs. without re-entries)
  • All filterable with same filter system

Venues view:

  • Manage venues (public/private)
  • Set up venue profiles (day-of-week defaults, blind structures, starting stacks)

Settings view:

  • Profile (email, display name, preferred currency)
  • AI configuration (provider, API key, research model, snapshot model)
  • Model picker with quick picks + fuzzy search

Auth & Multi-tenancy

Current (personal tool)

  • Email + password registration (bcrypt)
  • JWT access token (15 min) + refresh token (30 days, httpOnly cookie)
  • All queries scoped by user_id
  • No email verification (single user)

Data Visibility Rules

Data Visibility
Global events Everyone
Location schedules Users who have run research (give-to-get)
Public venues Everyone
Private venues Creator only (redacted as "Private Venue" in any shared context)
Trips Owner only
Sessions & results Owner only
Budgets Owner only
Snapshots Owner only
Venue profiles Owner only
Research jobs Owner only (approved venues/schedules become shared)

Tech Stack

Backend

  • Go (latest stable)
  • chi — lightweight router
  • pgx — Postgres driver
  • sqlc — type-safe SQL → Go codegen
  • goose — migrations
  • river — Postgres-backed job queue (no Redis)
  • golang-jwt/jwt/v5 — JWT
  • golang.org/x/crypto/bcrypt — password hashing
  • embed — React frontend baked into binary

Frontend

  • React + TypeScript (Vite)
  • Tailwind CSS (Catppuccin Mocha palette)
  • Framer Motion (animations)
  • Recharts (stats/charts)
  • Workbox (PWA/service worker)
  • Zustand or Jotai (lightweight state)

Infrastructure

  • Postgres (10.5.0.109, database: pokertrip)
  • SearXNG (self-hosted LXC on Proxmox)
  • Nginx Proxy Manager (existing, SSL termination)

Deployment

Single Go binary embeds the built React frontend. One process, one port.

poker.georgsen.dk
  → Nginx Proxy Manager (SSL)
    → Go binary :8080
      ├── /           → embedded React SPA
      ├── /api/v1/*   → API
      └── /api/v1/events/stream → SSE

Environment config via .env file (gitignored). See .env.example for template.

Dev experience:

  • make dev — Go hot reload (Air) + Vite dev server with proxy
  • make build — builds React, embeds into Go binary
  • make migrate — runs Postgres migrations (goose)

Project Structure

pokertrip/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── api/
│   │   ├── auth.go
│   │   ├── sessions.go
│   │   ├── trips.go
│   │   ├── venues.go
│   │   ├── research.go
│   │   ├── snapshots.go
│   │   └── middleware.go
│   ├── db/
│   │   ├── migrations/
│   │   ├── queries/
│   │   └── sqlc.yaml
│   ├── jobs/
│   │   ├── research.go
│   │   ├── scraper.go
│   │   └── snapshot_ocr.go
│   ├── ai/
│   │   ├── client.go
│   │   ├── tools.go
│   │   └── models.go
│   ├── storage/
│   │   └── local.go
│   └── config/
│       └── config.go
├── frontend/
│   ├── src/
│   │   ├── components/
│   │   │   ├── mobile/
│   │   │   ├── desktop/
│   │   │   └── shared/
│   │   ├── hooks/
│   │   ├── stores/
│   │   ├── api/
│   │   ├── App.tsx
│   │   └── main.tsx
│   ├── public/
│   │   ├── manifest.json
│   │   └── sw.js
│   ├── index.html
│   ├── vite.config.ts
│   ├── tailwind.config.ts
│   └── package.json
├── docs/
│   ├── manila-poker-tracker.jsx   ← reference JSX for Manila trip UI
│   ├── saas-considerations.md
│   └── 2026-03-18-pokertrip-design.md  ← this file
├── .env
├── .env.example
├── .gitignore
├── Makefile
├── Dockerfile
└── go.mod