From supabase-pack
Diagnoses and fixes Supabase errors in PostgREST, PostgreSQL, Auth, Storage, and Realtime including PGRST codes, RLS violations, unique constraints, and auth failures.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin supabase-packThis skill is limited to using the following tools:
Diagnostic guide for Supabase errors across PostgREST (`PGRST*`), PostgreSQL (numeric codes), Auth, Storage, and Realtime. Identify the error layer, trace the root cause, and apply the correct fix — every SDK call returns `{ data, error }` where `data` is null when `error` exists.
Identifies and fixes common Supabase pitfalls like service_role exposure, missing RLS, .single() errors, and performance issues. For code reviews, audits, onboarding, and debugging.
Build full-stack apps with Supabase: PostgreSQL database with RLS, authentication, storage, real-time subscriptions, edge functions. Use for auth, DB design, file storage, live features, serverless.
Provides Supabase best practices: verify current docs, enable RLS by default, security checklists for auth/JWT/sessions/storage, test implementations for Database/Auth/Edge Functions/Realtime.
Share bugs, ideas, or general feedback.
Diagnostic guide for Supabase errors across PostgREST (PGRST*), PostgreSQL (numeric codes), Auth, Storage, and Realtime. Identify the error layer, trace the root cause, and apply the correct fix — every SDK call returns { data, error } where data is null when error exists.
@supabase/supabase-js installed (npm install @supabase/supabase-js)SUPABASE_URL and SUPABASE_ANON_KEY (or SUPABASE_SERVICE_ROLE_KEY) configurednpx supabase --version)Every Supabase SDK call returns a { data, error } tuple. Never assume data exists — always check error first.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
)
// WRONG — data is null when error exists
const { data } = await supabase.from('todos').select('*')
console.log(data.length) // TypeError: Cannot read property 'length' of null
// CORRECT — always check error first
const { data, error } = await supabase.from('todos').select('*')
if (error) {
console.error(`[${error.code}] ${error.message}`)
console.error('Details:', error.details)
console.error('Hint:', error.hint)
// error.code tells you the layer:
// PGRST* = PostgREST (API gateway)
// 5-digit numeric = PostgreSQL (database)
// AuthApiError = Auth service
// StorageApiError = Storage service
return
}
// Safe to use data here
console.log(`Found ${data.length} rows`)
Troubleshooting: If error is undefined (not null), you may be using an older SDK version. Upgrade to @supabase/supabase-js@2.x or later.
Match the error code prefix to the correct subsystem, then look up the specific code in the tables below.
PostgREST errors start with PGRST and correspond to API-layer issues (JWT, query parsing, schema).
PostgreSQL errors are 5-character codes (e.g., 42501, 23505) from the database engine.
Auth errors come as AuthApiError with a human-readable message.
Storage errors come as StorageApiError with an HTTP status.
// Diagnostic helper — paste into your codebase to classify errors automatically
function diagnoseSupabaseError(error: { code?: string; message: string; status?: number }) {
if (!error) return 'No error'
if (error.code?.startsWith('PGRST')) {
return `PostgREST error ${error.code}: ${error.message}\n` +
'Check: JWT validity, column/table names, query syntax'
}
if (error.code && /^\d{5}$/.test(error.code)) {
return `PostgreSQL error ${error.code}: ${error.message}\n` +
'Check: RLS policies, constraints, schema migrations'
}
if (error.message?.includes('AuthApiError')) {
return `Auth error: ${error.message}\n` +
'Check: credentials, email confirmation, token expiry'
}
if (error.message?.includes('StorageApiError')) {
return `Storage error: ${error.message}\n` +
'Check: bucket exists, RLS on storage.objects, file size limits'
}
return `Unknown error: ${JSON.stringify(error)}`
}
Troubleshooting: If the error code is empty or missing, check the HTTP status code on the response. A 401 without a code usually means SUPABASE_ANON_KEY is wrong or missing. A 500 without a code usually means a database function threw an unhandled exception.
Once you have identified the error code, apply the corresponding fix from the Error Handling table. Then verify the fix by re-running the original operation.
// Example: Fix PGRST301 (JWT expired)
// Before: stale session causes 401
const { data, error } = await supabase.from('todos').select('*')
// error.code === 'PGRST301'
// Fix: refresh the session, then retry
const { error: refreshError } = await supabase.auth.refreshSession()
if (refreshError) {
// Token is fully invalid — force re-login
await supabase.auth.signOut()
console.error('Session expired. Please sign in again.')
return
}
// Retry the original query
const { data: retryData, error: retryError } = await supabase.from('todos').select('*')
if (retryError) {
console.error('Still failing after refresh:', retryError.code, retryError.message)
} else {
console.log('Fixed! Retrieved', retryData.length, 'rows')
}
// Example: Fix 42501 (RLS policy violation)
// Step A: Confirm RLS is the problem using service role client
const adminClient = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // bypasses RLS
{ auth: { autoRefreshToken: false, persistSession: false } }
)
const { data: adminData } = await adminClient.from('todos').select('*')
console.log('Admin sees', adminData?.length, 'rows') // If this works, RLS is blocking
// Step B: Check which user the JWT resolves to
const { data: { user } } = await supabase.auth.getUser()
console.log('Current auth.uid() =', user?.id)
// Step C: Fix the RLS policy in SQL Editor or migration
/*
CREATE POLICY "Users can read own todos"
ON todos FOR SELECT
USING (auth.uid() = user_id);
-- Verify with:
SET request.jwt.claim.sub = '<user-id>';
SELECT * FROM todos;
*/
// Step D: Retry original query
const { data: fixedData, error: fixedError } = await supabase.from('todos').select('*')
console.log(fixedError ? `Still blocked: ${fixedError.code}` : `Success: ${fixedData.length} rows`)
Troubleshooting: After applying a migration, you may need to reload the PostgREST schema cache. In the Supabase Dashboard, go to Settings > API and click "Reload schema cache", or call NOTIFY pgrst, 'reload schema' in SQL.
Deliverables after applying this skill:
if (error) checks) preventing silent null-data bugs| Code | HTTP | Meaning | Root Cause | Fix |
|---|---|---|---|---|
PGRST301 | 401 | JWT expired or invalid | SUPABASE_ANON_KEY is wrong, or the user session expired | Verify SUPABASE_ANON_KEY matches the project; call supabase.auth.refreshSession() |
PGRST302 | 401 | Missing Authorization header | Client created without a key, or middleware stripped the header | Pass SUPABASE_ANON_KEY to createClient(); check proxy/CDN config |
PGRST116 | 406 | No rows returned for .single() | Query matched 0 rows but .single() expects exactly 1 | Use .maybeSingle() for optional lookups, or check filters |
PGRST200 | 400 | Invalid query parameters | Malformed filter, bad operator, or invalid column reference | Check filter syntax: .eq('col', val) not .eq('col = val') |
PGRST204 | 400 | Column not found | Column name doesn't exist in the table or view | Verify column exists with supabase gen types typescript; check for typos |
PGRST000 | 503 | Connection pool exhausted | Too many concurrent connections from serverless functions | Enable pgBouncer (Supavisor) in project settings; reduce connection count |
| Code | Meaning | Root Cause | Fix |
|---|---|---|---|
42501 | RLS policy violation | Row-level security is blocking the operation for this user | Add or fix the RLS policy; test with service role to confirm |
23505 | Unique constraint violation | INSERT/UPDATE conflicts with an existing row | Use .upsert({ onConflict: 'column' }) or check existence first |
23503 | Foreign key violation | Referenced row doesn't exist in the parent table | Insert the parent row first, or check the foreign key value |
42P01 | Table or relation doesn't exist | Migration not applied, or wrong schema | Run supabase db push; verify schema with \dt in SQL Editor |
42703 | Column doesn't exist | Schema out of sync with code | Regenerate types: supabase gen types typescript --local > types/supabase.ts |
57014 | Query cancelled (statement timeout) | Query took longer than statement_timeout | Add indexes; simplify the query; increase timeout in postgresql.conf |
| Error Message | Cause | Fix |
|---|---|---|
invalid_credentials / Invalid login credentials | Wrong email or password | Verify credentials; check if email is confirmed |
email_not_confirmed / Email not confirmed | User hasn't clicked confirmation link | Check inbox/spam; for local dev check Inbucket at localhost:54324 |
user_already_exists / User already registered | Duplicate sign-up | Call signInWithPassword() instead of signUp() |
Token has expired or is invalid | Stale magic link or OTP | Request a new magic link or OTP; links expire after 5 minutes by default |
AuthRetryableFetchError | Network failure reaching Auth service | Retry with backoff; verify SUPABASE_URL is correct and reachable |
| Error | Cause | Fix |
|---|---|---|
Bucket not found | Bucket name is wrong or bucket doesn't exist | Create the bucket in Dashboard or via migration SQL |
The resource already exists | Uploading to a path that already has a file | Pass { upsert: true } in upload options to overwrite |
new row violates row-level security | Storage RLS blocking the upload/download | Add a policy on storage.objects for the operation (INSERT, SELECT, DELETE) |
413 Payload Too Large | File exceeds the bucket's size limit | Increase file_size_limit on the bucket, or use TUS resumable upload for large files |
| Symptom | Cause | Fix |
|---|---|---|
CHANNEL_ERROR on subscribe | Realtime not enabled for the table | Dashboard > Database > Replication > enable the table; or add it to supabase_realtime publication |
TIMED_OUT on subscribe | Network issue or firewall blocking WebSocket | Check that port 443 WebSocket connections are allowed |
| No events received | Table not in Realtime publication | Run: ALTER PUBLICATION supabase_realtime ADD TABLE your_table; |
| Events stop after deploy | Schema change drops Realtime connections | Clients auto-reconnect; ensure .subscribe() handles reconnection |
.single() on optional data (PGRST116)// BAD — crashes when user has no profile
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('user_id', userId)
.single() // throws PGRST116 if no row exists
// GOOD — returns null instead of erroring
const { data: profile, error } = await supabase
.from('profiles')
.select('*')
.eq('user_id', userId)
.maybeSingle()
if (!profile) {
// Create a default profile
const { data: newProfile } = await supabase
.from('profiles')
.insert({ user_id: userId, display_name: 'New User' })
.select()
.single()
}
// BAD — fails if row already exists
const { error } = await supabase
.from('user_settings')
.insert({ user_id: userId, theme: 'dark' })
// error.code === '23505' — unique constraint on user_id
// GOOD — inserts or updates based on conflict column
const { data, error } = await supabase
.from('user_settings')
.upsert(
{ user_id: userId, theme: 'dark' },
{ onConflict: 'user_id' }
)
.select()
.single()
const channel = supabase
.channel('todos-changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'todos' },
(payload) => {
console.log('Change received:', payload.eventType, payload.new)
}
)
.subscribe((status, err) => {
switch (status) {
case 'SUBSCRIBED':
console.log('Realtime connected')
break
case 'CHANNEL_ERROR':
console.error('Realtime error — is the table in the publication?', err)
// Fix: ALTER PUBLICATION supabase_realtime ADD TABLE todos;
break
case 'TIMED_OUT':
console.error('Realtime timed out — check network')
break
case 'CLOSED':
console.log('Channel closed')
break
}
})
// Always clean up on unmount / exit
process.on('SIGINT', async () => {
await supabase.removeChannel(channel)
process.exit(0)
})
// BAD — creates a new client per request in serverless (Lambda, Edge Functions)
export async function handler(req: Request) {
const supabase = createClient(url, key) // new connection every invocation
const { data } = await supabase.from('todos').select('*')
return Response.json(data)
}
// GOOD — reuse client across warm invocations
const supabase = createClient(url, key, {
auth: { autoRefreshToken: false, persistSession: false }
})
export async function handler(req: Request) {
const { data, error } = await supabase.from('todos').select('*')
if (error) {
if (error.code === 'PGRST000') {
// Pool exhausted — return 503 so the caller retries
return new Response('Service temporarily unavailable', { status: 503 })
}
return Response.json({ error: error.message }, { status: 400 })
}
return Response.json(data)
}
supabase-debug-bundle to generate a full diagnostic snapshot when errors persist after applying these fixes.supabase-security-basics to audit your RLS policies and prevent 42501 errors proactively.supabase-known-pitfalls for edge cases and SDK behavior that can cause subtle bugs.supabase-observability to set up logging and alerting so you catch errors before users report them.