Pular para o conteúdo

Backups

"Database error deleting user" no Supabase, e o que vai junto

"Database error deleting user" no Supabase: seu banco recusou a exclusão. O motivo está nos seus logs, e a correção decide o que mais é apagado.

Vlad Tkachenko12 min de leitura
Um livro de sócios com uma linha riscada e, atrás, mais dois livros de onde foram arrancadas as linhas com a mesma ficha.

Em resumo

  • "Database error deleting user" no Supabase quer dizer que a exclusão chegou ao seu banco e foi recusada, então nada foi removido. A linha que diz a causa está nos seus logs do Postgres, e a própria nota de troubleshooting do Supabase manda você para lá.
  • Quando essa linha diz que uma foreign key foi violada, uma linha de alguma das suas tabelas ainda guarda o id do usuário. O Postgres não remove uma pessoa enquanto algo ainda aponta para ela, a não ser que você tenha dito o que fazer com essas linhas.
  • A primeira resposta que você vai achar é on delete cascade. Ela apaga cada linha que aponta para o usuário, e cada linha que aponta para essas, em toda exclusão a partir daí. Decida tabela por tabela, e faça uma cópia antes de mudar uma regra.

Você abre Authentication → Users no painel do Supabase e exclui uma conta. Talvez a pessoa tenha pedido para ser removida, talvez seja uma conta de teste que você criou na semana passada. O painel responde:

Failed to delete user: Database error deleting user

A conta continua na lista. O app que você fez com Lovable, Bolt, v0, Cursor, Claude Code ou Replit funcionou o tempo todo, e a frase não cita tabela nenhuma nem motivo nenhum.

A primeira resposta que você vai achar é uma linha de SQL: coloque on delete cascade no que está atrapalhando. Funciona. Também decide, para cada conta excluída dali em diante, quais outras linhas vão junto, e ninguém volta a perguntar isso para você. Então leia primeiro o erro real, e depois tome essa decisão tabela por tabela.

O que quer dizer "Database error deleting user" no Supabase?

O seu banco recusou a exclusão e o Supabase desfez tudo, então a conta está exatamente como estava.

Excluir um usuário é trabalho do Supabase Auth, o serviço por trás do login do seu app. No código-fonte do Auth, a exclusão acontece dentro de uma única transação, um grupo de mudanças que acontecem todas ou nenhuma, e o passo central dela tira a linha da pessoa de auth.users. Se qualquer coisa no seu banco se opõe, a transação é revertida e o Auth responde com essa única frase. O Supabase lista a mensagem como HTTP 500 com o código unexpected_failure, ao lado da gêmea do cadastro, "Database error saving new user".

Pense no seu banco como um clube. auth.users é o livro de sócios, e cada sócio tem um número: o id de usuário. Os seus outros livros copiam esse número nas linhas deles, seja um perfil, um pedido, um post ou um comentário. Riscar alguém do livro de sócios é coisa de uma linha. Só que todo outro livro ainda carrega o número, e enquanto ninguém escreveu o que fazer com essas linhas, o secretário do clube se recusa.

Onde está o erro real?

Nos logs do Postgres do seu projeto, no momento em que a exclusão falhou.

A nota de troubleshooting do Supabase sobre os erros na gestão de usuários mostra o caminho. Tente a exclusão uma vez e anote a hora. Depois, no painel:

  1. Abra Logs e leia os logs de Auth nesse horário. A entrada é o registro que o próprio Auth guardou da exclusão que falhou.
  2. Abra os logs do Postgres no mesmo horário. Essa linha é o erro real, e ela cita uma tabela, uma regra ou uma permissão.

A mesma nota lista três causas, e a linha do log diz qual é a sua:

  • um trigger na tabela auth.users que falha
  • uma restrição que não está sendo cumprida, o que numa exclusão quer dizer uma foreign key de outra tabela apontando para auth.users
  • o Prisma, que segundo a nota pode quebrar as permissões em auth.users

Uma tabela que ainda aponta para o usuário

Se a linha do Postgres diz violates foreign key constraint, uma linha de alguma das suas tabelas ainda guarda o id desse usuário, e o Postgres não remove uma pessoa para quem algo ainda aponta.

A nota do Supabase imprime a forma que isso toma:

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".

