Home / RLS / Fix PGRST116: Cannot coerce the result
Supabase RLS · Error fix
Fix PGRST116: Cannot coerce the result to a single JSON object
.single() demands exactly one row. You got 0 (or more than 1). If the row does exist, RLS is usually hiding it from the current user, because a row RLS filters out simply isn't returned. Fix the policy or the session; use .maybeSingle() when zero rows is a normal answer.
01 What the error says
response
{
"code": "PGRST116",
"message": "Cannot coerce the result to a single JSON object",
"details": "The result contains 0 rows"
}
.single() asks the Data API for exactly one object. When the query returns 0 rows or more than 1, it answers HTTP 406 with PGRST116. The details line tells you which: 0 rows or 2 rows and so on.
02 "But the row exists": that's RLS
RLS doesn't raise an error for rows you can't see. It filters them out, so the query returns 0 rows and .single() turns that into PGRST116. You see the row in the dashboard because the dashboard bypasses RLS.
No session, or the wrong one
The client is calling as anon (not signed in yet, or a server client without the user's cookies), so auth.uid() is null and an owner policy matches nothing.
The policy doesn't cover this row
An owner policy like using (id = (select auth.uid())) only returns the caller's own row. Fetching someone else's profile with .single() gets 0 rows.
The filter really matches 0 or many rows
A wrong id, or a filter on a non-unique column that matches several rows.
03 Find out which one
debug.ts
const { data: { user } } = await supabase.auth.getUser(); console.log('calling as', user?.id ?? 'anon'); // null → cause 1 // same query without .single(): see how many rows come back const { data } = await supabase.from('profiles').select('*').eq('id', id); console.log(data.length); // 0 with a valid id → RLS is hiding it
04 The fix
- Cause 1: make the request carry the user's session. On the server, create the Supabase client from the request cookies so
auth.uid()resolves. - Cause 2: if other users should see this row, add a policy for that case (for example, a public profile field). Don't widen it to
using (true)on private data. - Zero rows is a normal answer (a profile not created yet): use
.maybeSingle(). It returnsdata: nullfor 0 rows and still errors on more than 1.
profile.ts
const { data: profile, error } = await supabase .from('profiles').select('*').eq('id', user.id).maybeSingle(); if (!profile) { /* first visit: create it */ }
Catch this before it ships
airlock-rls is a CI gate that fails your build when a table ships exposed or a policy is permissive: the same class of bug, caught in CI against a database with your migrations applied, before it reaches prod.
Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.
Grab the Supabase RLS cheat sheet
The golden rules, the footguns that leak in prod, correct policy snippets, and the isolation test — on one page.
FAQ
Should I just switch every .single() to .maybeSingle()?
Only where zero rows is a legitimate answer. If the row must exist, a PGRST116 is useful: it tells you the user can't see it, which is usually a session or policy bug you want to fix, not hide.
Can a missing GRANT cause PGRST116?
No. A missing grant fails louder: permission denied for table (42501). PGRST116 means the query ran and returned the wrong number of rows.