Supabase Patterns
Quick Guide: Use Supabase as your backend-as-a-service for Postgres database, authentication, realtime subscriptions, file storage, and edge functions. Always use the typed client with Database generic, enable RLS on every table, and use the secret key only on the server.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
(You MUST enable Row Level Security (RLS) on EVERY table in an exposed schema — no exceptions)
(You MUST use the Database generic type with createClient<Database>() for type-safe queries)
(You MUST NEVER expose the secret key in client-side code — use the publishable key in browsers, the secret key only on the server)
(You MUST use (select auth.uid()) wrapped in a subquery inside RLS policies for performance)
(You MUST handle all Supabase responses with { data, error } destructuring — never assume success)
</critical_requirements>
Auto-detection: Supabase, createClient, @supabase/supabase-js, @supabase/ssr, supabase-js, auth.uid(), RLS, row level security, realtime, postgres_changes, supabase.auth, supabase.from, supabase.storage, supabase.functions, supabase.channel, edge function, Deno.serve
When to use:
- Setting up a Supabase client with TypeScript type safety
- Implementing authentication (email/password, OAuth, magic links, session management)
- Querying Postgres via the Supabase client (select, insert, update, delete, RPC)
- Writing Row Level Security policies for data access control
- Subscribing to database changes in real time
- Uploading and serving files from Supabase Storage
- Building serverless functions with Supabase Edge Functions (Deno)
Key patterns covered:
- Typed client setup with
Databasegeneric and environment variables - Auth flows: sign up, sign in, OAuth, magic link, session refresh,
onAuthStateChange - Database queries with filters, joins, RPC calls, and error handling
- RLS policies:
USINGvsWITH CHECK,auth.uid(), role-based access - Realtime subscriptions via
channel().on('postgres_changes') - Storage: upload, signed URLs, public URLs, bucket policies
- Edge Functions:
Deno.serve, CORS headers, secrets, Supabase client in functions
When NOT to use:
- Direct Postgres connections (use a database driver skill instead)
- Complex server-side ORM patterns (use a dedicated ORM skill)
- Non-Supabase authentication providers (use dedicated auth skills)
Detailed Resources:
- For decision frameworks and anti-patterns, see [reference.md](reference.md)
Client & Queries:
- [examples/core.md](examples/core.md) — Client setup, typed queries, error handling patterns
Authentication:
- [examples/auth.md](examples/auth.md) — Full auth flows, OAuth, magic links, session refresh, middleware protection
Database:
- [examples/database.md](examples/database.md) — Complex queries, joins, RPC, migrations, type generation
Storage:
- [examples/storage.md](examples/storage.md) — File upload, signed URLs, bucket policies, image transforms
Edge Functions:
- [examples/edge-functions.md](examples/edge-functions.md) — Deno edge functions,
Deno.serve(), CORS, secrets
<philosophy>
Philosophy
Supabase is an open-source Firebase alternative built on Postgres. It provides a complete backend through a combination of Postgres extensions, auto-generated REST/GraphQL APIs, authentication, realtime subscriptions, file storage, and edge functions.
Core principles:
- Postgres at the core — Every feature is built on Postgres. RLS policies, auth, and realtime all leverage Postgres primitives. Understanding Postgres is understanding Supabase.
- Type safety end-to-end — Generate TypeScript types from your database schema with
supabase gen types. Pass theDatabasegeneric tocreateClientfor fully typed queries. - Security by default — RLS must be enabled on every table. The publishable key is safe for browsers (RLS enforces access). The secret key bypasses RLS and must never leave the server.
- Error as values — Every Supabase method returns
{ data, error }. Never assume success. Always checkerrorbefore usingdata. - Realtime built in — Postgres changes stream over WebSockets via channels. No separate pub/sub infrastructure needed.
- Edge-first functions — Edge Functions run Deno at the edge, close to users. Design for short-lived, idempotent operations.
When to use Supabase:
- Rapid backend development with Postgres, auth, and storage out of the box
- Projects needing realtime features (chat, notifications, live dashboards)
- Teams wanting to avoid managing separate auth, database, and storage services
- Applications that benefit from Row Level Security for multi-tenant data isolation
When NOT to use:
- Complex server-side business logic requiring a full application server (use Edge Functions for simple cases, a dedicated API for complex ones)
- Applications needing an ORM with advanced query building (Supabase query builder is powerful but not a full ORM)
- Offline-first applications requiring complex sync protocols
</philosophy>
<patterns>
Core Patterns
Pattern 1: Typed Client Setup
Always pass the Database generic to createClient for full autocomplete on table names, column names, and return types. Use environment variables for URL and keys.
export const supabase = createClient<Database>(
SUPABASE_URL,
SUPABASE_PUBLISHABLE_KEY,
);
Without the generic, typos in table/column names are not caught at compile time. See [examples/core.md](examples/core.md) for browser, server, and admin client setup patterns.
Pattern 2: Error Handling with { data, error }
Every Supabase method returns { data, error }. Always destructure and check error before using data. Never use non-null assertions on data.
const { data, error } = await supabase
.from("profiles")
.select("id, username")
.eq("id", userId)
.single();
if (error) throw new Error(`Failed to fetch profile: ${error.message}`);
See [examples/core.md](examples/core.md) for the reusable error handler pattern and common mistakes.
Pattern 3: Authentication Flows
Supabase Auth supports email/password (signInWithPassword), OAuth (signInWithOAuth), magic links (signInWithOtp), and phone OTP. Register onAuthStateChange early in the app lifecycle and always clean up with subscription.unsubscribe().
Key gotcha: Do NOT call Supabase methods directly inside onAuthStateChange — use setTimeout(..., 0) to defer.
See [examples/auth.md](examples/auth.md) for sign up, sign in, OAuth, magic link, session management, middleware protection, and password reset patterns.
Pattern 4: Database Queries
Use the query builder for type-safe CRUD with filters, joins, ordering, and pagination. Always add .select() after .insert() or .update() to return the affected row.
const { data, error } = await supabase
.from("posts")
.select("id, title, author:profiles(username)")
.eq("published", true)
.order("created_at", { ascending: false })
.range(0, PAGE_SIZE - 1);
See [examples/database.md](examples/database.md) for complex queries, upserts, RPC calls, conditional filters, counting, and migrations.
Pattern 5: Row Level Security (RLS) Policies
RLS is the primary security mechanism. Enable it on every table, write separate policies per operation (not FOR ALL), and wrap auth.uid() in a subquery for performance.
alter table public.posts enable row level security;
create policy "posts_select" on public.posts for select to authenticated
using ( published = true or (select auth.uid()) = author_id );
Never trust user_metadata from JWT for access control — it is user-modifiable. See [examples/da