Supabase errors
Status: đ© COMPLETE (đŠ LIVING â Supabase changes the error surface over time) Last updated: 2026-06-21 Plain-English tagline: Postgres error codes youâll hit through Supabase, plus the Supabase-specific surprises (RLS, auth, service_role). What the cryptic numbers mean, what the fix is.
What this is
A paste-and-find reference for errors that come back from @supabase/supabase-js calls, the Supabase Studio, and the Supabase CLI. Most of these are standard Postgres error codes wrapped by the Supabase client; a few are Supabase-specific.
Pattern: error message or code â what it means â fix.
Postgres error code prefix cheat sheet
Postgres errors have 5-character SQLSTATE codes. The first two characters tell you the class:
| Prefix | Class | Examples |
|---|---|---|
22*** | Data exception | 22001 value too long, 22P02 invalid text representation |
23*** | Integrity constraint | 23502 not null, 23503 FK, 23505 unique, 23514 check |
25*** | Invalid transaction state | 25P02 in failed transaction |
28*** | Invalid auth | 28000 invalid_authorization_specification |
42*** | Syntax / permission | 42501 insufficient privilege, 42P01 undefined table, 42703 undefined column |
PGRST*** | PostgREST (Supabase API layer) | PGRST116 no rows returned for .single() |
When you see a code, the prefix narrows the problem class fast.
âpermission denied for table usersâ â code 42501
What it means: The role making the query doesnât have permission to read/write this table.
Most common cause in Supabase: the table was created via raw SQL without GRANT statements. The service_role role doesnât automatically get full access to tables it didnât create through the dashboard.
Fix: add this migration (the canonical Bible-Quest-tested pattern):
-- Grant full access to standard Supabase roles on the public schema
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated, service_role;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO anon, authenticated, service_role;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO anon, authenticated, service_role;
-- Future tables get the same defaults
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO anon, authenticated, service_role;Different cause: youâre using the anon key but RLS policies block access. RLS is enforced before the role check â even with full grants, RLS denies if no policy allows.
Reference: Row-Level Security đ©, Supabase đ© đŠ
ânew row violates row-level security policyâ
What it means: RLS is enabled on the table, and no policy allows this INSERT/UPDATE for the current role.
Common causes:
- RLS is enabled but no policies exist. Default-deny means no operations work.
- Policies exist but donât match this operation. A policy
FOR SELECTdoesnât help with INSERT. - Youâre testing with
anonkey when policies requireauth.uid(). Without a logged-in user,auth.uid()isNULLand most policies fail.
Fix paths:
- For server-side code: use the
service_rolekey (bypasses RLS entirely). Standard pattern for Server Components, Server Actions, Route Handlers. - For client-side code: add or fix the policy. Example for âusers can insert their own profileâ:
CREATE POLICY "users insert own profile" ON profiles FOR INSERT WITH CHECK (auth.uid() = user_id); - Verify policies in Supabase Studio: Database â Policies â check whatâs active for the table.
Reference: Row-Level Security đ©, How-to: Enable Row-Level Security đ©
âduplicate key value violates unique constraintâ â code 23505
What it means: You tried to INSERT (or UPDATE) a value that conflicts with an existing UNIQUE column or composite UNIQUE constraint.
Common scenarios:
- A user trying to register with an email that already exists
- A second
INSERTof the same row (failed idempotency) - A race condition where two requests insert the same value simultaneously
Fix:
- For âalready existsâ UX: catch the error, surface a friendly message (âThat email is takenâ).
- For idempotent operations: use
UPSERTinstead ofINSERT:await supabase .from("user_badges") .upsert({ user_id, badge_id }, { onConflict: "user_id,badge_id" }); - For races: the DB constraint protected you â thatâs the point. Catch and treat as success in idempotent flows. The Bible Quest pattern:
if (error?.code === "23505") return { ok: true };
âinsert or update on table âXâ violates foreign key constraintâ â code 23503
What it means: Youâre inserting a row that references a non-existent parent row, or updating/deleting a row that has children depending on it.
Common scenarios:
- Inserting a
submissionwith aquestion_idthat doesnât exist (typo, race, soft-delete) - Deleting a
passagewhile childquestionsrows still reference it
Fix:
- For inserts: verify the parent ID exists before inserting. Or accept the error as âparent missing â bail.â
- For deletes: decide cascade behavior at FK definition time:
ON DELETE CASCADEâ children get deleted with the parentON DELETE SET NULLâ childrenâs FK column becomes NULLON DELETE RESTRICT(default) â prevents deletion until children are gone
The Bible Quest patterns: team_id on users is ON DELETE SET NULL (deleting a team un-assigns members, not deletes them). winner_id on prizes is ON DELETE SET NULL (preserves history).
Reference: Joins and relationships đ©
ânull value in column âXâ violates not-null constraintâ â code 23502
What it means: Youâre inserting a row where a required column is missing.
Fix:
- Add the value to your insert. If the column should have a default, add a default in the schema.
- Check for missing properties if youâre spreading a partial object:
{ ...userData }omitsundefinedkeys, butnullvalues still sendnull.
âinvalid input syntax for type uuidâ â code 22P02
What it means: You passed something that doesnât parse as a UUID (or whatever type the column expects).
Common causes:
- Sending an empty string instead of NULL
- Sending an integer where a UUID is expected
- Confusing column types between tables
Fix: validate the value before the query. For optional UUIDs, send null not "".
âJSON object requested, multiple (or no) rows returnedâ â code PGRST116
What it means: You used .single() (or .maybeSingle()) but the query returned a row count other than what you expected.
With .single(): errors on 0 OR >1 rows.
With .maybeSingle(): errors only on >1 rows; returns null for 0.
Fix:
- If 0 is OK: use
.maybeSingle()instead of.single(). Check fornull. - If >1 is unexpected: add filters to your query, or use a tighter constraint (LIMIT 1, ORDER BY).
- For âI want all rowsâ: drop the
.single()entirely and iterate.
// .single() â must be exactly 1
const { data } = await supabase.from("users").select().eq("id", id).single();
// .maybeSingle() â 0 or 1
const { data } = await supabase.from("users").select().eq("email", email).maybeSingle();âcould not find the function âXâ in the schema cacheâ
What it means: You called .rpc("function_name") but PostgREST doesnât know the function exists.
Common causes:
- The function was created but PostgRESTâs schema cache is stale.
- The function exists in a non-public schema.
- The function signature doesnât match what you passed.
Fix:
- In Supabase Studio: Database â API â âReload schema cacheâ button.
- Verify the function exists:
SELECT proname FROM pg_proc WHERE proname = 'function_name'; - Check the schema: the function must be in
public(or you setdb_schemain config).
âJWT expiredâ or âInvalid JWTâ
What it means: Supabase Authâs session token is expired or malformed.
Common causes:
- Session was idle past expiry (default ~1 hour for access token)
- The client didnât refresh in time
- Server-side code is using a token captured from an old request
Fix:
- Client-side:
supabase.auth.refreshSession(). Or just callsupabase.auth.getSession()â the SDK refreshes if needed. - For server-side Next.js: use
@supabase/ssrand the proper cookie helpers. Donât store JWTs in memory across requests.
Reference: Sessions and cookies đ©, JWT đ©
âUser already registeredâ or âEmail rate limit exceededâ
What they mean:
- Already registered: the email is in
auth.users. Use sign-in, not sign-up. - Email rate limit: Supabase free tier limits outbound emails (sign-ups, magic links, password resets) to prevent abuse. Default limits are low â easy to hit during testing.
Fix:
- Rate limit: configure a custom SMTP provider (Resend, Postmark, SendGrid) in Supabase Auth settings. Once set up, Supabase routes emails through your provider with their limits.
âRealtime: too many channelsâ or silent disconnects
What it means: Free-tier limits on concurrent Realtime channels per project, or the connection died and didnât auto-reconnect.
Fix:
- Channel limits: unsubscribe channels when components unmount. Donât open multiple subscriptions to the same table.
- Auto-reconnect: the SDK reconnects by default, but you can monitor
supabase.realtime.connection.connectionState()if you need to surface state to users.
âFREE_TIER_USAGE_LIMIT_REACHEDâ / âproject pausedâ
What it means: Free-tier project paused after 1 week of inactivity, or hit a usage limit (database size, monthly active users, etc.).
Fix:
- Paused project: visit the dashboard and click âRestore.â Wait 1-2 minutes.
- Usage limit: upgrade to Pro ($25/month) or stay under the cap.
âpermission denied to set roleâ or âmust be member of roleâ
What it means: A SQL statement (often in migrations) tried to switch role via SET ROLE or SET SESSION AUTHORIZATION and the current user lacks permission.
Fix:
- Run the migration as a role that has
service_roleorpostgresprivileges. The Supabase SQL Editor runs as a privileged role by default. - If youâre running migrations via the CLI, ensure your
db_urlpoints at the right connection string.
ârelation âXâ does not existâ â code 42P01
What it means: The table doesnât exist in the searched schemas.
Common causes:
- Typo in the table name.
- Migration hasnât been run yet (table doesnât exist in this environment).
- Wrong schema â table is in
auth.or another schema; you need to qualify. - Case sensitivity â Postgres folds unquoted identifiers to lowercase.
"Users"andusersare different if you used quotes when creating.
Fix: verify the table exists in Supabase Studioâs Table Editor. Re-run the migration if missing.
âcolumn âXâ does not existâ â code 42703
What it means: The column name doesnât exist on this table.
Common causes:
- Typo
- Case sensitivity (same as above for tables)
- Out-of-date schema cache (try âReload schema cacheâ in Studio)
- Migration not applied
Fix: verify in Studio. The Supabase TypeScript client can also auto-generate types via supabase gen types â type-safe queries catch this at compile time.
See also
- Common errors â index đ©
- Build errors đ©
- Git errors đ©
- Supabase đ© đŠ â the textbook
- Postgres đ© â the DB underneath
- Row-Level Security đ©
- SQL the language đ©
- How-to: Set up a Supabase project đ©
- How-to: Add Supabase auth đ©
- How-to: Enable Row-Level Security đ©
- Supabase CLI cheat sheet đ©