Data Model — Overview
This is the observability layer for our data itself. Until now there was no single place to answer "what entities does Playroll actually have, and how do they connect?" — you had to read migrations or poke Supabase. This section is that single place.
It is written to be read two ways:
- As a PM / non-engineer — read this page top to bottom. Every domain has a plain-language card and a glossary line. You can ignore the column tables.
- As an engineer (zoinkwiz, Vincenzo, Alessio, Marco) — the per-domain pages give you the key columns and the relationships; the full reference gives you every column, type, and foreign key of all 45 tables.
This atlas describes the live public schema of the Playroll Supabase
project (the C++ recorder's DB). It is generated from the database itself, not
hand-invented. The canonical schema home is
playroll-cpp/supabase/migrations — if a migration lands that changes the
shape below, this section is what gets updated alongside it.
The contributor portal has its own separate database and its own
Data Model page; the only bridge between
the two is the contributor_status_projection table described in
Identity & Access.
How to read the maps
Every domain page draws a Mermaid entity-relationship diagram. Two symbols carry all the meaning:
- A box is a table (an entity). Its top line is the name; the lines under it are the columns worth knowing.
- A line is a foreign key — a hard pointer from one table to another. The
crow's-foot (the little fork
<) sits on the "many" side. SoELIGIBLE_GAMES ||--o{ LOBBIESreads "one eligible game can back many lobbies."
When a relationship is by value rather than a real foreign key (e.g. a
status string, or a JSON rule that names a tier), we say so in prose instead of
drawing a line — exactly like the quests model does for audience rules. A handful
of edges are drawn to playroll_users for readability but physically reference
auth.users (the two are 1:1 on the same UUID); the domain pages flag those.
One more caveat the diagrams can't show: row-level security is enabled on every table. A relationship being drawn doesn't mean a browser client can read across it — many tables are reachable only through an edge function or the service key. Each domain page has a short "access model" note; trust those over the diagram when you reason about what the app can actually read.
The whole ecosystem, at a glance
Six domains, plus a handful of supporting tables. The recording spine in the centre is the thing everything else decorates — a user records a session, the session produces uploads, and identity / catalog / lobby / growth all hang off that act.
The six domains
🟦 Identity & Access
Who a player is, and what they're allowed to do. playroll_users is the
hub of the entire database — almost everything points back to a user. Around it
sit Steam identity (allowlist, OpenID nonces, enrichment), two separate RBAC
systems (player roles for app users, CRM roles for staff), the contributor-portal
bridge, and the append-only consent log.
→ Identity & Access
🟩 Recording spine
The core act of the product: record gameplay, prove it, upload it. A
recording_session is the unit of payment and support. It produces s3_uploads
(one row per uploaded file — the validated MP4 + CSV + JSON, plus optional
_mic.aac / _transcript.json sidecars), references the hardware it ran on
(device_hw_snapshots), and is the input to the GPU pipeline_* processing
stages. This is the spine the quests model bolts onto.
→ Recording spine
🟨 Lobby & Catalog
Co-op recording, and the games we allow. A lobby is a party recording
together, bound to exactly one game. The catalog (eligible_games and its
store / runtime / engine satellites) defines which games are recordable and how
to capture them; user_game_library is each player's scanned Playnite library.
→ Lobby & Catalog
🟪 Growth & Network
How players bring in other players. The shadow_graph is the Steam
friend-of-user network that feeds the headhunter view; invite_codes gate
Phase-0 access; referral columns on playroll_users close the loop.
→ Growth & Network
⬜ Releases & Notifications
Getting new builds and messages to the right players. The app_releases_v2
- rollout tables are the staged-update brain (tracks, percentages, A/B buckets,
force-update overrides), audited per resolve call. The
notification_*tables are the in-app + email delivery system with per-user prefs and read state. → Releases & Notifications
⬜ Diagnostics (shared)
Telling us why a recording went wrong. nvenc_known_contenders /
nvenc_contender_user_links track which third-party apps fight Playroll for the
GPU encoder; bug_reports is the in-app report channel. These are documented on
the Recording spine and
Releases & Notifications pages next to what they
diagnose.
Vocabulary — one word, one meaning
The same words get reused loosely in chat. Here is what they mean in the data:
| Term | In the database it means… |
|---|---|
| User / player | A row in playroll_users, keyed by user_id (a Supabase auth UUID). The single most-referenced entity. |
| Session | A row in recording_sessions — one start-to-stop recording attempt, keyed by a rec-<epoch-ms> string. The unit of payment and support. |
| Validated set | The artifacts the upload gate requires for a session — MP4, CSV (_input.csv), and JSON (_meta.json). If local validation blocks, none of them are registered for upload (the gate is the all-or-nothing point, not the upload itself). Each present file then lands as its own row in s3_uploads. |
| Sidecars | Optional artifacts that never gate validation: the _mic.aac mic audio and the _transcript.json transcript. A session can therefore have up to ~5 artifacts, not 3 — and a missing sidecar is normal, not a failure. |
| Lobby / party | A row in lobbies — people recording together for one attempt, bound to a single game. The "group of N". |
| Eligible game | A row in eligible_games — a game we allow recording for, keyed by igdb_id / slug. The hard boundary an upload is checked against. |
| Tier | playroll_users.contributor_tier — the player's current progression level (a string defaulting to player, not yet a separate table). |
| Track | A release channel a build is published to — the closed set `stable |
| Transcript | The _transcript.json of a session — a first-class artifact with its own GPU-gated generation, player-skip/edit flow, and state columns. See Recording Spine → The transcript. |
| Contender | A third-party process seen holding an NVENC (GPU encoder) session while Playroll recorded — i.e. encoder competition. |
Full table index
All 45 tables, grouped by the domain that owns them. Click through to the domain page for the diagram and key columns, or jump to the reference for every column.
| Table | Domain | One-liner |
|---|---|---|
playroll_users | Identity | The hub. One row per player; 40 columns spanning Steam identity, enrichment, network value, and attribution. |
steam_id_allowlist | Identity | Pre-authorized Steam IDs. The login edge fn checks this before issuing a session. |
auth_user_steam_lookup | Identity | steam_id → auth user UUID index, trigger-maintained. |
steam_openid_nonces | Identity | Replay-protection nonces for Steam OpenID sign-in. |
player_role_catalog | Identity | Catalog of valid player roles, ranked. New roles = INSERT, no migration. |
player_roles | Identity | Role assignments for app users (a player may hold several). |
crm_user_roles | Identity | Staff RBAC for the CRM, keyed by email (verified by Cloudflare Access). |
permissions | Identity | Catalog of (resource, action) permission pairs. |
role_permissions | Identity | Role → permission bundle the CRM admin UI edits. |
device_auth_requests | Identity | Legacy device-code login handshake (0 rows; superseded by the steam-login redirect). |
upload_credentials | Identity | Legacy/unused per-MAC credential (0 rows, no writer). |
playroll_user_consents | Identity | Append-only consent log; 5-value consent_type (age/tos/privacy/eula/edc_gameplay). |
contributor_status_projection | Identity | App-side bridge written by redeem-activation-code (link status, no PII). |
recording_sessions | Spine | One recording attempt; canonical duration + uploadability status. |
s3_uploads | Spine | One row per uploaded file (validated MP4/CSV/JSON + optional mic/transcript sidecars). |
device_hw_snapshots | Spine | Per-(user, device) hardware inventory; new row on any HW change. |
pipeline_sessions | Spine | GPU-pipeline view of a session's S3 artifacts and processing stage. |
pipeline_runs | Spine | One row per pipeline execution (Prefect flow run) over a session. |
nvenc_known_contenders | Diagnostics | Registry of third-party apps seen competing for NVENC. |
nvenc_contender_user_links | Diagnostics | Which users saw each contender (distinct-user count). |
bug_reports | Diagnostics | In-app bug reports with HW/version context. |
lobbies | Lobby | A party recording together, bound to one game. |
lobby_participants | Lobby | Who is in each lobby + their session id. |
lobby_join_events | Lobby | Join/leave history (INSERT on join, left_at UPDATE on leave); trigger-driven. |
eligible_games | Catalog | Games we allow recording for (igdb_id / slug). |
game_store_ids | Catalog | Per-store IDs (steam/epic/…) for an eligible game. |
game_runtime_profiles | Catalog | Per-executable capture profile (engine, settings enforcement). |
engine_defaults | Catalog | Per-engine default capture settings spec. |
user_game_library | Catalog | Each player's scanned Playnite library. |
shadow_graph | Growth | Steam friends of registered users; the recruitment network. |
invite_codes | Growth | Fixed pool of Phase-0 invite codes. |
invite_code_consumptions | Growth | One row per (code, user) — a user redeems a given code once. |
redeem_attempt_log | Growth* | Rate-limiter log for the activation-code flow (EC-227), not invite codes. (*lives in the activation/contributor area despite the table list grouping.) |
app_releases_v2 | Releases | A published build (installer + core + UI versions, notes, hash). |
app_rollouts_v2 | Releases | A rollout rule: track, percentage, A/B bucket, audience. |
app_rollout_overrides_v2 | Releases | Per-user / per-install pin or force-update override. |
app_update_resolve_audit_v1 | Releases | One row per "what update do I get?" resolve call. |
app_update_resolve_rate_limit | Releases | Fixed-window rate-limit counters for the resolve edge fn. |
notification_event | Notifications | An authored notification (category, payload, audience filter). |
notification_deliveries | Notifications | Per-user-per-channel delivery attempt + status. |
notification_state | Notifications | Per-user read / dismissed / acted state for an event. |
notification_groups | Notifications | Named targeting cohorts (founders, ops, beta…). |
notification_group_members | Notifications | Membership of those cohorts. |
notification_user_prefs | Notifications | Per-user channel + category opt-in toggles. |
Beyond the 45 tables, 11 views project this data for VCs, support, and
dashboards (investor_daily_recording_kpis_vc_view, support_recording_sessions,
playroll_users_view, …). They are listed with the tables that back them on the
reference page. The dashboards that consume them live
under Dashboards → Supabase Views.