Uma foreign key é uma regra numa coluna que diz que todo valor dela precisa existir em outra tabela. Aqui a regra se chama profiles_id_fkey, fica na sua tabela profiles, e a linha Key é o id do usuário que você tentou excluir. No clube, é uma linha do livro de perfis com o número do sócio, e o secretário se recusando a riscar o nome enquanto ela estiver lá.

A mesma regra também recusa pelo outro lado. Uma linha nova que cita um usuário que não existe volta como insert or update on table … violates foreign key constraint, e essa mensagem tem um post só dela.

A recusa é o padrão. Uma foreign key criada sem nada depois de references auth.users recebe o comportamento que o Postgres chama de no action, e o guia do Supabase sobre exclusões em cascata diz isso com todas as letras: NO ACTION é o comportamento padrão se você não especificar nada. Uma tabela que o seu builder criou assim funciona perfeitamente até a primeira vez que alguém é excluído.

Para ver cada tabela que aponta para os seus usuários, e o que cada uma faz numa exclusão, rode isto no 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;

Ela só lê o catálogo do próprio Postgres e não muda nada. A última linha deixa de fora as tabelas do Supabase no schema auth. Cada linha que mostra no action ou restrict é uma tabela que vai recusar a exclusão sempre que tiver uma linha desse usuário.

O que o on delete cascade apaga?

Cada linha que aponta para o usuário, depois cada linha que aponta para essas, em toda exclusão a partir daí.

É a correção que entregam para você primeiro, e o guia de gestão de usuários do Supabase usa essa correção numa tabela de perfis: ele pede que você especifique on delete cascade na referência. Para um perfil, está certo. Um perfil descreve a pessoa e mais nada, então deve sair quando ela sai.

O problema começa quando as mesmas palavras vão para todas as tabelas. O guia de cascata do Supabase descreve o comportamento sem rodeios: quando uma linha é apagada da tabela pai, todas as linhas relacionadas nas tabelas filhas são apagadas também. E continua descendo. No próprio exemplo do guia, uma única exclusão na tabela de cima esvazia as duas tabelas abaixo dela.

No clube, uma cascata manda o secretário arrancar cada linha com o número do sócio, e depois cada linha de qualquer outro livro que apontava para uma linha arrancada. Imagine um usuário que escreveu posts, com comentários de outras pessoas apontando para esses posts. Com cascata nos dois, excluir esse único usuário remove os posts dele e os comentários de todo mundo neles.

Uma cascata segue cada regra que aponta para a linha apagada, e depois cada regra que aponta para essas. O último livro perde linhas que eram de outras pessoas.

Nada avisa o que foi junto. O Auth responde a uma exclusão bem-sucedida com uma resposta vazia, o painel mostra uma conta a menos, e as linhas que saíram no mesmo momento estão faltando na próxima vez que alguém procura por elas.

O que cada tabela guarda

Decida tabela por tabela, porque cada uma guarda algo diferente sobre a pessoa que saiu.

O que a tabela guardaon deleteQuando o usuário é excluído
Só a pessoa: um perfil, as configurações delacascadeAs linhas vão junto
Coisas de que outros ainda precisam: um comentário na conversa de outro, um pagamentoset nullAs linhas ficam, com a coluna de dono vazia
Qualquer coisa que você queira resolver à mão primeirono action (o padrão)A exclusão é recusada enquanto elas existirem

O set null vem com duas condições. A coluna precisa aceitar valores vazios, ou a exclusão falha de novo com uma mensagem própria, null value in column … violates not-null constraint. E uma policy escrita como user_id = auth.uid() nunca mais vai mostrar essa linha para ninguém, porque em SQL nada é igual a um valor vazio. A linha fica guardada, e o seu app deixa de mostrar essa linha para qualquer pessoa.

Mudar uma regra quer dizer tirar e colocar de novo com o comportamento novo, que é o que a nota do Supabase recomenda:

alter table public.profiles
  drop constraint profiles_id_fkey,
  add constraint profiles_id_fkey
    foreign key (id) references auth.users (id) on delete cascade;

Use o nome de regra que a consulta acima mostrou, com a sua própria tabela e coluna.

