Identità e accesso
Riassunto in parole semplici. Questo dominio risponde a due domande: chi è
questo giocatore? e cosa gli è permesso fare? Tutto ruota attorno a un'unica
tabella, playroll_users — è l'hub dell'intero database, e quasi ogni altra
tabella punta a essa. Attorno a lei si collocano i meccanismi di accesso con
Steam, due sistemi di permessi separati (uno per i giocatori dell'app, uno per lo
staff), l'handshake di login dell'app desktop e un registro append-only di ciò a
cui ogni giocatore ha dato il consenso.
Mappa
playroll_users è in relazione 1:1 con auth.users (stesso UUID). Per
leggibilità gli archi qui sopra sono disegnati verso PLAYROLL_USERS, ma due di
essi fanno fisicamente riferimento a auth.users(id), non a playroll_users:
playroll_user_consents.user_id e contributor_status_projection.app_user_id.
player_roles.user_id, al contrario, ha davvero una FK verso playroll_users.
La distinzione conta solo quando scrivi join SQL o ragioni sui cascade delete.
playroll_users — l'hub
Una riga per giocatore, identificata da user_id (lo UUID di auth di
Supabase). Con 40 colonne è la tabella più larga e più referenziata dello schema.
Le colonne ricadono in gruppi naturali:
| Gruppo | Colonne | A cosa serve |
|---|---|---|
| Identity | display_name, avatar_url, steam_id, discord_id, discord_username, steam_username, steam_avatar_url | L'identità pubblica del giocatore tra Steam e Discord. |
| Steam enrichment | steam_hours, steam_games_owned, steam_level, steam_profile_public, steam_account_created, steam_account_age_days, steam_vac_banned, steam_vac_bans_count, steam_game_bans_count, steam_days_since_last_ban, steam_enriched_at | Recuperate dalla Steam Web API per qualificare l'account (anti-frode + segnali di valore). |
| Network | steam_friend_count, steam_friends_public, friends_fetched_at, network_value, network_whale_count | Alimenta la vista growth / headhunter. |
| Attribution | country, language, utm_source, utm_medium, utm_campaign, referrer, device_type | Da dove proviene il giocatore. |
| Referral & priority | referral_code, referred_by, referral_count, priority_score | Il ciclo di referral e la prioritizzazione dell'onboarding. |
| Program | contributor_tier, credits_earned, credits_claimed | Il tier attuale del giocatore e i totali del registro delle ricompense. |
contributor_tier è una stringaAl momento il tiering vive come una singola colonna di testo sull'utente (default
player). Il modello delle quest propone di promuoverla a una tabella dedicata
player_tier_state con le metriche che la guidano (reattività, ore validate,
completamento). Finché ciò non sarà realizzato, il tier è un valore, non una
tabella — vedi le note di design quests-data-model.
Alcune colonne sono mantenute da trigger — non scriverle direttamente:
steam_account_age_days è calcolata da steam_account_created dal trigger
playroll_users_fill_steam_age, e updated_at viene marcata a ogni
aggiornamento. user_id è una FK verso auth.users.id (ON DELETE CASCADE);
steam_id e discord_id sono UNIQUE.
La proiezione di lettura canonica è la vista playroll_users_view, che
effettua il join di playroll_users con auth.users. È una porzione snella di 6
colonne — user_id, steam_id, display_name (con alias steam_username),
avatar_url, email, discord_username — ordinata per created_at. Leggila
quando ti serve la email di auth insieme al profilo (nota: created_at è usata
per l'ordinamento ma non è una colonna proiettata).
Accesso con Steam
Tre piccole tabelle supportano il flusso di login Steam OpenID gestito dalla edge
function steam-login:
steam_id_allowlist— Steam ID pre-autorizzati. La login fn crea sempre prima la rigaauth.users+playroll_users(tramite il triggeron_auth_user_created), poi l'allowlist decide se emettere un token. Due sfumature importanti: (1) il gate è fail-open finché l'allowlist è vuota — tutti passano finché non viene inserita la prima riga; (2) una volta non vuota, unosteam_idassente mantiene la sua rigaplayroll_userscatturata ma non riceve alcuna sessione. Porta con sé unrole(FK versoplayer_role_catalog, defaultpioneer) e un flagenabled.auth_user_steam_lookup— un indicesteam_id → auth user UUID,UNIQUEsuuser_id, mantenuto dal triggerauth_user_steam_lookup_sync_trgsuauth.users. Esiste per sostituire una scansioneauth.admin.listUsers({perPage:1000}).find()nella edge function (M6) — è un puro indice di performance.steam_openid_nonces— nonce monouso (nonce,seen_at) che rendono il callback OpenID a prova di replay.
Quando viene creato un nuovo auth user, il trigger on_auth_user_created
(SECURITY DEFINER) fa due cose: inserisce la riga playroll_users e, se lo
steam_id è nell'allowlist, popola anche player_roles a partire dal ruolo
dell'allowlist (granted_by = 'on_auth_user_created:allowlist',
ON CONFLICT DO NOTHING). Il catalogo dei ruoli ordina
tester (50) < pioneer (100) < staff (200).
Due sistemi RBAC (non confonderli)
In questo schema ci sono due sistemi di permessi indipendenti. Sembrano simili ma servono popolazioni diverse:
| RBAC giocatori | RBAC staff | |
|---|---|---|
| Tabelle | player_roles + player_role_catalog | crm_user_roles + role_permissions + permissions |
| Soggetto | Giocatori dell'app (user_id UUID) | Staff EC (email) |
| Usato da | L'app / API Playroll | Il network-CRM (control.extrinsiccognition.com) |
| Verificato da | Supabase auth (JWT) | Cloudflare Access, poi il middleware Express |
| Forma | Un giocatore può avere molti ruoli; role è una FK nel catalogo, che ranka la gerarchia. Nuovi ruoli = una INSERT nel catalogo, nessuna migrazione. | crm_user_roles mappa una email a uno tra admin / cm / vc / sales (vc è definito ma non ancora popolato); role_permissions espande quel ruolo in un bundle di permissions (resource, action) dove action ∈ (read, write). La UI admin ACCESS & ROLES modifica role_permissions; il middleware authorize() la legge. |
Il collegamento da crm_user_roles.role a role_permissions.role è per
valore (una stringa di ruolo condivisa), non una foreign key di database — ed è
per questo che la mappa lo disegna come una relazione debole.
Il percorso authorize() dello staff si risolve attraverso due cache da 60
secondi, non una: ROLE_CACHE_TTL_MS (email → role) e PERMISSION_CACHE_TTL_MS
(role → permissions), concatenate da getUserPermissions. Esiste anche un bypass
di servizio X-Control-Review-Token con scope wiki:write per la review
automatizzata.
Login da dispositivo e credenziali di upload (entrambi legacy)
Nessuna delle due è sul percorso attuale. Sopravvivono nello schema ma oggi nessuno le scrive.
device_auth_requests era un handshake di login con device-code (pending →
completato, con tokens + uno snapshot user_profile). Il flusso Steam attuale non
lo usa più — steam-login dichiara "no polling, no device_code, no DB for auth
handshake"; ha 0 righe e solo una policy RLS deny-all, sostituito dal flusso
di redirect (playroll://auth/callback).
upload_credentials era una credenziale per-macchina che legava un MAC
hashato (mac_hash, la PK) a un user_id. Ha 0 righe e nessuno scrittore da
nessuna parte nel core; l'RLS sono due policy deny-all (solo service-key). Nota
che qui user_id è text con un vincolo UNIQUE — non una FK uuid verso
playroll_users.
Consenso
playroll_user_consents è un log append-only — una riga per accettazione,
mai aggiornata né cancellata (non esiste una colonna revoked). consent_type è
un enum chiuso a cinque valori:
consent_type | Cosa ha accettato l'utente |
|---|---|
age_confirmation | Conferma dell'età minima. |
tos | Termini di servizio. |
privacy | Informativa sulla privacy. |
eula | Contratto di licenza con l'utente finale. |
edc_gameplay | Consenso alla raccolta dei dati di gameplay (l'unico che gli utenti possono in seguito revocare). |
Ogni riga registra inoltre la version accettata, accepted_at, e i valori di
country, ip_address, user_agent al momento, più un blob JSON metadata.
Due edge function con service-role scrivono qui:
consents-acceptappende un'accettazione.consents-revokeappende una revoca — una nuova riga conmetadata.action = 'withdrawal'(nel pilot: soloedc_gameplayè revocabile).
Poiché è append-only, lo stato effettivo è "ultima accettazione vs ultima revoca"
per (user, consent_type). Solo il documento privacy è condiviso con la
tabella consents del contributor-portal; la edge function consents-status
aggrega entrambi i log attraverso i due database per quell'unico tipo. Vedi
Consent Versioning per il ciclo di vita
completo.
Il ponte verso il contributor-portal
contributor_status_projection è l'unica tabella che attraversa il confine
verso il mondo del contributor portal — ma fai attenzione alla direzione. Il
portale possiede la tabella sorgente (contributor_app_links); questa
proiezione è scritta lato app dalla edge function
redeem-activation-code (service role), che la fa upsert con link_status
hard-coded a 'verified' (l'unico valore consentito della colonna). L'app
Playroll legge la propria riga.
È deliberatamente minimale — app_user_id (uuid, PK, FK → auth.users.id
ON DELETE CASCADE), contributor_id (uuid), source_link_id (uuid), steam_id,
linked_at, e i timestamp di sync, tutti NOT NULL tranne steam_id — e non
deve mai contenere evidenze di documenti legali, dati IBAN o qualsiasi PII del
portale. Il design completo del collegamento d'identità vive nella pagina
Identity Link del contributor-portal.
Modello di accesso — l'RLS è attiva ovunque
La row-level security è abilitata su tutte le 13 tabelle di questo dominio, quindi la mappa qui sopra mostra relazioni logiche, non ciò che un client browser può leggere. La postura si divide in tre direzioni:
- Propria riga leggibile dall'utente:
playroll_users(propria riga R/W),playroll_user_consents(propria SELECT),contributor_status_projection(propria SELECT). Più i cataloghi semi-pubbliciplayer_role_catalog,steam_openid_nonces,auth_user_steam_lookup. - Service-role / deny-all (nessun accesso client — raggiungibili solo tramite
edge function o la service key):
crm_user_roles,device_auth_requests,player_roles,steam_id_allowlist,permissions,role_permissions,upload_credentials.
Quindi quando la mappa implica "l'app legge questa tabella", assumi che passi attraverso una edge function a meno che la tabella non sia nell'elenco delle proprie righe qui sopra.
Vedi anche: riferimento completo delle colonne per queste tabelle.