supabase
Supabase Auth, Realtime subscriptions, Edge Functions, Row Level Security, and Storage
SKILL.md.claude/skills/supabase/SKILL.md
GitHubSupabase Skill
Purpose
Build applications with Supabase as the backend platform. This skill covers authentication (email, OAuth, magic link), database access with Row Level Security, real-time subscriptions, file storage, and Edge Functions. Supabase is an open-source Firebase alternative built on PostgreSQL.
Key Concepts
Supabase Architecture
CLIENT (browser/app)
|
v
SUPABASE CLIENT SDK
|
+-- Auth -> GoTrue (JWT-based authentication)
+-- Database -> PostgREST (auto-generated REST API from PostgreSQL)
+-- Realtime -> Realtime server (WebSocket subscriptions)
+-- Storage -> S3-compatible object storage with RLS
+-- Functions -> Deno Edge Functions
SECURITY MODEL:
- Every request carries a JWT (from Auth)
- PostgREST passes JWT claims to PostgreSQL
- RLS policies use auth.uid() to check ownership
- Client can ONLY access data that RLS policies allow
Patterns
Client Setup
// lib/supabase/client.ts (browser client)
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
// lib/supabase/server.ts (server client for Next.js)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createServerSupabaseClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
},
);
}
Authentication
// Sign up with email
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword',
options: {
data: { full_name: 'Jane Doe' }, // Custom user metadata
},
});
// Sign in with email
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword',
});
// OAuth sign in
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
// Magic link
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
});
// Get current user
const { data: { user } } = await supabase.auth.getUser();
// Sign out
await supabase.auth.signOut();
Row Level Security (RLS)
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can read all published posts
CREATE POLICY "Public posts are viewable"
ON posts FOR SELECT
USING (published = true);
-- Users can only read their own drafts
CREATE POLICY "Users can view own drafts"
ON posts FOR SELECT
USING (auth.uid() = author_id AND published = false);
-- Users can insert their own posts
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Users can update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Users can delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
-- Admin can do anything (using custom claims)
CREATE POLICY "Admins have full access"
ON posts FOR ALL
USING (
(auth.jwt() -> 'user_metadata' ->> 'role') = 'admin'
);
-- Organization-based access
CREATE POLICY "Org members can access org data"
ON projects FOR SELECT
USING (
org_id IN (
SELECT org_id FROM memberships
WHERE user_id = auth.uid()
)
);
Realtime Subscriptions
// Subscribe to changes on a table
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT | UPDATE | DELETE | *
schema: 'public',
table: 'posts',
filter: 'author_id=eq.user_123', // Optional filter
},
(payload) => {
console.log('Change:', payload.eventType, payload.new);
// Update local state
},
)
.subscribe();
// Presence (who is online)
const room = supabase.channel('room-1');
room
.on('presence', { event: 'sync' }, () => {
const state = room.presenceState();
console.log('Online:', Object.keys(state));
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await room.track({ user_id: userId, name: userName });
}
});
// Cleanup
supabase.removeChannel(channel);
Database Queries
// Select with filters
const { data, error } = await supabase
.from('posts')
.select('id, title, content, author:users(name, avatar_url)')
.eq('published', true)
.order('created_at', { ascending: false })
.range(0, 19); // Pagination: first 20 rows
// Insert
const { data, error } = await supabase
.from('posts')
.insert({ title: 'New Post', content: 'Hello', author_id: userId })
.select()
.single();
// Update
const { data, error } = await supabase
.from('posts')
.update({ title: 'Updated Title' })
.eq('id', postId)
.select()
.single();
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId);
// RPC (call PostgreSQL functions)
const { data, error } = await supabase.rpc('search_posts', {
query: 'typescript',
limit_count: 10,
});
Storage
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/profile.jpg`, file, {
cacheControl: '3600',
upsert: true,
});
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/profile.jpg`);
// Download file
const { data, error } = await supabase.storage
.from('documents')
.download('report.pdf');
Best Practices
- Always enable RLS -- tables without RLS are publicly accessible via the API
- Use the server client for mutations -- server components have the service role key for admin operations
- Test RLS policies thoroughly -- write tests that verify unauthorized access is denied
- Use
auth.uid()in policies -- notcurrent_useror custom session variables - Create database functions for complex queries -- RPC calls are faster than chained client filters
- Unsubscribe from realtime channels -- prevent memory leaks in React useEffect cleanup
- Use storage policies -- apply RLS-like rules to file uploads and downloads
- Handle auth state changes -- use
onAuthStateChangeto react to sign-in/sign-out - Type your queries -- generate types with
supabase gen types typescript - Use migrations --
supabase db diffandsupabase migrationfor schema changes
Common Pitfalls
| Pitfall | Impact | Fix |
|---|---|---|
| RLS not enabled | Data publicly accessible | ALTER TABLE x ENABLE ROW LEVEL SECURITY |
| Using anon key server-side | Cannot bypass RLS for admin ops | Use service role key on server |
| Not cleaning up subscriptions | Memory leaks, stale listeners | removeChannel() in useEffect cleanup |
| Missing auth callback route | OAuth redirect fails | Create /auth/callback handler |
| Stale JWT on server | Unauthorized errors | Refresh session before server queries |
| No types generated | No type safety on queries | Run supabase gen types typescript |