Existe mais um caminho, e ele mantém todas as linhas. O Supabase Auth consegue fazer uma exclusão suave de uma conta: auth.admin.deleteUser() aceita a opção shouldSoftDelete, e com ela o Auth mantém a linha em auth.users, marca como excluída, embaralha o email e o telefone, apaga a senha e os metadados, e encerra as sessões. Nada foi removido, então nenhuma foreign key dispara. Isso roda num servidor com a sua chave secreta, nunca no navegador.

A mesma exclusão sob três regras. A primeira recusa e não muda nada, a segunda leva junto a linha que aponta para o sócio, e a terceira mantém essa linha com um espaço vazio.

Um trigger que não alcança a sua tabela

Se a linha do Postgres diz permission denied for table, um trigger em auth.users rodou durante a exclusão e tentou mexer numa tabela em que não tem permissão de entrar.

O papel com que o Supabase Auth trabalha, supabase_auth_admin, só tem as permissões que o trabalho dele precisa. A nota do Supabase diz assim: quando um trigger executado por supabase_auth_admin age fora do schema auth, ele causa um erro de permissão. Um trigger que faz a limpeza depois de uma conta excluída, por exemplo gravando numa tabela de log sua, falha exatamente assim, a menos que a função dele tenha sido criada com security definer.

O conserto é o mesmo do cadastro: uma função security definer, criada pelo papel postgres, que faz uma única coisa bem delimitada. A versão deste erro no cadastro explica linha por linha, incluindo o search path vazio que a mantém segura.

Se o seu app usa Prisma, leia primeiro a terceira causa na nota do Supabase, porque ela cita o Prisma como uma das formas de quebrar as permissões em auth.users.

O que acontece com os arquivos que a pessoa enviou?

Num projeto atual, a exclusão passa, e os arquivos ficam onde estão.

O guia de gestão de usuários do Supabase ainda avisa que você não consegue excluir um usuário que é dono de objetos no Supabase Storage. Isso era verdade enquanto storage.objects, a tabela em que o Storage guarda uma linha para cada arquivo, tinha uma foreign key chamada objects_owner_fkey apontando para auth.users. O Storage já removeu essa chave nas próprias migrações de schema (0017-drop-owner-foreign-key.sql), e o guia dele sobre propriedade de arquivos agora diz que o dono fica numa coluna owner_id. Se a consulta lá em cima listar storage.objects com objects_owner_fkey, o seu projeto ainda tem a regra antiga e o aviso do guia vale para você.

Senão, cada arquivo que a pessoa enviou fica no bucket depois que ela vai embora. No clube, os casacos continuam nos ganchos com uma ficha que ninguém tem mais. Uma policy escrita como owner_id = auth.uid()::text nunca mais vai encontrar esses arquivos, e um arquivo num bucket público continua sendo servido para qualquer um que tenha o link. Se um estranho consegue listar os arquivos de um bucket seu é uma das verificações do scan gratuito do Reeve.

Para tirar esses arquivos, passe pelo próprio Storage, pela página Storage do painel ou pela API do Storage. Um delete direto em SQL nas tabelas dele é recusado, e o Storage diz por quê: "Direct deletion from storage tables is not allowed. Use the Storage API instead." O que uma cópia do banco guarda desses arquivos, e o que deixa de fora, está em como fazer backup do Supabase Storage.

Faça uma cópia antes de mudar uma foreign key

O Postgres não tem lixeira, e uma cascata remove linhas de tabelas que você nem estava olhando.

O que uma cascata ou um set null muda é definitivo assim que a exclusão dá certo, e o único caminho de volta é uma cópia feita antes. O plano gratuito do Supabase não guarda nenhum backup. O Pro guarda 7 dias de cópias diárias, o Team 14 e o Enterprise até 30, e todas ficam dentro do mesmo projeto Supabase. Copiar o seu banco por conta própria leva poucos comandos, e uma cópia que inclua os seus usuários precisa do terceiro dos três comandos que o Supabase publica.

Depois teste as regras novas numa conta de teste antes de uma real. Crie uma conta, dê a ela uma linha em cada tabela que a consulta listou, exclua a conta e veja o que sobrou em cada tabela.

Se você prefere que essa cópia já exista antes de alguém apertar excluir, é para isso que servem os backups do Supabase do Reeve.

