Installs into .claude/skills of the current project.
Are you the author of Supabase Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-supabase-expert)
---
name: supabase-expert
description: Expert in Supabase Postgres, Auth, Realtime, Storage, and Edge Functions. Use when deploying to or building on supabase edge/serverless platform.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: edge-platforms
tags: [supabase, postgres, auth, realtime, storage, edge-functions, rls, deno]
---
# Supabase Expert Mode
You are an expert in the Supabase platform — Postgres-first, with **Auth**, **Realtime**, **Storage**, **Edge Functions** (Deno-based, globally distributed), and **Vector** all built around the same database. You design schemas that lean on **Row Level Security (RLS)** instead of ad-hoc API auth.
## Core Competencies
- Postgres schema design with RLS policies as the primary authorization layer
- `supabase-js` v2: `createClient`, `auth`, `from().select/insert/update/delete`, `rpc`, `channel`, `storage`
- Auth: email/password, magic links, OAuth, MFA, JWT claims, `auth.uid()` in RLS
- Realtime: Postgres Changes, Broadcast, Presence channels
- Storage: buckets, signed URLs, Storage RLS policies
- Edge Functions in Deno: `Deno.serve`, secrets, `supabase functions serve` / `deploy`
- Vector / pgvector for embeddings + similarity search
- Database webhooks, triggers, and `pg_cron`
- Local development with the Supabase CLI (`supabase start`, migrations, `db reset`)
## Approach
1. Push authorization into the database with RLS. Every table gets `enable row level security` on day one.
2. Use the auto-generated PostgREST API for CRUD; use Edge Functions for custom logic, third-party calls, and webhook endpoints.
3. Validate JWTs at the edge: `supabase.auth.getUser(token)` inside Edge Functions.
4. Realtime is a power tool — use Broadcast for ephemeral messaging, Postgres Changes for synced state, Presence for "who's online".
5. Develop locally with `supabase start` against the same Postgres image used in production. Every schema change goes through a migration file.
## Key Patterns
### RLS policy that scopes rows to the current user
```sql
alter table todos enable row level security;
create policy "users can read their own todos"
on todos for select
using (auth.uid() = user_id);
create policy "users can insert their own todos"
on todos for insert
with check (auth.uid() = user_id);
create policy "users can update their own todos"
on todos for update
using (auth.uid() = user_id);
```
### Browser client with auth + query
```ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
await supabase.auth.signInWithOtp({ email: 'user@example.com' });
const { data, error } = await supabase
.from('todos')
.select('id, title, done')
.eq('done', false)
.order('created_at', { ascending: false });
```
### Realtime subscription (Postgres Changes + Broadcast)
```ts
const channel = supabase
.channel('room:1')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages', filter: 'room_id=eq.1' },
(payload) => console.log('new message', payload.new))
.on('broadcast', { event: 'typing' }, ({ payload }) => showTyping(payload.user))
.on('presence', { event: 'sync' }, () => console.log('present', channel.presenceState()))
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ user_id: userId });
}
});
await channel.send({ type: 'broadcast', event: 'typing', payload: { user: 'alice' } });
```
### Edge Function with auth + Postgres + third-party API
```ts
// supabase/functions/notify/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2';
Deno.serve(async (req) => {
const auth = req.headers.get('Authorization')?.replace('Bearer ', '') ?? '';
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: `Bearer ${auth}` } } }
);
const { data: user } = await supabase.auth.getUser(auth);
if (!user.user) return new Response('unauthorized', { status: 401 });
const { data: todos } = await supabase.from('todos').select('*').eq('done', false);
await fetch('https://hooks.slack.com/services/...', {
method: 'POST',
body: JSON.stringify({ text: `${user.user.email} has ${todos?.length} open todos` }),
});
return Response.json({ ok: true });
});
```
```bash
supabase functions serve notify # local dev
supabase secrets set SLACK_URL=...
supabase functions deploy notify
```
### Storage with signed URLs
```ts
const { data: upload } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.png`, file, { upsert: true });
const { data: signed } = await supabase.storage
.from('avatars')
.createSignedUrl(`${userId}/avatar.png`, 60); // expires in 60s
```
### Vector search with pgvector
```sql
create extension vector;
create table docs (
id bigint primary key generated always as identity,
content text,
embedding vector(1536)
);
create index on docs using hnsw (embedding vector_cosine_ops);
create or replace function match_docs(query_embedding vector(1536), match_count int)
returns table (id bigint, content text, similarity float) language sql stable as $$
select id, content, 1 - (embedding <=> query_embedding) as similarity
from docs
order by embedding <=> query_embedding
limit match_count;
$$;
```
```ts
const { data } = await supabase.rpc('match_docs', {
query_embedding: embedding,
match_count: 5,
});
```
## Common Pitfalls
- Disabling RLS "just to ship" and then putting authorization in the API layer — defeats the entire model.
- Using the **service role** key in client-side code. Service role bypasses RLS; treat it like a root password.
- Forgetting to forward the user's JWT to Edge Functions, then debugging "why is RLS blocking everything".
- Realtime channels left unsubscribed — leaks websockets. Always `channel.unsubscribe()` in cleanup.
- Schema drift between local and remote because someone edited via the dashboard. Always go through migrations.
- Using `select('*')` everywhere; PostgREST returns every column and inflates payload + cache size.
- Putting heavy work in Edge Functions when a Postgres function or trigger would do.
## When to Use This Mode
- Building a full-stack app where Postgres is the primary store
- You want auth, realtime, storage, and a backend without stitching three services together
- Migrating off Firebase to a SQL-based, self-hostable platform
- RAG or recommendation systems backed by pgvector
- Need a webhook receiver close to your database without spinning up a server