Passa al contenuto principale

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

Target FK in questo diagramma

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:

GruppoColonneA cosa serve
Identitydisplay_name, avatar_url, steam_id, discord_id, discord_username, steam_username, steam_avatar_urlL'identità pubblica del giocatore tra Steam e Discord.
Steam enrichmentsteam_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_atRecuperate dalla Steam Web API per qualificare l'account (anti-frode + segnali di valore).
Networksteam_friend_count, steam_friends_public, friends_fetched_at, network_value, network_whale_countAlimenta la vista growth / headhunter.
Attributioncountry, language, utm_source, utm_medium, utm_campaign, referrer, device_typeDa dove proviene il giocatore.
Referral & priorityreferral_code, referred_by, referral_count, priority_scoreIl ciclo di referral e la prioritizzazione dell'onboarding.
Programcontributor_tier, credits_earned, credits_claimedIl tier attuale del giocatore e i totali del registro delle ricompense.
Oggi contributor_tier è una stringa

Al 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 riga auth.users + playroll_users (tramite il trigger on_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, uno steam_id assente mantiene la sua riga playroll_users catturata ma non riceve alcuna sessione. Porta con sé un role (FK verso player_role_catalog, default pioneer) e un flag enabled.
  • auth_user_steam_lookup — un indice steam_id → auth user UUID, UNIQUE su user_id, mantenuto dal trigger auth_user_steam_lookup_sync_trg su auth.users. Esiste per sostituire una scansione auth.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 giocatoriRBAC staff
Tabelleplayer_roles + player_role_catalogcrm_user_roles + role_permissions + permissions
SoggettoGiocatori dell'app (user_id UUID)Staff EC (email)
Usato daL'app / API PlayrollIl network-CRM (control.extrinsiccognition.com)
Verificato daSupabase auth (JWT)Cloudflare Access, poi il middleware Express
FormaUn 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)

Queste due tabelle sono superate — 0 righe, RLS deny-all

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 UNIQUEnon 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_typeCosa ha accettato l'utente
age_confirmationConferma dell'età minima.
tosTermini di servizio.
privacyInformativa sulla privacy.
eulaContratto di licenza con l'utente finale.
edc_gameplayConsenso 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-accept appende un'accettazione.
  • consents-revoke appende una revoca — una nuova riga con metadata.action = 'withdrawal' (nel pilot: solo edc_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-pubblici player_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.