Supabase Skill
When to use
- Setting up or migrating a project to Supabase (Postgres + RLS + Auth + Storage + Realtime + Edge Functions)
- Designing Row Level Security (RLS) policies
- Writing or debugging Supabase Edge Functions (Deno/TypeScript)
- Configuring Supabase Auth (OAuth providers, magic links, custom claims)
- Querying with
supabase-js, PostgREST REST API, or direct SQL - Diagnosing JWT claim issues, RLS infinite recursion, or N+1 query patterns via PostgREST
Workflow
- Understand data shape first — sketch entity relationships and access patterns before touching the database. Identify which rows each user role may read/write.
- Create tables with
supabase migration new— never ALTER tables manually in the Supabase Dashboard in production; always use versioned SQL migrations insupabase/migrations/. - Enable RLS immediately —
ALTER TABLE <table> ENABLE ROW LEVEL SECURITY;as part of the same migration that creates the table. A table with RLS disabled is publicly readable via PostgREST unless blocked at the API gateway. - Write the minimal RLS policies needed — one policy per operation (SELECT, INSERT, UPDATE, DELETE) per role. Use
auth.uid()andauth.jwt()for user-scoped checks; use arolecolumn or a separatemembershipsjoin table for team/org scoping. - Test policies with
SET LOCAL role = authenticated; SET LOCAL "request.jwt.claims" = '{"sub":"<uuid>"}'in aBEGIN … ROLLBACKblock before deploying. - Add DB indexes for every FK column and every column referenced inside an RLS policy
USINGclause — the policy runs per-row and can cause sequential scans without an index. - Use generated columns or DB functions for computed fields rather than fetching raw rows and computing in application code.
- Edge Functions: scaffold with
supabase functions new <name>, keep business logic thin (validate input → call DB or external service → return JSON), and usesupabase.auth.getUser()from the service-role client only for admin paths. - Realtime: enable only the tables and events (INSERT / UPDATE / DELETE) actually needed. Filter subscriptions on the client side with
.eq('user_id', userId)to avoid broadcasting rows the subscriber cannot see via RLS. - Before production: run
supabase db lintand check the Supabase Dashboard → Advisors → Security for exposed tables and missing indexes.
Standards
Do
- Store secrets in Supabase Vault (
vault.secrets) or environment variables for Edge Functions — never in table columns. - Use
service_rolekey only in Edge Functions or server-side code that runs in a trusted environment; never ship it to client bundles. - Prefer
supabase-jsv2's typed client generated bysupabase gen types typescript. - Use
SECURITY DEFINERfunctions sparingly and only when escalating privilege is intentional (e.g., looking up another user's public profile); always setsearch_path = ''inside them. - Name migrations with the pattern
YYYYMMDDHHMMSS_<description>.sql. - Pin
supabase-jsand Deno SDK versions indeno.json/package.json.
Do not
- Do not call
supabase.auth.admin.*from client-side code. - Do not use the
publicschema as a catch-all; group tables into schemas (app,billing,internal) where the project grows beyond ~10 tables. - Do not disable RLS on a table that is accessible via the PostgREST API (anon or authenticated role).
- Do not write RLS policies that JOIN to the same table recursively without a
security barrierview as the intermediary. - Do not use
*in PostgREST selects when only a few columns are needed — over-fetching triggers RLS checks on unused columns and inflates response size.
Common mistakes to avoid
| Mistake | Consequence | Fix |
|---|---|---|
Forgetting ENABLE ROW LEVEL SECURITY |
Table is fully public via API | Add to migration immediately after CREATE TABLE |
RLS policy referencing auth.uid() on a table without a user_id index |
Full table scan on every request | CREATE INDEX ON table(user_id) |
Using service_role key in a Next.js/React bundle |
Full DB bypass exposed to users | Move to a server action or Edge Function |
Circular RLS: policy on profiles references memberships, which has a policy referencing profiles |
Infinite recursion → 500 error | Break the cycle with a SECURITY DEFINER helper function |
| Deploying Edge Functions that import large npm packages via esm.sh | Cold-start latency spikes | Bundle only what is needed; prefer Deno std lib |
| Not testing migrations locally before push | Breaking schema changes in prod | Use supabase db reset locally; test in a branch project |
| Storing user PII in Realtime broadcast payloads | Data leaks to subscribers | Filter columns in the Realtime publication or use server-side filtered channels |
Output format
Migrations are plain .sql files in supabase/migrations/. Example structure:
supabase/
migrations/
20240601120000_create_posts.sql
20240601120001_rls_posts.sql
functions/
send-notification/
index.ts
deno.json
seed.sql
config.toml
Edge Function response shape:
return new Response(JSON.stringify({ data, error: null }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
Related checklists
.claude/checklists/security.md.claude/checklists/database.md.claude/checklists/launch.md
Related agents
.claude/agents/engineering/backend-engineer.md.claude/agents/quality/security-auditor.md.claude/agents/core/system-analyst.md