Supabase Architecture
Supabase wraps PostgreSQL with real-time subscriptions, REST/GraphQL APIs, auth, storage, and edge functions — all open-source.
Setup
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
Row Level Security (RLS)
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: users can only see their own posts
CREATE POLICY "Users see own posts" ON posts
FOR SELECT USING (auth.uid() = user_id);
-- Policy: users can create posts
CREATE POLICY "Users create posts" ON posts
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy: users can update own posts
CREATE POLICY "Users update own posts" ON posts
FOR UPDATE USING (auth.uid() = user_id);
-- Policy: public posts are visible to everyone
CREATE POLICY "Public posts visible" ON posts
FOR SELECT USING (published = true);
Authentication
// Email/password
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: { display_name: 'Alice' },
},
})
// Social OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: `${window.location.origin}/auth/callback` },
})
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Auth state listener
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
setUser(session?.user ?? null)
} else if (event === 'SIGNED_OUT') {
setUser(null)
}
})
Database Queries
// Type-safe with generated types
type Database = {
public: {
Tables: {
posts: {
Row: { id: string; title: string; content: string; user_id: string }
Insert: { title: string; content: string }
Update: Partial<{ title: string; content: string }>
}
}
}
}
const supabase = createClient<Database>(URL, KEY)
// CRUD operations
const { data: posts, error } = await supabase
.from('posts')
.select(`
id,
title,
content,
created_at,
user:users(id, name, avatar_url)
`)
.eq('published', true)
.order('created_at', { ascending: false })
.range(0, 19)
// Insert
const { data: post } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
// Update
const { data } = await supabase
.from('posts')
.update({ title: 'Updated' })
.eq('id', postId)
.select()
.single()
Real-time Subscriptions
// Subscribe to all changes on a table
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change:', payload.eventType, payload.new)
}
)
.subscribe()
// Subscribe to specific rows
const channel = supabase
.channel('my-post')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: `id=eq.${postId}`,
},
(payload) => setPost(payload.new)
)
.subscribe()
// Cleanup
return () => supabase.removeChannel(channel)
Storage
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.jpg`, file, {
cacheControl: '3600',
upsert: true,
contentType: 'image/jpeg',
})
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.jpg`)
// Signed URL (for private buckets)
const { data: { signedUrl } } = await supabase.storage
.from('private-docs')
.createSignedUrl('document.pdf', 3600) // 1 hour expiry
Edge Functions
// supabase/functions/send-welcome/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
serve(async (req) => {
const { email, name } = await req.json()
await sendEmail({ to: email, subject: 'Welcome!', body: `Hello ${name}` })
return new Response(JSON.stringify({ sent: true }), {
headers: { 'Content-Type': 'application/json' },
})
})
supabase functions deploy send-welcome
supabase functions invoke send-welcome --body '{"email":"user@example.com","name":"Alice"}'