Um caminho de volta quando uma exclusão leva mais do que devia

Uma cascata não avisa nada do que removeu. Você descobre quando alguém pergunta onde foram parar os comentários dele.

O Reeve Care guarda uma cópia do seu banco Supabase de antes da exclusão:

  • uma cópia criptografada conforme uma agenda, guardada no armazenamento do próprio Reeve na UE, fora da sua conta Supabase
  • cada cópia verificada antes de contar, contando as linhas de cada tabela
  • uma restauração em um clique que salva o que está lá agora antes de substituir
  • também os seus arquivos enviados, assim que você conectar uma credencial de Storage
  • tudo o que o Reeve Monitor faz: as nove verificações do seu app no ar a cada hora, e uma verificação de disponibilidade a cada 60 segundos

O que fazer hoje

O que fazer

  • Tente a exclusão uma vez, anote a hora e leia a linha do log do Postgres desse momento antes de mudar qualquer coisa.
  • Se ela citar uma foreign key, rode a consulta acima para listar cada tabela que aponta para os seus usuários e o que cada uma faz numa exclusão.
  • Decida tabela por tabela: cascade para linhas que só descrevem a pessoa, set null para linhas de que outros ainda precisam, o padrão para tudo o que você quer tirar à mão.
  • Faça uma cópia do banco antes de mudar qualquer uma dessas regras, e teste as regras novas primeiro numa conta de teste.
  • Tire também os arquivos da pessoa pelo Storage. Num projeto atual, a conta sai e os arquivos ficam.

Se linhas já foram embora além do que você queria, o que dá para recuperar depois de uma exclusão mostra onde olhar, em ordem.

Perguntas frequentes

O que quer dizer "Database error deleting user" no Supabase?

Quer dizer que o Supabase Auth tentou tirar a linha da pessoa de auth.users, algo no seu banco recusou, e a exclusão inteira foi desfeita. A conta está exatamente como estava. A mensagem não cita nenhuma tabela, e a linha que cita está nos logs do Postgres do seu projeto no mesmo momento.

Devo adicionar on delete cascade para resolver?

Numa tabela que só descreve a pessoa, como uma tabela de perfis, sim, e o próprio guia do Supabase faz exatamente isso. Numa tabela com coisas de que outras pessoas ainda precisam, como comentários ou pedidos, decida antes. O cascade apaga cada linha que aponta para o usuário e cada linha que aponta para essas, em toda exclusão dali em diante, e nada avisa o que foi removido.

Posso excluir um usuário e manter as linhas dele?

Sim, de dois jeitos. Mude a foreign key para on delete set null, e as linhas ficam com a coluna de dono vazia, o que exige que essa coluna aceite valores vazios. Ou chame auth.admin.deleteUser() de um servidor com a opção shouldSoftDelete: o Supabase Auth mantém a linha, marca como excluída, embaralha o email e o telefone e apaga a senha, então nada do que aponta para ela é tocado.

Por que o Supabase diz que não posso excluir um usuário que enviou arquivos?

O guia de gestão de usuários ainda diz isso, e era verdade enquanto storage.objects tinha uma foreign key chamada objects_owner_fkey apontando para auth.users. O Supabase Storage já removeu essa chave nas próprias migrações de schema. Num projeto atual a exclusão passa e os arquivos ficam no bucket, com um owner_id que nenhum usuário tem mais.

Excluir um usuário desconecta ele?

Não na hora. Segundo o guia do Supabase, um token de acesso já emitido continua válido até expirar, mesmo depois de a conta ser excluída. Excluir o usuário remove as sessões dele, então o app não consegue um token novo, mas o que já está em mãos funciona até a hora de expirar.

Escrito por

Vlad Tkachenko

Fundador da Reeve

Passo meus dias olhando apps criados com Lovable, Bolt, v0, Cursor e Replit, e a curta lista de erros que aparecem neles sem parar.

Mais sobre o autor

Leia depois

Todos os artigos

Faça backup do seu banco Supabase antes da próxima mudança

O Reeve guarda uma cópia criptografada em horários programados e a restaura para você se algo quebrar.

Ver backups do Supabase

Verificação externa automatizada, não uma auditoria completa. A ausência de achados não é garantia de segurança.