Noções de segurança
Supabase "multiple (or no) rows returned" quase sempre é nenhuma
O Supabase devolve PGRST116, "JSON object requested, multiple (or no) rows returned", quando a consulta não achou nada. Linha escondida parece ausente.

Em resumo
- "JSON object requested, multiple (or no) rows returned" é o erro PGRST116, e quer dizer que sua consulta voltou com um número de linhas diferente de um. Quase sempre esse número é zero.
- Com Row Level Security ligada, uma linha que seu visitante não pode ler volta como nenhuma linha. Então um registro que você vê no Table Editor do Supabase pode faltar no seu app enquanto os dados estão perfeitamente bem.
- Use maybeSingle() onde não achar nada for uma resposta legítima e single() onde não for. Rode antes uma consulta no SQL Editor, para saber qual dos dois casos você tem.
Seu app diz a alguém que um registro não foi encontrado. Você abre o Table Editor do Supabase e o registro está ali, na tabela, com o id certo.
Isto foi o que voltou do banco:
{
"code": "PGRST116",
"message": "JSON object requested, multiple (or no) rows returned",
"details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row"
}
E esta é a parte que guia após guia erra: zero linhas é uma resposta que suas
próprias regras conseguem produzir, e ela se lê exatamente como dados que
sumiram. Procure essa mensagem e vão te dizer para trocar .single() por
.maybeSingle(). Isso de fato para o erro. Não te diz nada sobre a linha estar
faltando ou sobre seu visitante nunca ter tido permissão de vê-la, e esses dois
casos se consertam de formas diferentes.
O que significa "JSON object requested, multiple (or no) rows returned"?
Sua consulta pediu ao banco exatamente uma linha e o resultado tinha outro número. Quase sempre esse número é zero.
Pense numa sala de leitura onde você preenche uma ficha e alguém atrás do balcão vai buscar o livro. Sua ficha diz: traga o único livro com este número. O balcão volta e diz que não há nada correspondente à ficha. Essa é a única resposta que o balcão dá, e é a mesma tanto se o livro nunca esteve no catálogo quanto se ele está numa estante para a qual você não tem autorização. Ninguém está te enrolando. O sistema de fichas foi construído para que aquilo que você não tem autorização de ver simplesmente não apareça na sua visão do catálogo.
Seu app é quem segura a ficha, e o número com que o balcão voltou está escrito na
linha details da resposta.
Duas mensagens diferentes, e o mesmo código de erro
O código é estável e a frase não é, então se guie pelo PGRST116.
Há duas redações em circulação para esse único erro, e qual você vê depende de
qual versão do PostgREST seu projeto roda. O PostgREST é o servidor que transforma
suas tabelas do Supabase na API HTTP com que seu app conversa. A documentação da
versão 12 dá a mensagem
JSON object requested, multiple (or no) rows returned, com uma linha details
dizendo
Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row. Nas
versões 13 e 14 a mesma situação está documentada como
Cannot coerce the result to a single JSON object, com details dizendo
The result contains 0 rows.
A frase mais antiga tem uma segunda vida além dos projetos velhos. Quando você
chama .maybeSingle() e mais de uma linha bate, o cliente JavaScript do Supabase
escreve o erro ele mesmo em vez de repassar um do servidor, e a mensagem que ele
escreve é a redação antiga. Então a primeira frase continua chegando em apps cujo
banco teria mandado a segunda.
O que não se move é o código. A tabela de erros do PostgREST lista PGRST116 em
406 nas três versões, com a mesma descrição: mais de uma linha, ou nenhuma,
quando exatamente uma foi pedida. Leia o campo code e ignore o texto.
Por que .single() produz isso
Porque .single() é uma instrução sobre a forma da resposta e não um filtro
sobre os dados.
Quando você escreve .single(), o cliente do Supabase acrescenta um cabeçalho ao
pedido: Accept: application/vnd.pgrst.object+json. Esse cabeçalho pede que o
banco mande a linha sozinha, como um registro. É ele que deixa seu código
escrever data.name onde de outra forma teria de escrever data[0].name.
O banco cumpre isso à risca. Uma linha volta como um registro. Qualquer outro
número de linhas não pode ser mandado nessa forma, então a resposta é um 406 e um
objeto de erro. Tire o .single() e a consulta idêntica devolve [] com um 200 e
nenhum erro em lugar nenhum.
Então nada precisou quebrar para produzir isso. Um resultado vazio virou um erro porque sua consulta disse que a resposta tinha de conter exatamente um registro.
Causa 1: a linha está lá e suas regras dizem não
Row Level Security não recusa sua consulta. Ela tira linhas dela, e uma linha tirada nunca esteve no resultado que é contado.
Row Level Security é o conjunto de regras em cada tabela do Supabase que decide,
linha a linha, quais linhas um dado visitante pode ler. Quando nenhuma regra
permite que seu visitante leia a linha que sua consulta nomeou, essa linha é
retirada antes de o resultado ser medido. A contagem é zero. Seu .single()
queria uma. Você recebe PGRST116, e nenhuma parte da resposta menciona que
houve uma regra no meio.
É a sala de leitura de novo, e é por isso que o balcão diz a mesma coisa duas vezes. Sua autorização é aplicada antes mesmo de a estante ser vasculhada, então um livro que você não pode levar e um livro que não existe produzem uma ficha idêntica de volta.
É também por isso que o Table Editor te engana. O SQL Editor e o Table Editor do seu painel do Supabase rodam com privilégios aos quais suas policies não se aplicam, então mostram todas as linhas. O pedido do seu app leva sua publishable key e a sessão do seu visitante, e nele as policies se aplicam. Duas visões de uma mesma tabela, e só uma delas é a que seus usuários recebem.
Essa diferença dá para ler de fora, e é assim que conseguimos contá-la. Nosso scanner lê apps no ar como um estranho faria. Atrás de 8.435 dos apps que ele avaliou há um projeto Supabase. Em 3.680 deles a pergunta "alguém sem conta consegue ler esta tabela?" teve uma resposta utilizável, e 1.584 responderam que não: um estranho que pediu as linhas não recebeu nada. Esse nada é o mesmo nada que o seu próprio app recebe quando nenhuma policy se aplica a ele.
Como distinguir uma linha escondida de uma que falta
Rode a mesma busca duas vezes, uma onde as policies se aplicam e outra onde não se aplicam. Uma consulta resolve.
- Abra seu painel do Supabase, vá no SQL Editor e pegue a linha pelo mesmo
valor que seu app usou:
select * from your_table where id = 'the-id-your-app-sent';. Uma consulta a partir do SQL Editor roda pelo papelpostgres, ao qual suas policies não se aplicam. - Se ela devolver a linha, a linha existe e alguma coisa entre seu app e a linha a retirou. Essa coisa é uma policy, e você está na causa 1. Leia as policies daquela tabela.
- Se ela também não devolver nada, a linha falta de verdade e você está na causa 3 mais abaixo. Suas regras não têm nada a ver.
Faça isso antes de mudar uma linha de código. As duas causas produzem o mesmo objeto de erro, e os dois consertos não têm nada em comum.
Já que você está olhando as policies daquela tabela, vale saber o que elas dizem hoje para alguém sem conta nenhuma. Nosso scan gratuito lê seu app no ar e diz quais das suas tabelas respondem a um estranho. Leva cerca de 20 segundos e não precisa de conta: escanear seu app.
Causa 2: voltaram várias linhas
.single() foi usado numa busca que pode bater com mais de uma linha.
Uma busca por chave primária bate com uma linha ou com nenhuma, por definição. Uma busca por email, por slug, por nome, por status, por chave estrangeira: qualquer uma delas pode tocar duas. A história de sempre é que a coluna tinha um valor por linha nos seus dados de teste e deixou de ter quando gente de verdade chegou.
A resposta te diz em qual causa você está. As duas redações da linha details
dizem quantas linhas o resultado continha. Zero te manda para a causa 1 ou para a
causa 3. Um número acima de zero aponta esta aqui, e é a contagem das linhas que
bateram.
O conserto não é .maybeSingle(). Ele aceita zero linhas e continua dando erro
com duas, então aqui não muda nada. Ou você estreita a consulta para uma linha
com a coluna que é de fato única, ou aceita que há várias e lê uma lista.
Causa 3: a linha sumiu mesmo
Às vezes a linha falta, e a consulta da seção anterior é o que confirma isso.
Os motivos comuns, na ordem em que aparecem num app feito no vibe coding. O valor pelo qual sua consulta filtrava estava vazio, porque o id não tinha carregado quando a consulta saiu, então nada bateu. A linha foi apagada, por você ou por um prompt que reescreveu mais do que você esperava. Ou a consulta nomeia uma tabela que foi refeita com outro nome e as linhas antigas foram junto com a tabela antiga.
Nada disso é um problema de segurança, e nenhum se resolve mudando uma policy. Se o SQL Editor não achar nada, pare de ler sobre Row Level Security e vá ver o que seu app está passando.
single ou maybeSingle: qual a sua consulta quer
.maybeSingle() onde não achar nada é uma resposta legítima. .single() onde
não é.
.single() trata qualquer número de linhas diferente de um como erro. É o
comportamento certo para uma busca por chave primária, onde zero linhas quer
dizer que o registro de que seu código trata não existe e seguir em frente seria
pior do que parar.
.maybeSingle() aceita zero linhas: devolve data como null, sem erro e com
um 200. Mais de uma linha continua sendo erro, com o mesmo código PGRST116.
Então ele serve para uma linha de perfil que pode ainda não ter sido criada, ou
para um registro opcional para o qual sua página desenha um estado vazio.
Uma coisa para tomar cuidado. Trocar .single() por .maybeSingle() muda como
seu app se comporta e não muda nada do que o banco mandou. Se uma policy estava
escondendo a linha, você transformou um erro visível numa página que aparece como
se o registro não existisse, justo para a única pessoa que deveria vê-lo. O erro
estava te dizendo alguma coisa. Faça a troca depois do teste acima, quando você
souber que zero é a resposta honesta.
Ficar de olho nas suas regras depois de mudá-las
Uma policy que você abre para se livrar desse erro é uma mudança que ninguém vê de dentro do seu app. Sua própria sessão continua funcionando, o erro sumiu, e se aquela tabela agora responde a um estranho é uma pergunta que alguém precisa ir fazer.
O Reeve Monitor faz ela por você:
- as nove verificações a cada hora, em até três apps
- um email no dia em que um novo scan avaliar seu app pior que o anterior
- se o app responde, a cada 60 segundos
- um relatório mensal do que ele viu
Uma regra afrouxada o bastante para se livrar de um erro pode deixar um estranho mudar linhas além de lê-las. O Reeve Care guarda uma cópia do seu banco de dados do Supabase onde nenhuma mudança nas suas regras alcança.
- uma cópia criptografada todo dia, guardada fora da sua conta do 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á antes de substituir
- seus arquivos enviados também, assim que você conectar um acesso de Storage
- tudo o que o Monitor faz
O que fazer hoje
O que fazer
- Se guie pelo campo
code.PGRST116chega sob duas frases diferentes, e qual delas você recebe depende da versão do PostgREST que seu projeto roda. - Leia a contagem de linhas na linha
details. Zero quer dizer resultado vazio. Acima de zero, sua busca tocou várias linhas e precisa de uma coluna única. - Num resultado vazio, rode a mesma busca no SQL Editor antes de mudar código. Se a linha voltar lá e não no seu app, uma policy a retirou.
- Passe para
.maybeSingle()quando souber que zero é a resposta honesta. Numa linha que uma policy esconde, ele troca um erro por uma página vazia, sem aviso. - Depois de mudar uma policy para se livrar disso, verifique o que aquela tabela devolve agora para alguém sem conta.
Comece por uma consulta no SQL Editor, porque é ela que decide qual metade deste artigo vale para você. Se a resposta for uma policy, ligar Row Level Security em todas as tabelas mostra como escrever uma regra que deixa a pessoa certa passar, e "new row violates row-level security policy" é a mensagem que você recebe quando uma policy recusa uma escrita que você vê com os próprios olhos.
Perguntas frequentes
O que significa PGRST116?
Significa que sua consulta pediu ao banco exatamente uma linha e o resultado tinha outro número. O PostgREST, a parte do Supabase que responde ao seu app por HTTP, devolve isso como HTTP 406 com o código PGRST116. O número está na linha details da resposta, e quase sempre é zero.
Por que minha consulta não devolve linha nenhuma se eu vejo a linha no Supabase?
Porque o SQL Editor do seu painel roda com privilégios aos quais suas policies de Row Level Security não se aplicam, e seu app não. Se nenhuma policy permite que seu visitante leia aquela linha, ela é retirada do resultado dele antes de o resultado ser contado, e o que volta é um zero. A linha está lá. Ela nunca esteve na resposta.
Qual é a diferença entre single e maybeSingle?
single() trata qualquer número de linhas diferente de um como erro. maybeSingle() aceita zero linhas e devolve data como null, sem erro, e continua dando erro quando mais de uma linha bate. Então maybeSingle() serve onde não achar nada é uma resposta legítima, como um perfil que ainda não existe, e single() serve para uma busca por uma coluna que só pode bater com uma linha.
Row Level Security causa esse erro?
É a causa mais comum num app feito com Lovable, Bolt, Replit ou v0. Row Level Security filtra linhas para fora do resultado em vez de recusar o pedido, então uma linha que ninguém pode ler é indistinguível de uma linha que não existe. As duas dão contagem zero, e uma consulta que exige exatamente uma linha falha nas duas.
Por que é um 406?
406 Not Acceptable é o status de um pedido que o servidor não consegue responder na forma pedida. Sua consulta levava um cabeçalho Accept dizendo que a resposta tinha de ser um registro único e não uma lista, e sem linha nenhuma para pôr dentro não há resposta aceitável para mandar. Tire esse cabeçalho e a mesma consulta devolve uma lista vazia com um 200.