Vai al contenuto

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.

Vlad Tkachenko11 min di lettura
Un registro dei soci con una riga barrata e, dietro, altri due libri da cui sono state strappate le righe con lo stesso gettone.

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:

  1. Apri Logs e leggi i log di Auth a quell'ora. La voce è il resoconto che Auth stesso ha registrato della cancellazione fallita.
  2. 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.users che 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.

Una cascata segue ogni regola che punta alla riga cancellata, poi ogni regola che punta a quelle. L'ultimo libro perde righe che appartenevano ad altre persone.

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 tabellaon deleteQuando l'utente viene eliminato
Solo la persona: un profilo, le sue impostazionicascadeLe righe se ne vanno con lui
Cose che servono ancora ad altri: un commento nel thread di un altro, un pagamentoset nullLe righe restano, con la colonna del proprietario vuota
Tutto ciò di cui vuoi occuparti tu per primono 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.

La stessa cancellazione con tre regole. La prima rifiuta e non cambia niente, la seconda porta via la riga che punta al socio, e la terza tiene quella riga con uno spazio vuoto.

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: cascade per le righe che descrivono solo la persona, set null per 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.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Fai il backup del database Supabase prima della prossima modifica

Reeve ne tiene una copia cifrata a intervalli programmati e la ripristina per te se qualcosa va storto.

Vedi i backup Supabase

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.