Home / RLS / Fix PGRST116: Cannot coerce the result

Supabase RLS · Error fix

Fix PGRST116: Cannot coerce the result to a single JSON object

Postgres 15 · Supabase✓ code tested against a real database
TL;DR

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

CAUSE 1 · most common

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.

CAUSE 2

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.

CAUSE 3

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

profile.ts

const { data: profile, error } = await supabase
  .from('profiles').select('*').eq('id', user.id).maybeSingle();
if (!profile) { /* first visit: create it */ }
◆ FREE · MIT · npm + GitHub Action

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.

npx airlock-rls→

Or start from nextjs-supabase-starter : auth + a table with RLS + an isolation test, so you start from a pattern that's already sealed.

FREE PDF

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.