Supabase is an open-source Firebase alternative built on top of PostgreSQL. It provides auth, realtime subscriptions, storage, and Edge Functions — all with a generous free tier. This cheat sheet covers everything you need to build production apps.
Quick reference
| Task | Code |
|---|---|
| Install JS client | npm i @supabase/supabase-js |
| Create client | createClient(url, anonKey) |
| Sign up | auth.signUp({ email, password }) |
| Sign in | auth.signInWithPassword({ email, password }) |
| Get user | auth.getUser() |
| Select rows | .from('table').select('*') |
| Insert row | .from('table').insert({ col: val }) |
| Update row | .from('table').update({ col: val }).eq('id', id) |
| Delete row | .from('table').delete().eq('id', id) |
| Upload file | storage.from('bucket').upload(path, file) |
| Subscribe | .channel('x').on('postgres_changes', ...) |
Installation and setup
npm install @supabase/supabase-js
// lib/supabase.ts
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)
# Python
from supabase import create_client
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
Server-side client (with service role key)
import { createClient } from '@supabase/supabase-js'
// Only use on the server — service role bypasses RLS
export const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
Authentication
Email + password
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secret123',
options: {
data: { full_name: 'Jane Doe' } // stored in user_metadata
}
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secret123'
})
// Sign out
await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Get session (includes access token)
const { data: { session } } = await supabase.auth.getSession()
OAuth (Google, GitHub, etc.)
// Redirect to OAuth provider
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
})
Auth state listener
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session?.user)
} else if (event === 'SIGNED_OUT') {
console.log('User signed out')
} else if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed')
}
})
Password reset
// Send reset email
await supabase.auth.resetPasswordForEmail('user@example.com', {
redirectTo: 'https://yourapp.com/reset-password'
})
// Update password (after user follows link)
await supabase.auth.updateUser({ password: 'newSecret123' })
Database queries
Supabase wraps PostgreSQL with a REST-like query builder. The JS client returns { data, error }.
SELECT
// All rows
const { data, error } = await supabase.from('posts').select('*')
// Specific columns
const { data } = await supabase
.from('posts')
.select('id, title, created_at')
// Join related table (foreign key)
const { data } = await supabase
.from('posts')
.select('id, title, author:profiles(name, avatar_url)')
// Many-to-many join
const { data } = await supabase
.from('posts')
.select('*, tags(name)')
// Count
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
Filtering
// Equality
.eq('status', 'published')
// Inequality
.neq('status', 'draft')
// Comparison
.gt('views', 100) // greater than
.gte('views', 100) // greater than or equal
.lt('views', 1000)
.lte('views', 1000)
// Range
.gte('price', 10).lte('price', 50)
// In / not in
.in('status', ['published', 'featured'])
.not('status', 'in', '("draft","archived")')
// Like / ilike (case-insensitive)
.ilike('title', '%supabase%')
// Is null / not null
.is('deleted_at', null)
.not('deleted_at', 'is', null)
// Contains (for arrays and JSONB)
.contains('tags', ['typescript'])
.containedBy('tags', ['typescript', 'nextjs'])
// Full text search
.textSearch('content', 'supabase tutorial', { type: 'websearch' })
// OR conditions
.or('status.eq.published,status.eq.featured')
Pagination and ordering
// Order by column
const { data } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
// Limit and offset
const { data } = await supabase
.from('posts')
.select('*')
.range(0, 9) // rows 0 to 9 (10 results)
// Cursor-based pagination using a timestamp
const { data } = await supabase
.from('posts')
.select('*')
.lt('created_at', lastCreatedAt)
.order('created_at', { ascending: false })
.limit(10)
INSERT, UPDATE, UPSERT, DELETE
// Insert single row
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', body: 'World', user_id: user.id })
.select() // return the inserted row
// Insert multiple rows
await supabase.from('tags').insert([
{ name: 'typescript' },
{ name: 'supabase' }
])
// Update
const { data, error } = await supabase
.from('posts')
.update({ title: 'Updated Title', updated_at: new Date().toISOString() })
.eq('id', postId)
.select()
// Upsert (insert or update on conflict)
await supabase.from('profiles').upsert(
{ id: user.id, full_name: 'Jane', updated_at: new Date().toISOString() },
{ onConflict: 'id' }
)
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
Raw SQL (RPC — calling a Postgres function)
// Call a stored procedure / Postgres function
const { data, error } = await supabase
.rpc('search_posts', { query_text: 'supabase', limit_count: 10 })
-- The Postgres function (defined in SQL editor or migration)
CREATE OR REPLACE FUNCTION search_posts(query_text text, limit_count int)
RETURNS TABLE(id uuid, title text, rank float)
LANGUAGE sql
AS $$
SELECT id, title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', query_text) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT limit_count;
$$;
Row Level Security (RLS)
RLS is Supabase's permission system. Enable it per table, then write policies.
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Read own posts only
CREATE POLICY "Users see own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Read all published posts
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
USING (status = 'published');
-- Insert only as authenticated user
CREATE POLICY "Authenticated users can insert"
ON posts FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Update own posts only
CREATE POLICY "Users update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Delete own posts only
CREATE POLICY "Users delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Admin bypass (use with service role key on server only)
CREATE POLICY "Service role has full access"
ON posts
USING (auth.role() = 'service_role');
Key functions inside RLS policies:
| Function | Description |
|---|---|
auth.uid() |
UUID of the current user |
auth.role() |
'anon', 'authenticated', or 'service_role' |
auth.jwt() |
Full JWT payload (access user_metadata, app_metadata) |
auth.email() |
Current user's email |
Storage
// Upload a file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${user.id}/avatar.png`, file, {
cacheControl: '3600',
upsert: true
})
// Get public URL (for public buckets)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${user.id}/avatar.png`)
// data.publicUrl = 'https://...supabase.co/storage/v1/object/public/avatars/...'
// Create signed URL (for private buckets, expires in 60 seconds)
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(`${user.id}/report.pdf`, 60)
// List files in a bucket
const { data, error } = await supabase.storage
.from('avatars')
.list(user.id, { limit: 20, sortBy: { column: 'created_at', order: 'desc' } })
// Download a file
const { data, error } = await supabase.storage
.from('documents')
.download(`${user.id}/report.pdf`)
// Delete a file
await supabase.storage.from('avatars').remove([`${user.id}/avatar.png`])
Storage bucket RLS (SQL)
-- Users can upload their own files
CREATE POLICY "Users upload own files"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);
-- Users can read their own files
CREATE POLICY "Users read own files"
ON storage.objects FOR SELECT
TO authenticated
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = auth.uid()::text);
Realtime 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)
// payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE'
// payload.new: new row data
// payload.old: old row data
}
)
.subscribe()
// Subscribe to specific events with filter
const channel = supabase
.channel('my-room')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`
},
(payload) => {
setMessages((prev) => [...prev, payload.new as Message])
}
)
.subscribe()
// Broadcast (custom events between clients)
const channel = supabase.channel('room-1')
// Send broadcast
await channel.send({
type: 'broadcast',
event: 'cursor-position',
payload: { x: 100, y: 200, userId: user.id }
})
// Receive broadcast
channel.on('broadcast', { event: 'cursor-position' }, (payload) => {
console.log('Cursor:', payload)
})
channel.subscribe()
// Unsubscribe
await supabase.removeChannel(channel)
Edge Functions
Edge Functions run on Deno at the edge, close to users.
// supabase/functions/hello/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'
Deno.serve(async (req) => {
const authHeader = req.headers.get('Authorization')!
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
})
}
const body = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${user.email}!`, data: body }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
# Deploy function
supabase functions deploy hello
# Invoke locally
supabase functions serve hello
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/hello' \
--header 'Authorization: Bearer <ACCESS_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{"name": "World"}'
CLI commands
# Install
npm install -g supabase
# Login
supabase login
# Init local project
supabase init
# Link to remote project
supabase link --project-ref <ref>
# Start local dev environment (Docker required)
supabase start
# Stop local environment
supabase stop
# Generate TypeScript types from DB schema
supabase gen types typescript --local > src/types/database.ts
# Create a new migration
supabase migration new add_profiles_table
# Apply migrations locally
supabase db push
# Pull remote schema changes
supabase db pull
# Reset local database
supabase db reset
# Inspect local DB
supabase db inspect
# View Edge Function logs
supabase functions logs hello
TypeScript type generation
Generate types once and get full autocomplete:
supabase gen types typescript --project-id <ref> > src/types/database.ts
// Use generated types
import { Database } from '@/types/database'
const supabase = createClient<Database>(url, key)
// Now queries are fully typed
const { data } = await supabase
.from('posts') // autocomplete table names
.select('id, title') // autocomplete column names
// data is typed as { id: string; title: string }[]
Next.js integration pattern
// lib/supabase/server.ts (Server Components)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createSupabaseServerClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name) { return cookieStore.get(name)?.value },
set(name, value, options) { cookieStore.set({ name, value, ...options }) },
remove(name, options) { cookieStore.set({ name, value: '', ...options }) }
}
}
)
}
// app/page.tsx (Server Component — no "use client")
export default async function Page() {
const supabase = createSupabaseServerClient()
const { data: posts } = await supabase
.from('posts')
.select('id, title')
.order('created_at', { ascending: false })
.limit(10)
return <PostList posts={posts ?? []} />
}
Common mistakes
| Mistake | Fix |
|---|---|
| Using anon key on the server with admin operations | Use SUPABASE_SERVICE_ROLE_KEY server-side only |
| Not enabling RLS on a table | Run ALTER TABLE t ENABLE ROW LEVEL SECURITY; |
Forgetting to check error in { data, error } |
Always check if (error) throw error |
Storing service role key in client-side env (NEXT_PUBLIC_) |
Only expose NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY |
Using supabase.from().select() without RLS policies |
All rows will be returned or blocked — write explicit policies |
| Not unsubscribing realtime channels on unmount | Call supabase.removeChannel(channel) in cleanup |
Calling createClient on every render |
Create client once per request (server) or as a singleton (client) |
Forgetting .select() after insert/update to get returned data |
Chain .select() to get the row back |
Supabase vs Firebase vs PlanetScale
| Feature | Supabase | Firebase | PlanetScale |
|---|---|---|---|
| Database | PostgreSQL | NoSQL (Firestore) | MySQL (serverless) |
| Open source | ✅ Yes | ❌ No | ❌ No |
| Self-hostable | ✅ Yes | ❌ No | ❌ No |
| Auth | ✅ Built-in | ✅ Built-in | ❌ None |
| Realtime | ✅ Postgres CDC | ✅ Firestore listeners | ❌ None |
| Storage | ✅ Built-in | ✅ Cloud Storage | ❌ None |
| Edge Functions | ✅ Deno | ✅ Cloud Functions | ❌ None |
| TypeScript types | ✅ Auto-generated | ⚠️ Manual | ✅ Auto-generated |
| SQL support | ✅ Full SQL | ❌ None | ✅ Full SQL |
| Free tier rows | Unlimited (500MB) | 1GB Firestore | 5GB |
FAQ
Q: Can I use Supabase with any framework?
Yes. Supabase has official clients for JavaScript/TypeScript, Python, Dart (Flutter), Swift, Kotlin, and C#. The REST API works with any HTTP client. The @supabase/ssr package handles cookies for Next.js, SvelteKit, Remix, Nuxt, and other SSR frameworks.
Q: What is the difference between anon and service_role keys?
The anon key is public and respects RLS policies — safe to expose in the browser. The service_role key bypasses RLS entirely and has full admin access. Never expose the service role key client-side. Use it only in server-side code (API routes, Edge Functions, cron jobs).
Q: How does Supabase realtime work?
Supabase uses PostgreSQL logical replication (the wal2json plugin) to stream database changes over WebSockets. You subscribe to postgres_changes events filtered by table, event type, and column values. Realtime also supports Broadcast (arbitrary events between clients) and Presence (online status tracking).
Q: How do I handle auth in Next.js App Router?
Use @supabase/ssr which provides createServerClient for Server Components and Route Handlers, and createBrowserClient for Client Components. The library manages session cookies automatically through middleware. Run supabase.auth.getUser() on the server — never rely on getSession() alone, which is unauthenticated.
Q: Can I use Supabase locally without internet?
Yes. supabase start spins up a complete local stack via Docker: Postgres, PostgREST, GoTrue (auth), Realtime, Storage, and the Studio dashboard at http://localhost:54323. Your local environment is an exact mirror of production.
Q: How do I migrate from Firebase to Supabase?
Export Firestore documents as JSON, then transform and import into Postgres tables. Migrate Firebase Auth users via the Supabase Admin API (supabase.auth.admin.createUser). Replace Firestore listeners with Supabase realtime subscriptions. The data model changes from document collections to relational tables, which enables SQL queries, joins, and transactions.