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:

PrefixClassExamples
22***Data exception22001 value too long, 22P02 invalid text representation
23***Integrity constraint23502 not null, 23503 FK, 23505 unique, 23514 check
25***Invalid transaction state25P02 in failed transaction
28***Invalid auth28000 invalid_authorization_specification
42***Syntax / permission42501 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:

  1. RLS is enabled but no policies exist. Default-deny means no operations work.
  2. Policies exist but don’t match this operation. A policy FOR SELECT doesn’t help with INSERT.
  3. You’re testing with anon key when policies require auth.uid(). Without a logged-in user, auth.uid() is NULL and most policies fail.

Fix paths:

  • For server-side code: use the service_role key (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 INSERT of 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 UPSERT instead of INSERT:
    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 submission with a question_id that doesn’t exist (typo, race, soft-delete)
  • Deleting a passage while child questions rows 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 parent
    • ON DELETE SET NULL — children’s FK column becomes NULL
    • ON 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 } omits undefined keys, but null values still send null.

”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 for null.
  • 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:

  1. The function was created but PostgREST’s schema cache is stale.
  2. The function exists in a non-public schema.
  3. 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 set db_schema in 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 call supabase.auth.getSession() — the SDK refreshes if needed.
  • For server-side Next.js: use @supabase/ssr and 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_role or postgres privileges. The Supabase SQL Editor runs as a privileged role by default.
  • If you’re running migrations via the CLI, ensure your db_url points 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:

  1. Typo in the table name.
  2. Migration hasn’t been run yet (table doesn’t exist in this environment).
  3. Wrong schema — table is in auth. or another schema; you need to qualify.
  4. Case sensitivity — Postgres folds unquoted identifiers to lowercase. "Users" and users are 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


Sources