| name | supabase-developer |
| description | Build full-stack applications with Supabase (PostgreSQL, Auth, Storage, Real-time, Edge Functions). Use when implementing authentication, database design with RLS, file storage, real-time features, or serverless functions. |
| source | https://github.com/daffy0208/ai-dev-standards/tree/main/SKILLS/supabase-developer |
Supabase Developer
Build production-ready full-stack applications with Supabase.
Supabase is an open-source Firebase alternative providing PostgreSQL database, authentication, storage, real-time subscriptions, and edge functions. This skill guides you through building secure, scalable applications using Supabase's full feature set.
When to Use This Skill
- Authentication: Implementing user signup/login with email, OAuth, magic links, or phone auth
- Database: Designing PostgreSQL schemas with Row Level Security (RLS)
- Storage: Managing file uploads, downloads, and access control
- Real-time: Building live features with subscriptions and broadcasts
- Edge Functions: Serverless TypeScript functions at the edge
- Migrations: Managing database schema changes
- Integration: Connecting Next.js, React, Vue, or other frameworks
Core Supabase Concepts
1. Database (PostgreSQL)
Supabase uses PostgreSQL with extensions:
- PostgREST: Auto-generates REST API from schema
- pg_graphql: Optional GraphQL support
- Extensions: pgvector for embeddings, pg_cron for scheduled jobs
2. Authentication
Built-in auth with multiple providers:
- Email/password with confirmation
- Magic links (passwordless)
- OAuth (Google, GitHub, etc.)
- Phone/SMS authentication
- SAML SSO (enterprise)
3. Row Level Security (RLS)
PostgreSQL policies that enforce data access at the database level:
- User can only read their own data
- Admin can read all data
- Public read, authenticated write
4. Storage
S3-compatible object storage with RLS:
- Public and private buckets
- File size and type restrictions
- Image transformations on the fly
- CDN integration
5. Real-time
WebSocket-based subscriptions:
- Database changes (INSERT, UPDATE, DELETE)
- Broadcast messages to channels
- Presence tracking (who's online)
6. Edge Functions
Deno-based serverless functions:
- Deploy globally at the edge
- TypeScript/JavaScript runtime
- Background jobs and webhooks
- Custom API endpoints
6-Phase Supabase Implementation
Phase 1: Project Setup & Configuration
Goal: Initialize Supabase project and connect to your application
1.1 Create Supabase Project
npx supabase init
npx supabase start
1.2 Install Client Libraries
npm install @supabase/supabase-js
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
1.3 Environment Configuration
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Server-side only!
1.4 Initialize Client
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Next.js 13+ App Router Pattern:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
}
}
}
)
}
Phase 2: Authentication Implementation
Goal: Secure user authentication with session management
2.1 Authentication Strategies
Email/Password Authentication:
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}
OAuth Authentication:
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) throw error
return data
}
Magic Link (Passwordless):
async function signInWithMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
2.2 Session Management
async function getSession() {
const {
data: { session },
error
} = await supabase.auth.getSession()
return session
}
async function getUser() {
const {
data: { user },
error
} = await supabase.auth.getUser()
return user
}
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
if (event === 'SIGNED_IN') {
}
if (event === 'SIGNED_OUT') {
}
if (event === 'TOKEN_REFRESHED') {
}
})
2.3 Protected Routes (Next.js)
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session }
} = await supabase.auth.getSession()
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
}
Phase 3: Database Design & RLS
Goal: Design secure database schema with Row Level Security
3.1 Schema Design
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE posts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE comments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX comments_post_id_idx ON comments(post_id);
3.2 Row Level Security (RLS) Policies
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
CREATE POLICY "Users can insert their own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true OR auth.uid() = user_id);
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
CREATE POLICY "Comments are viewable by everyone"
ON comments FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create comments"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own comments"
ON comments FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own comments"
ON comments FOR DELETE
USING (auth.uid() = user_id);
3.3 Database Functions
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER handle_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER handle_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, full_name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'username',
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
3.4 Querying with TypeScript
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My First Post',
content: 'Hello World!',
user_id: userId
})
.select()
.single()
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*), comments(*)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
const { data, error } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
const { error } = await supabase.from('posts').delete().eq('id', postId)
const { count } = await supabase.from('posts').select('*', { count: 'exact', head: true })
const { data } = await supabase.from('posts').select('*').textSearch('content', 'supabase', {
type: 'websearch',
config: 'english'
})
Phase 4: Storage Implementation
Goal: Manage file uploads with access control
4.1 Bucket Configuration
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
INSERT INTO storage.buckets (id, name, public)
VALUES ('private-docs', 'private-docs', false);
4.2 Storage RLS Policies
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can access their own documents"
ON storage.objects FOR SELECT
USING (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can upload their own documents"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
4.3 File Upload/Download
async function uploadFile(bucket: string, path: string, file: File) {
const { data, error } = await supabase.storage.from(bucket).upload(path, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data
}
async function downloadFile(bucket: string, path: string) {
const { data, error } = await supabase.storage.from(bucket).download(path)
if (error) throw error
return data
}
function getPublicUrl(bucket: string, path: string) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path)
return data.publicUrl
}
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
async function deleteFile(bucket: string, path: string) {
const { error } = await supabase.storage.from(bucket).remove([path])
if (error) throw error
}
async function listFiles(bucket: string, folder: string = '') {
const { data, error } = await supabase.storage.from(bucket).list(folder, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) throw error
return data
}
4.4 Image Transformations
function getTransformedImage(
bucket: string,
path: string,
options: {
width?: number
height?: number
quality?: number
}
) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path, {
transform: {
width: options.width,
height: options.height,
quality: options.quality || 80
}
})
return data.publicUrl
}
Phase 5: Real-time Features
Goal: Build live, collaborative features
5.1 Database Change Subscriptions
const subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
payload => {
console.log('New post created:', payload.new)
}
)
.subscribe()
supabase
.channel('comments-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'comments'
},
payload => {
console.log('Change detected:', payload)
}
)
.subscribe()
supabase
.channel('my-posts-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
payload => {
console.log('My post changed:', payload)
}
)
.subscribe()
subscription.unsubscribe()
5.2 Broadcast Messages
const channel = supabase.channel('room-1')
channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!', user: 'John' }
})
channel
.on('broadcast', { event: 'message' }, payload => {
console.log('Message received:', payload)
})
.subscribe()
5.3 Presence Tracking
const channel = supabase.channel('online-users', {
config: {
presence: {
key: userId
}
}
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async status => {
if (status === 'SUBSCRIBED') {
await channel.track({
user: userId,
online_at: new Date().toISOString()
})
}
})
await channel.track({ status: 'typing' })
await channel.untrack()
Phase 6: Edge Functions & Advanced Features
Goal: Serverless functions and advanced capabilities
6.1 Edge Functions
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async req => {
try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const {
data: { user }
} = await supabaseClient.auth.getUser(token)
const { data, error } = await supabaseClient.from('posts').select('*').eq('user_id', user?.id)
if (error) throw error
return new Response(JSON.stringify({ data }), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
})
Deploy Edge Function:
supabase functions deploy hello
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
6.2 Database Webhooks
CREATE OR REPLACE FUNCTION send_post_webhook()
RETURNS TRIGGER AS $$
BEGIN
PERFORM
net.http_post(
url := 'https://your-api.com/webhook',
headers := '{"Content-Type": "application/json"}'::jsonb,
body := jsonb_build_object(
'event', 'new_post',
'post_id', NEW.id,
'title', NEW.title
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_post_created
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION send_post_webhook();
6.3 Vector Search (pgvector)
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE posts ADD COLUMN embedding vector(1536);
CREATE INDEX ON posts USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
SELECT *
FROM posts
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
import OpenAI from 'openai'
const openai = new OpenAI()
async function addEmbedding(postId: string, text: string) {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text
})
const embedding = response.data[0].embedding
await supabase.from('posts').update({ embedding }).eq('id', postId)
}
async function semanticSearch(query: string) {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query
})
const queryEmbedding = response.data[0].embedding
const { data } = await supabase.rpc('match_posts', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 10
})
return data
}
Database Migration Management
Local Development Workflow
supabase init
supabase start
supabase migration new add_posts_table
supabase db reset
supabase gen types typescript --local > types/supabase.ts
Production Deployment
supabase link --project-ref your-project-ref
supabase db push
supabase db remote commit
Security Best Practices
1. Never Expose Service Role Key
const supabase = createClient(url, SERVICE_ROLE_KEY)
const supabase = createClient(url, ANON_KEY)
const supabase = createClient(url, SERVICE_ROLE_KEY)
2. Always Use RLS
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
secret TEXT
);
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
secret TEXT
);
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their data"
ON sensitive_data FOR ALL
USING (auth.uid() = user_id);
3. Validate Input
await supabase.from('posts').insert({ title: userInput })
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10).max(10000)
})
const validated = schema.parse(userInput)
await supabase.from('posts').insert(validated)
4. Rate Limiting
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { count } = await supabase
.from('api_calls')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.gte('created_at', oneHourAgo)
if (count >= 100) {
return new Response('Rate limit exceeded', { status: 429 })
}
Performance Optimization
1. Use Indexes
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_user_published_idx ON posts(user_id, published);
CREATE INDEX posts_content_fts_idx ON posts USING gin(to_tsvector('english', content));
2. Select Only What You Need
const { data } = await supabase.from('posts').select('*')
const { data } = await supabase.from('posts').select('id, title, created_at')
3. Use Pagination
const { data } = await supabase.from('posts').select('*').range(0, 9)
const { data } = await supabase
.from('posts')
.select('*')
.gt('created_at', lastCreatedAt)
.order('created_at', { ascending: false })
.limit(10)
4. Cache Static Data
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data } = await supabase.from('posts').select('*')
return data
},
staleTime: 5 * 60 * 1000
})
}
Testing
Unit Tests
import { createClient } from '@supabase/supabase-js'
jest.mock('@supabase/supabase-js', () => ({
createClient: jest.fn(() => ({
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => ({
single: jest.fn(() =>
Promise.resolve({
data: { id: '1', title: 'Test' },
error: null
})
)
}))
}))
}))
}))
}))
test('fetches post by id', async () => {
const post = await getPostById('1')
expect(post.title).toBe('Test')
})
Integration Tests
const testSupabase = createClient(
process.env.TEST_SUPABASE_URL!,
process.env.TEST_SUPABASE_ANON_KEY!
)
beforeEach(async () => {
await testSupabase.from('posts').delete().neq('id', '00000000-0000-0000-0000-000000000000')
})
test('creates post', async () => {
const { data, error } = await testSupabase
.from('posts')
.insert({ title: 'Test Post', content: 'Content' })
.select()
.single()
expect(error).toBeNull()
expect(data.title).toBe('Test Post')
})
Common Patterns
1. Optimistic UI Updates
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async newPost => {
const { data } = await supabase.from('posts').insert(newPost).select().single()
return data
},
onMutate: async newPost => {
await queryClient.cancelQueries({ queryKey: ['posts'] })
const previousPosts = queryClient.getQueryData(['posts'])
queryClient.setQueryData(['posts'], old => [...old, newPost])
return { previousPosts }
},
onError: (err, newPost, context) => {
queryClient.setQueryData(['posts'], context.previousPosts)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
}
2. Soft Deletes
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
CREATE POLICY "Only show non-deleted posts"
ON posts FOR SELECT
USING (deleted_at IS NULL);
CREATE OR REPLACE FUNCTION soft_delete_post(post_id UUID)
RETURNS void AS $$
BEGIN
UPDATE posts
SET deleted_at = NOW()
WHERE id = post_id AND user_id = auth.uid();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
3. Audit Logs
CREATE TABLE audit_logs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
auth.uid()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER audit_posts
AFTER INSERT OR UPDATE OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION audit_trigger();
Troubleshooting
Issue: RLS Policies Not Working
Symptom: Can't query data even with correct policies
Solution:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
SELECT * FROM pg_policies WHERE tablename = 'your_table';
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims.sub TO 'user-uuid';
SELECT * FROM your_table;
Issue: "JWT expired" Errors
Solution:
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
}
})
const { data, error } = await supabase.auth.refreshSession()
Issue: Storage Upload Fails
Solution:
const { data: buckets } = await supabase.storage.listBuckets()
Issue: Real-time Not Working
Solution:
ALTER PUBLICATION supabase_realtime ADD TABLE posts;
SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
Quick Reference
Essential Commands
supabase init
supabase start
supabase stop
supabase status
supabase migration new migration_name
supabase db reset
supabase db push
supabase gen types typescript --local > types/supabase.ts
supabase functions new function_name
supabase functions serve
supabase functions deploy function_name
supabase link --project-ref your-ref
Common Queries
const { data } = await supabase.from('table').select('*')
const { data } = await supabase.from('table').insert({ ... })
const { data } = await supabase.from('table').update({ ... }).eq('id', id)
const { data } = await supabase.from('table').delete().eq('id', id)
.eq('column', value)
.neq('column', value)
.gt('column', value)
.gte('column', value)
.lt('column', value)
.lte('column', value)
.like('column', '%pattern%')
.ilike('column', '%pattern%')
.is('column', null)
.in('column', [1, 2, 3])
.contains('array_column', ['value'])
.textSearch('column', 'query')
.order('column', { ascending: false })
.limit(10)
.range(0, 9)
.single()
.maybeSingle()
Integration Examples
Next.js 13+ App Router
See Phase 1 for client/server setup patterns.
React + Vite
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
export function useAuth() {
const [user, setUser] = useState(null)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
})
const {
data: { subscription }
} = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
})
return () => subscription.unsubscribe()
}, [])
return { user }
}
Summary
This skill covers the complete Supabase development lifecycle:
- ✅ Setup: Project initialization and client configuration
- ✅ Auth: Multiple authentication strategies with session management
- ✅ Database: PostgreSQL schema design with Row Level Security
- ✅ Storage: File management with access control
- ✅ Real-time: Live subscriptions, broadcasts, and presence
- ✅ Edge Functions: Serverless TypeScript functions
- ✅ Security: Best practices for production applications
- ✅ Performance: Optimization strategies for scale
- ✅ Testing: Unit and integration testing patterns
- ✅ Migration: Database change management
Key Takeaway: Supabase provides a complete backend platform with PostgreSQL at its core. Row Level Security is your primary security layer—design RLS policies carefully to ensure data is secure by default.
For complex scenarios, combine this skill with:
- api-designer for custom API endpoints
- security-engineer for advanced security reviews
- performance-optimizer for scaling large applications
- data-engineer for complex data pipelines