| name | supabase-known-pitfalls |
| description | Avoid and fix the most common Supabase mistakes: exposing service_role key
in client bundles, forgetting to enable RLS, not using connection pooling
in serverless, .single() throwing on empty results, missing .select() after
insert/update, not destructuring { data, error }, creating multiple client
instances, and not using generated types.
Use when reviewing Supabase code, onboarding developers, auditing an
existing project, or debugging unexpected behavior.
Trigger with phrases like "supabase mistakes", "supabase anti-patterns",
"supabase pitfalls", "supabase code review", "supabase gotchas",
"supabase debugging", "what not to do supabase", "supabase common errors".
|
| allowed-tools | Read, Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","supabase","anti-patterns","code-review","debugging","security","pitfalls"] |
| compatibility | Designed for Claude Code, also compatible with Codex and OpenClaw |
Supabase Known Pitfalls
Overview
The twelve most common Supabase mistakes, ranked by severity: security (service_role exposure, missing RLS, permissive policies), data integrity (ignoring { data, error }, missing .select() after mutations, .single() on optional results), performance (select('*'), N+1 queries, missing FK indexes, synchronous auth checks), and maintainability (no generated types, multiple client instances, hardcoded connection strings). Each pitfall shows the broken code, explains why it fails, and provides the correct pattern using createClient from @supabase/supabase-js.
Prerequisites
- Access to a Supabase project codebase for review
@supabase/supabase-js v2+ installed
- Basic understanding of Row Level Security (RLS)
Step 1 — Security Pitfalls (Critical)
These mistakes can expose all your data to any user with browser dev tools.
Pitfall 1: Exposing service_role Key in Client Code
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!
)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
)
Detection:
grep -rn 'SERVICE_ROLE' --include="*.tsx" --include="*.jsx" --include="*.ts" src/ app/ components/ pages/
grep -rn 'NEXT_PUBLIC.*SERVICE_ROLE' .env* *.ts *.tsx
Pitfall 2: Tables Without RLS Enabled
CREATE TABLE public.medical_records (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id uuid REFERENCES auth.users(id),
diagnosis text,
ssn text
);
CREATE TABLE public.medical_records (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id uuid REFERENCES auth.users(id),
diagnosis text,
ssn text
);
ALTER TABLE public.medical_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "patients_read_own" ON public.medical_records
FOR SELECT USING (patient_id = auth.uid());
Detection:
SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname = 'public'
AND rowsecurity = false
AND tablename NOT LIKE '\_%';
Pitfall 3: Overly Permissive RLS Policies
CREATE POLICY "anyone_can_read" ON public.messages
FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "anyone_can_update" ON public.profiles
FOR UPDATE USING (auth.uid() IS NOT NULL);
CREATE POLICY "read_own_messages" ON public.messages
FOR SELECT USING (
sender_id = auth.uid() OR recipient_id = auth.uid()
);
CREATE POLICY "update_own_profile" ON public.profiles
FOR UPDATE USING (id = auth.uid());
Pitfall 4: Not Using Connection Pooling in Serverless
const connectionString = 'postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres'
const connectionString = 'postgresql://postgres.xxx:pass@aws-0-us-east-1.pooler.supabase.com:6543/postgres'
Step 2 — Data Integrity Pitfalls (High)
These mistakes cause silent data loss, null pointer errors, and inconsistent state.
Pitfall 5: Not Handling { data, error }
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data } = await supabase.from('orders').insert(order).select().single()
console.log(data.id)
const { data, error } = await supabase.from('orders').insert(order).select().single()
if (error) {
console.error('Insert failed:', error.code, error.message, error.details)
throw new Error(`Order creation failed: ${error.message}`)
}
console.log(data.id)
Pitfall 6: Missing .select() After Insert/Update/Upsert
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data } = await supabase.from('todos').insert({ title: 'Buy milk' })
console.log(data)
const { data, error } = await supabase
.from('todos')
.insert({ title: 'Buy milk' })
.select('id, title, is_complete, created_at')
.single()
if (error) throw new Error(`Insert failed: ${error.message}`)
console.log(data)
Pitfall 7: .single() on Empty or Multiple Results
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data, error } = await supabase
.from('profiles')
.select('id, username, avatar_url')
.eq('username', searchTerm)
.single()
const { data, error } = await supabase
.from('profiles')
.select('id, username, avatar_url')
.eq('username', searchTerm)
.maybeSingle()
Step 3 — Performance and Maintainability Pitfalls (Medium/Low)
Pitfall 8: select('*') Everywhere
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data } = await supabase.from('posts').select('*')
const { data } = await supabase
.from('posts')
.select('id, title, slug, excerpt, published_at, author:profiles(name, avatar_url)')
Pitfall 9: N+1 Query Pattern
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data: projects } = await supabase.from('projects').select('id, name')
for (const project of projects ?? []) {
const { data: tasks } = await supabase
.from('tasks')
.select('id, title, status')
.eq('project_id', project.id)
project.tasks = tasks
}
const { data } = await supabase
.from('projects')
.select(`
id, name,
tasks (id, title, status)
`)
const projectIds = projects?.map(p => p.id) ?? []
const { data: allTasks } = await supabase
.from('tasks')
.select('id, title, status, project_id')
.in('project_id', projectIds)
Pitfall 10: Missing Indexes on Foreign Key Columns
CREATE TABLE public.comments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
post_id uuid REFERENCES public.posts(id),
author_id uuid REFERENCES auth.users(id),
body text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE public.comments (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
post_id uuid REFERENCES public.posts(id),
author_id uuid REFERENCES auth.users(id),
body text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_comments_post_id ON public.comments(post_id);
CREATE INDEX idx_comments_author_id ON public.comments(author_id);
Detection:
SELECT
tc.table_name,
kcu.column_name,
'CREATE INDEX idx_' || tc.table_name || '_' || kcu.column_name
|| ' ON public.' || tc.table_name || '(' || kcu.column_name || ');' AS fix
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
LEFT JOIN pg_indexes pi
ON pi.tablename = tc.table_name
AND pi.indexdef LIKE '%' || kcu.column_name || '%'
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND pi.indexname IS NULL;
Pitfall 11: Creating Multiple Client Instances
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
import { createClient } from '@supabase/supabase-js'
import type { Database } from './database.types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
Pitfall 12: Not Using Generated Types
interface Todo {
id: number
title: string
done: boolean
createdAt: string
}
import type { Database } from './database.types'
type Todo = Database['public']['Tables']['todos']['Row']
type TodoInsert = Database['public']['Tables']['todos']['Insert']
import { createClient } from '@supabase/supabase-js'
const supabase = createClient<Database>(url, key)
const { data } = await supabase.from('todos').select('id, title, is_complete')
Output
- Security pitfalls identified: service_role exposure, missing RLS, permissive policies, no connection pooling
- Data integrity pitfalls fixed:
{ data, error } handling, .select() after mutations, .maybeSingle() usage
- Performance pitfalls resolved: column-specific selects, JOIN queries, FK indexes
- Maintainability improved: singleton client, generated types
- Detection commands for automated scanning of each pitfall
Error Handling
| Issue | Cause | Solution |
|---|
PGRST116: JSON object requested, multiple (or no) rows returned | Used .single() when 0 or 2+ rows match | Use .maybeSingle() for optional lookups |
data is null after insert | Missing .select() chain | Add .select('column1, column2') after .insert() |
TypeError: Cannot read property of null | Destructured only data, ignoring error | Always destructure { data, error } and check error first |
too many connections for role | Direct connection from serverless | Use pooled connection string (port 6543) |
permission denied for table | RLS blocking access, no matching policy | Check RLS policies match the authenticated user's JWT claims |
relation does not exist | Table name typo, not caught at compile time | Use generated types for compile-time validation |
Examples
Quick Security Audit
echo "=== Pitfall 1: Service role in client code ==="
grep -rn 'SERVICE_ROLE' --include="*.tsx" --include="*.ts" src/ app/ components/ 2>/dev/null || echo "Clean"
echo "=== Pitfall 2: Tables without RLS ==="
echo "=== Pitfall 3: Overly permissive policies ==="
Code Review Checklist
## Supabase PR Review Checklist
### Security
- [ ] No `SERVICE_ROLE_KEY` in client-side code or `NEXT_PUBLIC_*` vars
- [ ] RLS enabled on all new tables
- [ ] RLS policies scope to `auth.uid()` or org membership (no `USING (true)` for writes)
### Data Integrity
- [ ] All Supabase calls destructure `{ data, error }` and check error
- [ ] `.select()` chained after `.insert()`, `.update()`, `.upsert()`
- [ ] `.maybeSingle()` used for optional lookups (not `.single()`)
### Performance
- [ ] Column names specified in `.select()` (no `select('*')`)
- [ ] No N+1 patterns (use embedded joins or `.in()`)
- [ ] Foreign key columns have indexes
### Maintainability
- [ ] Single `createClient` instance (singleton pattern)
- [ ] Generated types used (not manual interfaces)
- [ ] Pooled connection string for serverless deployments
Resources
Next Steps
This completes the Supabase pitfalls reference. To start a new project with best practices from day one, see supabase-hello-world.