3 schemi PostgreSQL per strutturare Supabase in modo pulito
Punti chiave: Separare un database Supabase PostgreSQL in tre schemi distinti (privat,collaborative,gamification) fin dall'inizio evita il caos man mano che il progetto cresce. Questo articolo copre il perché, il come, le politiche RLS, le convenzioni di denominazione e gli errori da non ripetere.
Quando avvii un progetto, la tentazione è enorme: crei le tue tabelle nello schema public di PostgreSQL e vai avanti. Una tabella qui, un'altra lì. In tre settimane, ti ritrovi con 40 tabelle mescolate senza alcuna logica, nomi incoerenti e una paura tremenda di toccare qualsiasi cosa.
Ho deciso molto presto nello sviluppo di TAMSIV di strutturare il database con tre schemi separati. Sei mesi e oltre 650 commit dopo, è probabilmente la migliore decisione architetturale che abbia mai preso. Ecco perché, e come farlo concretamente con Supabase.
Perché non mettere tutto nello schema public?
Lo schema public di PostgreSQL è quello predefinito. Supabase lo usa per le sue tabelle interne. Quando aggiungi le tue tabelle lì, mescoli i tuoi dati di business con l'infrastruttura. È come riporre i vestiti in cucina — funziona, ma è il caos.
I problemi concreti che ho identificato:
- Leggibilità: Con oltre 40 tabelle in un unico schema, trovare una tabella diventa un gioco di indovinelli.
- Sicurezza: Le politiche RLS (Row Level Security) diventano impossibili da ragionare quando tabelle di domini diversi coesistono.
- Evoluzione: Aggiungere un nuovo dominio funzionale (gamification, analytics) senza toccare l'esistente è impossibile se tutto è nello stesso calderone.
- Collaborazione: Quando un altro sviluppatore (o tu stesso tra 6 mesi) scopre il progetto, non sa da dove iniziare.
Come strutturare gli schemi di un'app Supabase?
Ecco i tre schemi di TAMSIV e cosa contengono:
Schema privat — i dati personali
Tutto ciò che appartiene a un utente e solo a lui:
privat.tasks— Attività personaliprivat.memos— Memo vocali e testualiprivat.calendar_events— Eventi del calendarioprivat.user_profiles— Profili utenteprivat.task_attachments/privat.memo_attachments— Allegati
Perché privat e non private? Perché private è una parola riservata in SQL. L'ho imparato a mie spese dopo una migrazione fallita che ha rotto l'intero schema. Il messaggio di errore non era nemmeno chiaro — ci sono voluti 45 minuti per capire che il nome dello schema era il problema.
Schema collaborative — i gruppi
Tutto ciò che riguarda il lavoro di squadra:
collaborative.groups— Gruppi gerarchici (fino a 6 livelli)collaborative.group_members— Membri e ruolicollaborative.group_tasks/collaborative.group_memos— Contenuto condivisocollaborative.checklists— Liste di controllo con convalida
Questo schema è il più complesso in termini di politiche RLS perché le regole di accesso dipendono dal ruolo dell'utente nel gruppo, dalla gerarchia dei gruppi e dalle autorizzazioni ereditate. Ho dettagliato la complessità dei gruppi gerarchici nell'articolo dedicato.
Schema gamification — l'engagement
Tutto ciò che rende l'app coinvolgente (nel buon senso):
gamification.user_stats— Punti, livello, streak attualegamification.user_badges— Badge sbloccabiligamification.points_history— Cronologia dettagliata dei puntigamification.daily_challenges— Sfide quotidianegamification.feed_activity— Flusso di attività sociale
Questo schema è stato aggiunto tre mesi dopo l'inizio del progetto. Grazie alla separazione, ho creato un nuovo schema senza toccare gli altri due. Zero rischio di rompere l'esistente. L'architettura di gamification è dettagliata nell'articolo sullo schema di gamification.
Quali vantaggi concreti in pratica?
Al di là della teoria, ecco cosa mi ha portato la separazione in schemi quotidianamente:
La leggibilità immediata
Quando faccio SELECT * FROM privat.tasks, so istantaneamente che si tratta di un dato personale. Quando vedo collaborative.group_members, so che è legato ai gruppi. Non c'è bisogno di documentazione aggiuntiva — lo schema È la documentazione.
Politiche RLS più semplici da ragionare
Le regole dello schema privat sono limpide: tu vedi solo i tuoi dati. Punto. La politica RLS sta in una riga:
CREATE POLICY "users_own_data" ON privat.tasks
USING (user_id = auth.uid());
Quelle dello schema collaborative sono più complesse: tu vedi i dati dei gruppi di cui sei membro, in base al tuo ruolo, con ereditarietà delle autorizzazioni parentali. Ma poiché sono isolate nel loro schema, la complessità non si riversa sul resto.
L'evoluzione senza rischi
Aggiungere la gamification non ha richiesto alcuna modifica agli schemi esistenti. Aggiungere l'agenda con partecipanti e filtri neanche. Ogni nuovo dominio funzionale è autonomo.
Come gestire le politiche RLS con più schemi?
Supabase utilizza Row Level Security (RLS) per proteggere l'accesso ai dati. Il principio è semplice: ogni tabella ha delle regole che determinano chi può leggere, scrivere, modificare, eliminare.
In pratica, è un labirinto. In totale, TAMSIV conta più di 30 politiche RLS, ognuna testata individualmente. Ecco come le organizzo:
- Schema
privat: Politiche semplici, basate suauth.uid() = user_id. - Schema
collaborative: Politiche complesse che verificano l'appartenenza al gruppo E il ruolo. Utilizzo di sottoquery sucollaborative.group_members. - Schema
gamification: Politiche miste — lettura pubblica per la classifica, scrittura solo tramite funzioni RPC server.
La trappola più comune: una politica RLS troppo permissiva che lascia trapelare dati tra utenti. Ne parlo nell'articolo sull'audit di sicurezza e il rate limiting.
Quali convenzioni di denominazione adottare?
Convenzioni rigorose fin dal primo giorno evitano ore di confusione in seguito. Ecco quelle di TAMSIV:
- Tabelle:
snake_caseal plurale (tasks,group_members,daily_challenges) - Colonne:
snake_case(user_id,created_at,is_completed) - Parametri RPC: Prefisso
p_(p_start_date,p_group_id,p_user_id) - Funzioni RPC: verbo_oggetto (
get_consolidated_feed,add_gamification_points)
Il prefisso p_ per i parametri RPC è critico. Senza di esso, il giorno in cui il tuo parametro user_id entra in conflitto con la colonna user_id in una query, PostgreSQL non solleva un errore — prende la colonna in priorità. Il risultato: una query che restituisce dati inattesi, un bug silenzioso e pericoloso.
Come migrare a più schemi se il progetto esiste già?
Se hai già tutto in public e vuoi migrare, ecco la strategia:
- Audit: Elenca tutte le tue tabelle e classificale per dominio funzionale.
- Crea gli schemi:
CREATE SCHEMA privat; CREATE SCHEMA collaborative; - Migra tabella per tabella:
ALTER TABLE public.tasks SET SCHEMA privat; - Aggiorna le politiche RLS: Sono legate alla tabella, quindi seguono lo spostamento.
- Aggiorna il codice frontend: Tutte le query Supabase devono ora specificare lo schema.
Attenzione: le foreign key, i trigger e le funzioni RPC devono essere aggiornati manualmente. È un lavoro impegnativo, ma il guadagno in manutenibilità è enorme. Se usi Supabase, leggi anche il mio articolo sulla riduzione dell'egress per ottimizzare i costi.
Quali errori evitare durante la strutturazione?
In 6 mesi di sviluppo da solo, ho identificato diverse trappole:
- Non usare parole riservate SQL come nomi di schema.
private,public,usersono tutti riservati. Usa alternative (privat,app_public,accounts). - Non dimenticare i permessi
GRANT. Creare uno schema non basta — devi esplicitamente dare i diritti di accesso ai ruoli Supabase (anon,authenticated,service_role). - Non fidarti mai dei file di migrazione locali. Il database reale è l'unica fonte di verità. I file di migrazione possono divergere dopo correzioni manuali.
- Testa ogni politica RLS individualmente. Oltre 30 politiche significano almeno 30 scenari di test. È metodico, noioso e assolutamente indispensabile.
Che impatto sulle prestazioni?
Buone notizie: gli schemi PostgreSQL non hanno alcun impatto sulle prestazioni delle query. Un SELECT FROM privat.tasks è esattamente veloce quanto un SELECT FROM public.tasks. Gli schemi sono un'organizzazione logica, non fisica.
Invece, la separazione facilita l'ottimizzazione. Puoi aggiungere indici specifici per dominio, configurare parametri di vacuum diversi per schema e monitorare le prestazioni per dominio funzionale.
Per il frontend, il refactoring clean architecture ha anche giocato un ruolo cruciale nella manutenibilità del codice che interagisce con il database.
FAQ
Quanti schemi bisogna creare per un'app classica?
Due o quattro sono sufficienti per la maggior parte dei progetti. Uno per i dati utente, uno per le funzionalità condivise/collaborative, eventualmente uno per l'analytics o la gamification. L'importante è separare per dominio funzionale, non per tipo di dato.
Supabase supporta bene gli schemi multipli?
Sì, ma bisogna configurare i permessi manualmente. Per impostazione predefinita, solo lo schema public è accessibile tramite l'API REST. Per esporre un altro schema, bisogna aggiungerlo nelle impostazioni del progetto Supabase e configurare i GRANT appropriati sui ruoli anon e authenticated.
Servono foreign key tra schemi?
Sì, ed è perfettamente supportato da PostgreSQL. Una tabella in collaborative può fare riferimento a una tabella in privat. I vincoli di foreign key funzionano tra schemi senza alcuna limitazione.
Come gestire le funzioni RPC che toccano più schemi?
Le funzioni RPC (come get_consolidated_feed) possono unire tabelle di schemi diversi senza problemi. La buona pratica è creare la funzione nello schema del dominio principale che serve, e usare nomi pienamente qualificati (privat.tasks) nelle query.
Si può tornare indietro dopo aver migrato a più schemi?
Tecnicamente sì, con ALTER TABLE SET SCHEMA public. Ma in pratica, una volta che il codice frontend fa riferimento agli schemi, il ritorno indietro è costoso. Ecco perché è meglio prendere questa decisione all'inizio del progetto.