Backups
"Database error deleting user" su Supabase, e cosa porta con sé
"Database error deleting user" su Supabase: il tuo database ha rifiutato la cancellazione. La causa è nei log, e la soluzione decide cos'altro sparisce.

In breve
- "Database error deleting user" su Supabase vuol dire che la cancellazione è arrivata al tuo database ed è stata rifiutata, quindi non è stato tolto niente. La riga che dice la causa sta nei tuoi log di Postgres, e la nota di troubleshooting di Supabase ti manda proprio lì.
- Quando quella riga dice che una foreign key è violata, una riga di una delle tue tabelle contiene ancora l'id dell'utente. Postgres non toglie una persona finché qualcosa punta ancora a lei, a meno che tu non gli abbia detto cosa fare di quelle righe.
- La prima risposta che trovi è on delete cascade. Cancella ogni riga che punta all'utente, e ogni riga che punta a quelle, a ogni cancellazione da quel momento in poi. Decidi tabella per tabella, e fai una copia prima di cambiare una regola.
Apri Authentication → Users nella dashboard di Supabase ed elimini un account. Magari la persona ti ha chiesto di toglierla, magari è un account di prova creato la settimana scorsa. La dashboard risponde:
Failed to delete user: Database error deleting user
L'account è ancora nella lista. L'app che hai fatto con Lovable, Bolt, v0, Cursor, Claude Code o Replit ha funzionato per tutto il tempo, e la frase non nomina né una tabella né un motivo.
La prima risposta che trovi è una riga di SQL: aggiungi on delete cascade a
quello che blocca. Funziona. Decide anche, per ogni account cancellato da lì in
poi, quali altre righe se ne vanno con lui, e nessuno te lo chiede più. Quindi
leggi prima l'errore vero, e poi prendi quella decisione tabella per tabella.
Che cosa vuol dire "Database error deleting user" su Supabase?
Il tuo database ha rifiutato la cancellazione e Supabase ha annullato tutto, quindi l'account è esattamente com'era.
Eliminare un utente è un compito di Supabase Auth, il servizio dietro il login
della tua app. Nel codice sorgente di Auth la cancellazione avviene dentro una
sola transazione, un gruppo di modifiche che o passano tutte o nessuna, e il suo
passaggio centrale toglie la riga della persona da auth.users. Se qualcosa nel
tuo database si oppone, la transazione viene annullata e Auth risponde con questa
sola frase. Supabase la elenca come HTTP 500 con il codice unexpected_failure,
accanto alla sua gemella della registrazione,
"Database error saving new user".
Pensa al tuo database come a un circolo. auth.users è il registro dei soci, e
ogni socio ha un numero: il suo id utente. Gli altri tuoi libri copiano quel
numero sulle loro righe, che sia un profilo, un ordine, un post o un commento.
Cancellare qualcuno dal registro richiede una riga. Però ogni altro libro porta
ancora il numero, e finché nessuno ha scritto cosa fare di quelle righe, il
segretario del circolo si rifiuta.
Dov'è l'errore vero?
Nei log di Postgres del tuo progetto, nel momento in cui la cancellazione è fallita.
La nota di troubleshooting di Supabase sugli errori nella gestione degli utenti indica la strada. Prova la cancellazione una volta e segnati l'ora. Poi, nella dashboard:
- Apri Logs e leggi i log di Auth a quell'ora. La voce è il resoconto che Auth stesso ha registrato della cancellazione fallita.
- Apri i log di Postgres alla stessa ora. Quella riga è l'errore vero, e nomina una tabella, una regola o un permesso.
La stessa nota elenca tre cause, e la riga del log ti dice quale hai:
- un trigger sulla tabella
auth.usersche fallisce - un vincolo non rispettato, che in una cancellazione vuol dire una foreign key
di un'altra tabella che punta a
auth.users - Prisma, che secondo la nota può rompere i permessi su
auth.users
Una tabella che punta ancora all'utente
Se la riga di Postgres dice violates foreign key constraint, una riga di una
delle tue tabelle contiene ancora l'id di questo utente, e Postgres non toglie
una persona a cui qualcosa punta ancora.
La nota di Supabase stampa la forma che prende:
ERROR: 23503: update or delete on table "users" violates foreign key constraint "profiles_id_fkey" on table "profiles"
DETAIL: Key (id)=(7428a53c-75b7-4531-9ae9-1567d9c4ac0a) is still referenced from table "profiles".
Una foreign key è una regola su una colonna che dice che ogni valore al suo
interno deve esistere in un'altra tabella. Qui la regola si chiama
profiles_id_fkey, sta sulla tua tabella profiles, e la riga Key è l'id
dell'utente che hai provato a eliminare. Nel circolo, è una riga del libro dei
profili con il numero del socio, e il segretario che si rifiuta di cancellarlo
finché lei è lì.
La stessa regola rifiuta anche dall'altro lato. Una nuova riga che nomina un
utente che non esiste torna indietro come
insert or update on table … violates foreign key constraint, e
quel messaggio ha un articolo tutto suo.
Il rifiuto è il comportamento predefinito. Una foreign key creata senza niente
dopo references auth.users riceve il comportamento che Postgres chiama
no action, e la guida di Supabase sulle cancellazioni a cascata lo dice
chiaramente: NO ACTION è il comportamento predefinito se non specifichi nulla.
Una tabella che il tuo builder ha creato così funziona benissimo fino alla prima
volta che qualcuno viene eliminato.
Per vedere ogni tabella che punta ai tuoi utenti, e cosa fa ognuna in una cancellazione, esegui questo nel SQL Editor:
select conrelid::regclass as table_name,
conname as rule,
case confdeltype
when 'c' then 'cascade'
when 'n' then 'set null'
when 'd' then 'set default'
when 'r' then 'restrict'
else 'no action'
end as on_delete
from pg_constraint
where contype = 'f'
and confrelid = 'auth.users'::regclass
and connamespace <> 'auth'::regnamespace;
Legge solo il catalogo di Postgres e non cambia niente. L'ultima riga lascia
fuori le tabelle di Supabase nello schema auth. Ogni riga che dice no action
o restrict è una tabella che rifiuterà la cancellazione ogni volta che contiene
una riga di quell'utente.
Che cosa cancella on delete cascade?
Ogni riga che punta all'utente, poi ogni riga che punta a quelle, a ogni cancellazione da quel momento in poi.
È la soluzione che ti danno per prima, e la guida di Supabase alla gestione degli
utenti la usa per una tabella dei profili: dice di specificare
on delete cascade nel riferimento. Per un profilo è giusto. Un profilo descrive
la persona e nient'altro, quindi deve andarsene quando se ne va lei.
I guai cominciano quando le stesse parole finiscono su ogni tabella. La guida di Supabase sulla cascata descrive il comportamento senza giri di parole: quando una riga viene cancellata dalla tabella padre, vengono cancellate anche tutte le righe collegate nelle tabelle figlie. E continua a scendere. Nell'esempio stesso della guida, una sola cancellazione nella tabella in alto svuota le due tabelle sotto.
Nel circolo, una cascata dice al segretario di strappare ogni riga con il numero del socio, e poi ogni riga di qualunque altro libro che puntava a una riga strappata. Immagina un utente che ha scritto dei post, con i commenti di altre persone che puntano a quei post. Con la cascata su entrambi, eliminare quel solo utente toglie i suoi post e i commenti di tutti gli altri sotto.
Niente ti dice cosa se n'è andato con lui. Auth risponde a una cancellazione riuscita con una risposta vuota, la dashboard mostra un account in meno, e le righe sparite nello stesso istante mancano la prossima volta che qualcuno le cerca.
Che cosa tiene ogni tabella
Decidi tabella per tabella, perché ognuna conserva qualcosa di diverso sulla persona che se n'è andata.
| Che cosa contiene la tabella | on delete | Quando l'utente viene eliminato |
|---|---|---|
| Solo la persona: un profilo, le sue impostazioni | cascade | Le righe se ne vanno con lui |
| Cose che servono ancora ad altri: un commento nel thread di un altro, un pagamento | set null | Le righe restano, con la colonna del proprietario vuota |
| Tutto ciò di cui vuoi occuparti tu per primo | no action (il predefinito) | La cancellazione è rifiutata finché restano |
set null ha due condizioni. La colonna deve accettare valori vuoti, altrimenti
la cancellazione fallisce di nuovo con un messaggio tutto suo,
null value in column … violates not-null constraint. E una policy scritta come
user_id = auth.uid() non mostrerà mai più quella riga a nessuno, perché in SQL
niente è uguale a un valore vuoto. La riga resta, e la tua app smette di
mostrarla a chiunque.
Cambiare una regola vuol dire toglierla e rimetterla con il nuovo comportamento, come consiglia la nota di Supabase:
alter table public.profiles
drop constraint profiles_id_fkey,
add constraint profiles_id_fkey
foreign key (id) references auth.users (id) on delete cascade;
Usa il nome della regola che la query sopra ha stampato, con la tua tabella e la tua colonna.
C'è un'altra strada, e tiene tutte le righe. Supabase Auth può fare una
cancellazione morbida di un account: auth.admin.deleteUser() accetta
un'opzione shouldSoftDelete, e con questa Auth tiene la riga in auth.users,
la segna come cancellata, rimescola email e telefono, svuota password e
metadati, e chiude le sessioni. Non è stato tolto niente, quindi non scatta
nessuna foreign key. Va eseguita da un server con la tua chiave segreta, mai dal
browser.
Un trigger che non arriva alla tua tabella
Se la riga di Postgres dice permission denied for table, durante la
cancellazione è partito un trigger su auth.users che ha provato a toccare una
tabella in cui non ha il permesso di entrare.
Il ruolo con cui lavora Supabase Auth, supabase_auth_admin, ha solo i permessi
che servono al suo lavoro. La nota di Supabase lo dice così: quando un trigger
eseguito da supabase_auth_admin agisce fuori dallo schema auth, causa un errore
di permessi. Un trigger che fa pulizia dopo un account eliminato, per esempio
scrivendo in una tua tabella di log, fallisce proprio così, a meno che la sua
funzione non sia stata creata con security definer.
La riparazione è la stessa della registrazione: una funzione security definer,
creata dal ruolo postgres, che fa una sola cosa ben delimitata.
La versione di questo errore per la registrazione
la spiega riga per riga, compreso il search path vuoto che la tiene al sicuro.
Se la tua app usa Prisma, leggi prima la terza causa nella nota di Supabase,
perché nomina Prisma come uno dei modi in cui i permessi su auth.users si
rompono.
Che fine fanno i file che ha caricato?
Su un progetto attuale la cancellazione va a buon fine, e i file restano dove sono.
La guida di Supabase alla gestione degli utenti avverte ancora che non puoi
eliminare un utente proprietario di oggetti in Supabase Storage. Era vero finché
storage.objects, la tabella in cui Storage tiene una riga per ogni file, aveva
una foreign key chiamata objects_owner_fkey verso auth.users. Storage ha poi
tolto quella chiave nelle sue migrazioni di schema
(0017-drop-owner-foreign-key.sql), e la sua guida sulla proprietà dei file dice
ora che il proprietario sta in una colonna owner_id. Se la query più sopra
elenca storage.objects con objects_owner_fkey, il tuo progetto ha ancora la
vecchia regola e l'avvertimento della guida vale per te.
Altrimenti ogni file che la persona ha caricato resta nel suo bucket dopo che lei
se n'è andata. Nel circolo, i cappotti restano appesi ai ganci con un gettone che
non ha più nessuno. Una policy scritta come owner_id = auth.uid()::text non li
troverà mai più, e un file in un bucket pubblico viene ancora servito a chiunque
abbia il suo link. Se un estraneo può elencare i file di un tuo bucket è uno dei
controlli della scansione gratuita di Reeve.
Per toglierli passa da Storage stesso, dalla pagina Storage della dashboard o con l'API di Storage. Una cancellazione SQL diretta sulle sue tabelle viene rifiutata, e Storage dice perché: "Direct deletion from storage tables is not allowed. Use the Storage API instead." Che cosa tiene di quei file una copia del database, e che cosa lascia fuori, lo trovi in fare il backup di Supabase Storage.
Fai una copia prima di cambiare una foreign key
Postgres non ha un cestino, e una cascata toglie righe da tabelle che non stavi guardando.
Quello che una cascata o un set null cambia è definitivo appena la
cancellazione riesce, e l'unica strada per tornare indietro è una copia fatta
prima. Il piano gratuito di Supabase non tiene nessun backup. Pro tiene 7 giorni
di copie giornaliere, Team 14 ed Enterprise fino a 30, e ognuna vive dentro lo
stesso progetto Supabase.
Copiare il database da solo
richiede pochi comandi, e
una copia che comprenda i tuoi utenti ha
bisogno del terzo dei tre comandi che Supabase pubblica.
Poi prova le nuove regole su un account di prova prima che su uno vero. Crea un account, dagli una riga in ogni tabella che la query ha elencato, eliminalo e guarda cosa resta in ogni tabella.
Se preferisci che quella copia esista già prima che qualcuno prema elimina, a questo servono i backup Supabase di Reeve.
Una strada per tornare indietro quando una cancellazione porta via troppo
Una cascata non segnala niente di ciò che ha tolto. Lo scopri quando qualcuno chiede dove sono finiti i suoi commenti.
Reeve Care tiene una copia del tuo database Supabase di prima della cancellazione:
- una copia cifrata secondo una pianificazione, conservata sullo storage di Reeve nell'UE, fuori dal tuo account Supabase
- ogni copia verificata prima di contare, contando le righe di ogni tabella
- un ripristino in un clic che salva quello che c'è adesso prima di sostituirlo
- anche i tuoi file caricati, appena colleghi una credenziale di Storage
- tutto quello che fa Reeve Monitor: tutti e nove i controlli della tua app online ogni ora, e un controllo di raggiungibilità ogni 60 secondi
Che cosa fare oggi
Cosa fare
- Prova la cancellazione una volta, segnati l'ora e leggi la riga del log di Postgres di quel momento prima di cambiare qualsiasi cosa.
- Se nomina una foreign key, esegui la query sopra per elencare ogni tabella che punta ai tuoi utenti e cosa fa ognuna in una cancellazione.
- Decidi tabella per tabella:
cascadeper le righe che descrivono solo la persona,set nullper quelle che servono ancora ad altri, il predefinito per tutto ciò che vuoi togliere a mano. - Fai una copia del database prima di cambiare una qualsiasi di queste regole, e prova le nuove regole prima su un account di prova.
- Togli anche i file della persona passando da Storage. Su un progetto attuale l'account se ne va e i file restano.
Se delle righe se ne sono già andate oltre quello che volevi, che cosa puoi recuperare dopo una cancellazione spiega dove guardare, in ordine.
FAQ
Che cosa vuol dire "Database error deleting user" su Supabase?
Vuol dire che Supabase Auth ha provato a togliere la riga della persona da auth.users, qualcosa nel tuo database si è rifiutato, e tutta la cancellazione è stata annullata. L'account è esattamente com'era. Il messaggio non nomina nessuna tabella, e la riga che la nomina sta nei log di Postgres del tuo progetto nello stesso istante.
Devo aggiungere on delete cascade per risolvere?
Su una tabella che descrive solo la persona, come una tabella dei profili, sì, e la guida di Supabase fa esattamente così. Su una tabella con cose che servono ancora ad altri, come commenti o ordini, decidi prima. Cascade cancella ogni riga che punta all'utente e ogni riga che punta a quelle, a ogni cancellazione da lì in avanti, e niente ti dice cosa ha tolto.
Posso eliminare un utente e tenere le sue righe?
Sì, in due modi. Imposta la foreign key su on delete set null, e le righe restano con la colonna del proprietario vuota, il che richiede che quella colonna accetti valori vuoti. Oppure chiama auth.admin.deleteUser() da un server con l'opzione shouldSoftDelete: Supabase Auth tiene la riga, la segna come cancellata, rimescola email e telefono e svuota la password, quindi niente di ciò che punta a lei viene toccato.
Perché Supabase dice che non posso eliminare un utente che ha caricato file?
La sua guida alla gestione degli utenti lo dice ancora, ed era vero finché storage.objects aveva una foreign key chiamata objects_owner_fkey verso auth.users. Supabase Storage ha poi tolto quella chiave nelle sue migrazioni di schema. Su un progetto attuale la cancellazione va a buon fine e i file restano nel loro bucket, sotto un owner_id che nessun utente ha più.
Eliminare un utente lo disconnette?
Non subito. Secondo la guida di Supabase, un token di accesso già emesso resta valido finché scade, anche dopo che l'account è stato eliminato. Eliminare l'utente toglie le sue sessioni, quindi l'app non può ottenere un token nuovo, ma quello che ha già funziona fino alla sua scadenza.