Skip to main content

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.
Source of truth

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. So ELIGIBLE_GAMES ||--o{ LOBBIES reads "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:

TermIn the database it means…
User / playerA row in playroll_users, keyed by user_id (a Supabase auth UUID). The single most-referenced entity.
SessionA row in recording_sessions — one start-to-stop recording attempt, keyed by a rec-<epoch-ms> string. The unit of payment and support.
Validated setThe 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.
SidecarsOptional 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 / partyA row in lobbies — people recording together for one attempt, bound to a single game. The "group of N".
Eligible gameA row in eligible_games — a game we allow recording for, keyed by igdb_id / slug. The hard boundary an upload is checked against.
Tierplayroll_users.contributor_tier — the player's current progression level (a string defaulting to player, not yet a separate table).
TrackA release channel a build is published to — the closed set `stable
TranscriptThe _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.
ContenderA 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.

TableDomainOne-liner
playroll_usersIdentityThe hub. One row per player; 40 columns spanning Steam identity, enrichment, network value, and attribution.
steam_id_allowlistIdentityPre-authorized Steam IDs. The login edge fn checks this before issuing a session.
auth_user_steam_lookupIdentitysteam_id → auth user UUID index, trigger-maintained.
steam_openid_noncesIdentityReplay-protection nonces for Steam OpenID sign-in.
player_role_catalogIdentityCatalog of valid player roles, ranked. New roles = INSERT, no migration.
player_rolesIdentityRole assignments for app users (a player may hold several).
crm_user_rolesIdentityStaff RBAC for the CRM, keyed by email (verified by Cloudflare Access).
permissionsIdentityCatalog of (resource, action) permission pairs.
role_permissionsIdentityRole → permission bundle the CRM admin UI edits.
device_auth_requestsIdentityLegacy device-code login handshake (0 rows; superseded by the steam-login redirect).
upload_credentialsIdentityLegacy/unused per-MAC credential (0 rows, no writer).
playroll_user_consentsIdentityAppend-only consent log; 5-value consent_type (age/tos/privacy/eula/edc_gameplay).
contributor_status_projectionIdentityApp-side bridge written by redeem-activation-code (link status, no PII).
recording_sessionsSpineOne recording attempt; canonical duration + uploadability status.
s3_uploadsSpineOne row per uploaded file (validated MP4/CSV/JSON + optional mic/transcript sidecars).
device_hw_snapshotsSpinePer-(user, device) hardware inventory; new row on any HW change.
pipeline_sessionsSpineGPU-pipeline view of a session's S3 artifacts and processing stage.
pipeline_runsSpineOne row per pipeline execution (Prefect flow run) over a session.
nvenc_known_contendersDiagnosticsRegistry of third-party apps seen competing for NVENC.
nvenc_contender_user_linksDiagnosticsWhich users saw each contender (distinct-user count).
bug_reportsDiagnosticsIn-app bug reports with HW/version context.
lobbiesLobbyA party recording together, bound to one game.
lobby_participantsLobbyWho is in each lobby + their session id.
lobby_join_eventsLobbyJoin/leave history (INSERT on join, left_at UPDATE on leave); trigger-driven.
eligible_gamesCatalogGames we allow recording for (igdb_id / slug).
game_store_idsCatalogPer-store IDs (steam/epic/…) for an eligible game.
game_runtime_profilesCatalogPer-executable capture profile (engine, settings enforcement).
engine_defaultsCatalogPer-engine default capture settings spec.
user_game_libraryCatalogEach player's scanned Playnite library.
shadow_graphGrowthSteam friends of registered users; the recruitment network.
invite_codesGrowthFixed pool of Phase-0 invite codes.
invite_code_consumptionsGrowthOne row per (code, user) — a user redeems a given code once.
redeem_attempt_logGrowth*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_v2ReleasesA published build (installer + core + UI versions, notes, hash).
app_rollouts_v2ReleasesA rollout rule: track, percentage, A/B bucket, audience.
app_rollout_overrides_v2ReleasesPer-user / per-install pin or force-update override.
app_update_resolve_audit_v1ReleasesOne row per "what update do I get?" resolve call.
app_update_resolve_rate_limitReleasesFixed-window rate-limit counters for the resolve edge fn.
notification_eventNotificationsAn authored notification (category, payload, audience filter).
notification_deliveriesNotificationsPer-user-per-channel delivery attempt + status.
notification_stateNotificationsPer-user read / dismissed / acted state for an event.
notification_groupsNotificationsNamed targeting cohorts (founders, ops, beta…).
notification_group_membersNotificationsMembership of those cohorts.
notification_user_prefsNotificationsPer-user channel + category opt-in toggles.
Views are documented too

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.