Backend gestito dal progetto Supabase di proprietà di Pratix: Postgres + Auth + Storage + Realtime.
- RLS sempre attiva su ogni tabella con dati utente.
- Foreign key verso
auth.userssolo quando utili all'integrità — restano le policy RLS a decidere l'accesso. - Mai memorizzare ruoli su
profiles— tabellauser_rolesseparata + funzionehas_role()SECURITY DEFINER. - Mai modificare schemi riservati Supabase:
auth,storage,realtime,supabase_functions,vault, salvo policy Storage documentate e versionate in migration. - Validazioni con trigger, non con
CHECKimmutabili (rompono il restore quando dipendono danow()). - Mai hardcodare ruoli o admin lato client (localStorage, ecc.) — sempre verifica server-side.
- Relazioni tra tabelle utente sempre owner-composite: la FK include
(record_id, user_id)e punta a una chiave univoca(id, user_id).
(per il dettaglio aggiornato vedi src/integrations/supabase/types.ts — auto-generato)
profiles— anagrafica del professionista (P.IVA, CF, indirizzo, regime fiscale, IBAN, ecc.)user_roles— ruoli applicativi (separati daprofiles)clients— anagrafica clientiprincipals— committenti, cioè soggetti a cui viene emessa fatturaprincipal_clients— relazione molti-a-molti fra committenti e clienticounterparties— controparti del recupero crediticounterparty_subjects— soggetti interni a una controparte compostacases(Pratiche) — posizioni legali, con numero pratica numerico univococase_credit_transfers— storico cessioni credito fra clientiprice_books— prezzi annuali per committenteprice_items— voci di prezzo per compensi/onorari e rimborsi spesecase_activities— attività economiche da fatturare o fatturatecase_activity_hearings— date udienza collegate alle attività che le richiedonoactivity_attachments— metadati allegati su compensi e rimborsibilling_runs— selezioni fatturazione per committente e periodobilling_run_items— righe incluse, rinviate o escluse da una fatturazionebilling_exports— rendiconti Excel generatiimports— sessioni della Creazione guidata manualeimport_rows— righe di staging importinvoices— fatture emesse, con righe e dati FatturaPAinvoice_lines— righe fattura
Tutte le tabelle utente hanno user_id uuid not null e RLS che filtra per user_id = (select auth.uid()).
-- Esempio: clienti visibili solo al proprietario
alter table public.clients enable row level security;
create policy "clients_owner_select"
on public.clients for select
using ((select auth.uid()) = user_id);
create policy "clients_owner_insert"
on public.clients for insert
with check ((select auth.uid()) = user_id);
create policy "clients_owner_update"
on public.clients for update
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "clients_owner_delete"
on public.clients for delete
using ((select auth.uid()) = user_id);create type public.app_role as enum ('admin', 'user');
create table public.user_roles (
id uuid primary key default gen_random_uuid(),
user_id uuid not null,
role app_role not null,
unique (user_id, role)
);
alter table public.user_roles enable row level security;
create or replace function public.has_role(_user_id uuid, _role app_role)
returns boolean
language sql stable security definer
set search_path = public
as $$
select exists (
select 1 from public.user_roles
where user_id = _user_id and role = _role
)
$$;Le policy che richiedono ruolo usano poi public.has_role((select auth.uid()), 'admin').
- Usa Supabase CLI e i file in
supabase/migrations/. - Mai
ALTER DATABASE postgresnelle migrazioni: non è permesso. - Mai modificare manualmente
src/integrations/supabase/types.ts: è generato dall'API. - Prima di applicare una migration remota, usa
npm run db:push:dry-run. - Dopo modifiche a schema, RLS, trigger o funzioni, usa:
npm run db:advisors:securitynpm run db:advisors:performance
- Per eseguire tutti i controlli Supabase collegati, preferisci
npm run db:verify: lancia dry-run e advisor in sequenza, evitando più connessioni simultanee al pooler. - Dopo modifiche a bucket o policy Storage, usa almeno:
npm run db:push:dry-runnpm run db:advisors:security
- Rigenera i tipi Supabase solo con
npm run db:types, poi controlla il diff.
Non automatizzare supabase db push da GitHub Actions finché Pratix usa un solo
progetto Supabase. Il deploy del database resta manuale e intenzionale: il
workflow CI deve verificare, non cambiare la produzione.
Impostazioni operative desiderate nel dashboard Supabase:
- Registrazione aperta:
Allow new users to sign upattivo. - Conferma email attiva per le nuove registrazioni.
- Secure Email Change non attivo per scelta di prodotto attuale.
- Leaked Password Protection: attivare se il progetto Supabase passa a un piano Pro o superiore; sul piano gratuito l'advisor security può continuare a segnalarla anche se la UI non usa password.
- Policy password: non centrale nel percorso corrente, perché login e registrazione usano magic link via email. Non abbassare la lunghezza minima password per configurare il codice OTP: sono impostazioni diverse.
- Passkey:
passkey_enabled = truesul progetto hosted, RP IDpratix.vercel.app, RP display namePratix, RP originhttps://pratix.vercel.app. Restano una feature sperimentale Supabase: mantenere magic link come fallback operativo. Cambiare l'RP ID invalida tutte le passkey già registrate dagli utenti. - Anonymous sign-ins disattivati.
- Rate limits Auth rivisti e lasciati su valori prudenti per il piano gratuito.
- Site URL produzione:
https://pratix.vercel.app/. È il dominio autoritativo perché coincide con l'RP ID delle passkey: un Site URL diverso manda i magic link su un'origine dove la ceremony WebAuthn fallisce. - Dal 2026-07-28 l'integrazione Supabase del team Vercel usa
projectSelection: selecteded esclude Pratix. In precedenza riscrivevasite_urlal dominio project-scoped dopo ogni deploy production. Non riabilitare l'accesso dell'integrazione a Pratix: le variabili Vercel sono già configurate sul progetto enpm run publish:finishcontinua a bloccare la chiusura se il Site URL diverge. - Redirect URL produzione:
https://pratix.vercel.app/dashboardhttps://pratix.vercel.app/reimposta-password
- Il dominio project-scoped non è nella allow-list dei redirect: è protetto da
Vercel Authentication (302 verso
vercel.com/sso-api) e non è una destinazione valida. Le preview restano coperte dai pattern con wildcard. - Codice OTP email a 6 cifre. Nel Dashboard hosted va configurato come
Email OTP length del provider Email; nella Management API corrisponde a
mailer_otp_length, mentre nelsupabase/config.tomllocale corrisponde aauth.email.otp_length = 6. - Template email Supabase personalizzati in italiano per conferma account, magic
link e cambio email. Il template magic link include sia
{{ .ConfirmationURL }}sia{{ .Token }}, così l'utente può entrare con link oppure codice monouso.
La UI usa signInWithOtp sia per login sia per registrazione. La registrazione
passa full_name nei metadata Supabase, così il trigger profilo conserva il nome
del professionista quando l'utente viene creato. Dopo l'invio email, login e
registrazione accettano anche il codice monouso tramite verifyOtp con
type: "email".
Il template Magic Link è tracciato anche in supabase/config.toml e
supabase/templates/magic_link.html. Per il progetto hosted preferire la
verifica dal Dashboard Supabase o dalla Management API prima di usare
supabase config push, perché un push Auth può sovrascrivere anche impostazioni
non collegate al codice OTP.
Al 2026-05-16 il percorso free-tier scelto è:
- registrazione aperta;
- magic link via email come metodo principale;
- passkey come accesso rapido sperimentale: dal 2026-07-25
auth.passkeyeauth.webauthnsono configurati sul progetto hosted (via Management APIPATCH /v1/projects/<ref>/config/auth) e tracciati insupabase/config.toml. Lato client restano dietroVITE_ENABLE_PASSKEYS=true, da impostare solo sull'ambiente Production di Vercel: l'RP IDpratix.vercel.appesclude preview e localhost, dove il browser rifiuta la ceremony WebAuthn; /recupera-passworde/reimposta-passwordrestano come pagine informative no-password per vecchi link o bookmark;- MFA non implementata per scelta attuale;
- Secure Email Change non attivo per scelta attuale;
- Leaked Password Protection lasciata fuori perché richiede un piano a pagamento e perché la UI non espone più password.
- Bonifica password residue: il 2026-05-17 sono stati azzerati 3 hash password
legacy in
auth.users.encrypted_password, creati prima del passaggio passwordless. Il check successivo trova 0 utenti con password residua.
SQL usato per la verifica e la bonifica one-off:
select count(*) as users_with_password
from auth.users
where encrypted_password is not null;
update auth.users
set encrypted_password = null,
updated_at = now()
where encrypted_password is not null;
select count(*) as users_with_password
from auth.users
where encrypted_password is not null;Se un test di registrazione reale incontra over_email_send_rate_limit, non
alzare subito i limiti: attendi il reset della finestra Supabase e riprova con
una casella controllata. Evita tentativi ripetuti con indirizzi fittizi, perché
possono consumare il limite email.
Supabase Auth supporta CAPTCHA su registrazione e login. Pratix è predisposto per Cloudflare Turnstile, ma l'integrazione non è attiva nel percorso corrente: non creare widget Cloudflare e non configurare secret Turnstile finché non viene presa una nuova decisione.
Se in futuro si decide di attivarla:
- Crea il widget Turnstile per
pratix.vercel.appe per gli eventuali domini preview. - Inserisci la secret key in Supabase Auth → Bot and Abuse Protection.
- Aggiungi
VITE_TURNSTILE_SITE_KEYin Vercel Production e Preview. - Ridistribuisci: i form pubblici mostreranno la verifica solo quando la site key è presente.
Non salvare la secret key Turnstile in GitHub o nei file .env tracciati.
Il template Supabase Magic Link è personalizzato in italiano. Conferma account e cambio email devono restare allineati alla stessa linea editoriale quando vengono toccati. Custom SMTP e dominio email dedicato restano opzionali: non sono un blocco operativo nel percorso gratuito attuale.
Template italiani attesi:
- Confirm signup: oggetto
Conferma il tuo account Pratix; testo breve con link di conferma e nota "Se non hai richiesto tu la registrazione, ignora questa email." - Magic Link: oggetto
Accedi a Pratix; testo breve con link monouso e nota "Se non hai richiesto tu l'accesso, ignora questa email." - Change email: testo neutro con conferma del nuovo indirizzo, se la funzione è attiva nel dashboard Supabase.
Non inserire dati personali, importi o riferimenti a clienti nei template Auth.
Pratix usa un solo bucket privato:
pratix-documents
Il bucket ospita documenti, fatture, allegati delle attività, asset profilo ed export. I file devono sempre stare sotto una cartella proprietario con UUID utente come primo segmento:
<user_id>/invoices/<invoice_id>/<file>
<user_id>/cases/<case_id>/<file>
<user_id>/activities/<activity_id>/<file>
<user_id>/billing-exports/<billing_run_id>/<file>
<user_id>/imports/<import_id>/<file>
<user_id>/profile/<file>
<user_id>/exports/<file>
Le costanti e i builder path vivono in src/lib/storage-paths.ts. Non costruire
path Storage a mano dentro componenti o server functions, così resta più facile
mantenere le policy allineate.
Il bucket è privato, con limite file a 25 MB e MIME types comuni per PDF, XML, ZIP, CSV, Excel, OpenDocument, testo, immagini e documenti office. Se serve un formato nuovo, aggiungi prima il MIME type nella migration Storage e poi aggiorna questa guida.
Le policy su storage.objects sono owner-scoped:
selectsolo se il primo segmento del path è(select auth.uid())::text;insertsolo dentro la cartella dell'utente autenticato;updateconusingewith checksulla stessa cartella;deletesolo dentro la cartella dell'utente.
Per upload con upsert servono select, insert e update: non rimuovere una
di queste policy pensando che basti insert.
Non rendere pubblico il bucket. Per condivisioni temporanee usa URL firmati generati lato server, senza inserire nomi clienti, importi o dati personali nei log.
Per attivare realtime su una tabella:
ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;Lato client:
import { supabase } from "@/integrations/supabase/client";
const channel = supabase
.channel("messages")
.on("postgres_changes", { event: "*", schema: "public" }, (payload) => {
console.log(payload);
})
.subscribe();Le policy RLS continuano a valere anche sui messaggi realtime.
- Default 1000 righe per query Supabase: se "mancano dati", verifica il limit prima di cercare bug.
- Ogni modifica strutturale passa da migration versionata: non eseguirla solo in console.
supabase db advisors --linked --type securityper anomalie di sicurezza.supabase db advisors --linked --type performanceper anomalie di performance.- Linter pulito non garantisce sicurezza: review manuale delle policy obbligatoria.
Per Storage, gli advisors vanno accompagnati da un controllo manuale delle
policy su storage.objects: bucket privato, path owner-scoped, nessuna policy
to public e nessuna condizione basata su metadata modificabili dall'utente.
- Security —
Leaked Password Protection Disabled: warning accettato nel percorso gratuito corrente. La protezione richiede un piano Supabase a pagamento; la UI non espone password, ma l'advisor può restare visibile sul piano Free. - Performance — foreign key senza indice: gli avvisi su
case_status_history.user_ideinvoice_lines.user_idsono stati chiusi con la migration20260502173000_add_fk_supporting_indexes.sql. - Performance — indici unused: gli indici segnalati come non usati su
cases,invoiceseinvoice_linesnon vanno rimossi alla cieca. Rivalutarli solo dopo traffico reale, query osservate e conferma che non servano a filtri, join, ordinamenti o policy RLS.
Il piano gratuito non deve dipendere da Point-in-Time Recovery o Log Drains. Per Pratix il backup operativo è manuale:
- Esporta periodicamente un dump logico del database con strumenti Supabase CLI o Postgres.
- Conserva il dump fuori dal repository GitHub.
- Non salvare mai nel repo dump reali, dati clienti, fatture, email o chiavi.
- Quando possibile, prova il restore su ambiente locale o su un progetto temporaneo non produttivo.
- Per Storage, esporta periodicamente anche gli oggetti del bucket
pratix-documentscon strumenti Supabase/S3 compatibili, sempre fuori dal repository e preferibilmente in archivio cifrato.
Le migrations e supabase/schema.sql restano in GitHub; i dati reali no.
Se un export serve durante una migrazione, mettilo in una cartella ignorata o
fuori dal checkout e cancellalo appena il restore e la verifica sono conclusi.