Tutti i playbookNext.js + Supabase

Aggiungere una tabella con RLS

Salvare nuovi dati di proprietà dell'utente in Supabase con la row level security, così ogni utente legge e modifica solo le proprie righe.

Quando usarlo

Il prodotto ha bisogno di dati propri che appartengono a un utente, per esempio note, progetti o elementi salvati.

Regole da rispettare

  • Le migration sono append-only: crea un nuovo file in supabase/migrations/, non modificarne mai una già applicata.
  • La RLS è il confine di autorizzazione. Ogni nuova tabella in public abilita la row level security nella stessa migration che la crea.
  • Il proprietario arriva dalla sessione (auth.uid()), mai dal corpo della richiesta.
  • Il codice server usa il client per richiesta di src/lib/supabase/server.ts, che agisce come l'utente autenticato. SUPABASE_SERVICE_ROLE_KEY scavalca la RLS: non usarla per i dati degli utenti e non importarla mai nel codice client.

Passi

  1. Crea supabase/migrations/<timestamp>_<nome>.sql con un timestamp successivo all'ultima migration. Per una tabella notes:

    create table public.notes (
      id uuid primary key default gen_random_uuid(),
      owner_id uuid not null default auth.uid() references auth.users(id) on delete cascade,
      body text not null check (char_length(body) between 1 and 2000),
      created_at timestamptz not null default now()
    );
    
    create index notes_owner_id_idx on public.notes (owner_id);
    
    alter table public.notes enable row level security;
    
    create policy "notes_select_own" on public.notes for select
      to authenticated using ((select auth.uid()) = owner_id);
    create policy "notes_insert_own" on public.notes for insert
      to authenticated with check ((select auth.uid()) = owner_id);
    create policy "notes_update_own" on public.notes for update
      to authenticated using ((select auth.uid()) = owner_id)
      with check ((select auth.uid()) = owner_id);
    create policy "notes_delete_own" on public.notes for delete
      to authenticated using ((select auth.uid()) = owner_id);
    
    revoke all on public.notes from anon;
    
  2. Scrivi solo le policy che servono al prodotto: una tabella che gli utenti leggono ma non modificano ha solo una policy select. Scrivi (select auth.uid()) come in profiles, così Postgres lo valuta una volta per query e non una volta per riga.

  3. Se gli utenti non devono modificare alcune colonne, segui profiles: revoke update sulla tabella, poi grant update (<colonne>) solo su quelle modificabili.

  4. Leggi e scrivi da un Server Component o da un Route Handler con createClient() di src/lib/supabase/server.ts. Come in src/app/api/me/route.ts, chiama prima auth.getUser() e rispondi 401 se l'utente manca. Non filtrare su un owner_id preso dalla richiesta: la RLS limita già le righe.

  5. Applica la migration (supabase db push oppure l'editor SQL del progetto) e verifica l'isolamento con due account: l'utente A crea una riga, l'utente B non deve vederla, modificarla o cancellarla, e una richiesta anonima non riceve righe.

  6. Metti la validazione dell'input in un modulo puro sotto src/lib/ e testala in test/ con Vitest; i test non usano la rete.

  7. Descrivi la tabella e le sue regole in DATABASE.md.

Completato quando

  • i comandi di verifica passano;
  • la nuova tabella ha la RLS abilitata e una policy per ogni operazione consentita;
  • la verifica con due account del passo 5 è riuscita;
  • DATABASE.md descrive la tabella.