supabase
Full-stack backend with Postgres database
$ npx docs2skills add supabase-backendSupabase
Full-stack backend-as-a-service with PostgreSQL at its core
What this skill does
Supabase provides a complete backend stack built on PostgreSQL, replacing Firebase with open-source alternatives. It combines a full Postgres database with real-time subscriptions, user authentication, file storage, and globally distributed edge functions. Developers use Supabase to rapidly build full-stack applications without managing backend infrastructure.
The platform automatically generates REST and GraphQL APIs from your database schema, handles user authentication across multiple providers, provides real-time database change subscriptions, manages file storage with CDN distribution, and runs serverless functions at the edge. This eliminates the need to build and maintain separate backend services for common application needs.
Prerequisites
- Node.js 16+ for JavaScript/TypeScript projects
- Supabase account and project at https://supabase.com
- Project URL and anon key from Supabase dashboard
- For local development: Docker and Supabase CLI
Quick start
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = 'your-anon-key'
const supabase = createClient(supabaseUrl, supabaseKey)
// Insert data
const { data, error } = await supabase
.from('users')
.insert([{ name: 'John', email: 'john@example.com' }])
// Query data
const { data: users } = await supabase
.from('users')
.select('*')
.eq('active', true)
Core concepts
Database-first architecture: Your PostgreSQL schema drives the entire API surface. Table structure, RLS policies, and constraints automatically generate REST endpoints and real-time subscriptions.
Row Level Security (RLS): PostgreSQL policies control data access at the row level. Users can only see/modify data allowed by SQL policies, enforced at the database level.
Real-time subscriptions: Database changes trigger WebSocket events to subscribed clients. Insert, update, delete operations broadcast to listeners instantly.
Edge functions: Deno-based serverless functions deployed globally. Execute server-side logic closest to users with automatic scaling.
Unified client: Single JavaScript client handles database queries, authentication, storage, and real-time subscriptions with consistent API patterns.
Key API surface
// Database queries
supabase.from('table').select('*')
supabase.from('table').insert(data)
supabase.from('table').update(data).eq('id', 1)
supabase.from('table').delete().eq('id', 1)
// Authentication
supabase.auth.signUp({ email, password })
supabase.auth.signInWithPassword({ email, password })
supabase.auth.signInWithOAuth({ provider: 'github' })
supabase.auth.signOut()
supabase.auth.getUser()
// Real-time subscriptions
supabase.channel('channel-name').on('postgres_changes', {}, callback)
supabase.channel('room-1').on('broadcast', {}, callback)
// Storage
supabase.storage.from('bucket').upload(path, file)
supabase.storage.from('bucket').download(path)
supabase.storage.from('bucket').remove([path])
Common patterns
Authentication with protected routes
// Check auth state
const { data: { user } } = await supabase.auth.getUser()
// Listen for auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') console.log('User signed in:', session.user)
if (event === 'SIGNED_OUT') console.log('User signed out')
})
// Protected query with RLS
const { data } = await supabase
.from('private_notes')
.select('*') // Only returns user's own notes due to RLS policy
Real-time data sync
// Subscribe to table changes
const channel = supabase
.channel('todos-channel')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'todos'
}, (payload) => {
console.log('Change received!', payload)
updateUI(payload)
})
.subscribe()
// Broadcast to other clients
channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200 }
})
File upload with progress
const uploadFile = async (file) => {
const fileName = `${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName)
return publicUrl
}
Edge function with database access
// functions/hello/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data } = await supabase.from('users').select('count')
return new Response(JSON.stringify({ users: data }), {
headers: { 'Content-Type': 'application/json' }
})
})
Configuration
Environment variables
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Server-side only
Client options
const supabase = createClient(url, key, {
auth: {
persistSession: true,
autoRefreshToken: true,
detectSessionInUrl: true
},
realtime: {
params: {
eventsPerSecond: 10
}
}
})
Row Level Security policy example
-- Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Users can only see their own todos
CREATE POLICY "Users can view own todos" ON todos
FOR SELECT USING (auth.uid() = user_id);
-- Users can only insert their own todos
CREATE POLICY "Users can insert own todos" ON todos
FOR INSERT WITH CHECK (auth.uid() = user_id);
Best practices
- Enable RLS on all tables containing user data to prevent unauthorized access
- Use
select('*')sparingly - explicitly list needed columns for better performance - Implement database-level constraints rather than relying only on client validation
- Use service role key only server-side - never expose in client code
- Structure real-time channels by user/room to minimize unnecessary events
- Set up proper database indexes for frequently queried columns
- Use storage policies to control file access permissions
- Handle auth state changes to update UI when users sign in/out
- Implement proper error handling for network failures and rate limits
Gotchas and common mistakes
RLS policies don't apply to service role: Service role key bypasses all RLS policies. Use anon key for client-side operations.
Real-time requires proper grants: Users need SELECT permissions on tables to subscribe to changes. Grant access or subscriptions fail silently.
Auth state isn't immediately available: getUser() is async and may return null initially. Always check auth state before protected operations.
Storage bucket policies are separate: File access requires both storage policies AND bucket permissions. Both must allow the operation.
Edge functions timeout at 150 seconds: Long-running operations need to be split or moved to background jobs.
Database connections are limited: Each client maintains connection pools. Don't create multiple clients unnecessarily.
Real-time events can be duplicated: Network issues may cause duplicate events. Implement idempotent handlers.
Foreign key constraints affect RLS: Joining tables requires RLS policies on all referenced tables.
Local development needs Docker: Supabase CLI requires Docker to run local Postgres and services.
Auth redirects require exact URL matching: OAuth redirect URLs must match exactly in Supabase dashboard settings.
File uploads fail without CORS: Storage operations from browsers require proper CORS configuration.
Migration ordering matters: Database migrations must be applied in correct sequence to avoid dependency errors.