Supabase: Open Source Firebase Alternative
Set up Supabase, the open source PostgreSQL-based backend platform with REST/GraphQL APIs, real-time subscriptions, authentication, and storage.
- Step 1
Architecture Overview
Supabase is a modular Backend-as-a-Service built on PostgreSQL. The platform consists of several microservices that work together: PostgREST for auto-generated REST APIs, GoTrue for authentication, Realtime for WebSocket subscriptions, Storage API for file storage, Kong as an API gateway, and pg_graphql for GraphQL support. Each component is open source and can be self-hosted or used via Supabase's cloud offering.
- Step 2
Quick Start with Supabase Cloud
The fastest way to get started is using Supabase's hosted platform. Create a free account and provision a new project. You'll get a dedicated PostgreSQL database, API endpoints, and authentication configured automatically.
# Visit https://supabase.com/dashboard and: # 1. Sign up / log in # 2. Click 'New project' # 3. Choose organization, name, password, and region # 4. Wait ~2 minutes for provisioning # Your project URL and keys will be available in Settings > API - Step 3
Install the Supabase CLI
The Supabase CLI enables local development with a full Supabase stack running in Docker. It manages migrations, generates TypeScript types, and syncs with your cloud project.
# Install via npm (requires Node.js 18+) npm install -g supabase # Or via Homebrew on macOS brew install supabase/tap/supabase # Or via Scoop on Windows scoop bucket add supabase https://github.com/supabase/scoop-bucket.git scoop install supabase # Verify installation supabase --version - Step 4
Initialize a local Supabase project
Initialize Supabase in your project directory. This creates a
supabase/folder with configuration files and a migrations directory.# Navigate to your project root cd your-project # Initialize Supabase (creates supabase/ directory) supabase init # Link to your cloud project (optional, for syncing) supabase link --project-ref <your-project-ref> - Step 5
Start the local Supabase stack
Start all Supabase services locally using Docker. This spins up PostgreSQL, PostgREST, GoTrue, Realtime, Storage, Kong gateway, and the Supabase Studio UI.
# Start all services (requires Docker) supabase start # Services will be available at: # - API: http://localhost:54321 # - Studio: http://localhost:54323 # - Database: postgresql://postgres:postgres@localhost:54322/postgres # Stop services when done supabase stop - Step 6
Create your first table with RLS
Create a migration file and define a table with Row Level Security (RLS) enabled. RLS policies control data access at the row level based on user identity.
-- Generate a new migration -- supabase migration new create_todos -- In the generated migration file: create table todos ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users(id) on delete cascade not null, title text not null, is_complete boolean default false, created_at timestamptz default now() ); -- Enable RLS alter table todos enable row level security; -- Policy: users can only see their own todos create policy "Users can view their own todos" on todos for select using (auth.uid() = user_id); -- Policy: users can insert their own todos create policy "Users can insert their own todos" on todos for insert with check (auth.uid() = user_id); -- Policy: users can update their own todos create policy "Users can update their own todos" on todos for update using (auth.uid() = user_id); - Step 7
Apply migrations
Run your migrations to update the local database schema. The CLI tracks which migrations have been applied via the
supabase_migrations.schema_migrationstable.# Apply all pending migrations to local DB supabase db push # Reset local DB from scratch (useful for development) supabase db reset # Push migrations to linked cloud project supabase db push --linked - Step 8
Install the Supabase JavaScript client
Install the official Supabase JavaScript client library for interacting with your database, auth, storage, and real-time subscriptions from the browser or Node.js.
npm install @supabase/supabase-js - Step 9
Initialize the Supabase client
Create a client instance with your project URL and anon key. The anon key is safe to expose in browser code — Row Level Security policies enforce access control.
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) - Step 10
Query data with the Supabase client
Use the auto-generated REST API through the JavaScript client. All CRUD operations respect your RLS policies — users automatically only see/modify their own data.
// Fetch all todos for the authenticated user const { data: todos, error } = await supabase .from('todos') .select('*') .order('created_at', { ascending: false }) // Insert a new todo const { data, error } = await supabase .from('todos') .insert({ title: 'Learn Supabase', user_id: user.id }) .select() .single() // Update a todo const { error } = await supabase .from('todos') .update({ is_complete: true }) .eq('id', todoId) // Delete a todo const { error } = await supabase .from('todos') .delete() .eq('id', todoId) - Step 11
Set up authentication
Supabase Auth (powered by GoTrue) supports multiple providers: email/password, magic links, OAuth (GitHub, Google, etc.), and phone auth. Configure providers in the Supabase dashboard.
// Sign up with email and password const { data, error } = await supabase.auth.signUp({ email: 'user@example.com', password: 'secure-password' }) // Sign in with email and password const { data, error } = await supabase.auth.signInWithPassword({ email: 'user@example.com', password: 'secure-password' }) // Sign in with OAuth (GitHub example) const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github' }) // Get current user session const { data: { session } } = await supabase.auth.getSession() // Sign out const { error } = await supabase.auth.signOut() - Step 12
Listen to auth state changes
Subscribe to authentication state changes to update your UI when users sign in or out. The listener fires whenever the session changes.
// Set up 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('Session token refreshed') } }) // The listener returns an unsubscribe function const { data: { subscription } } = supabase.auth.onAuthStateChange( (event, session) => { /* ... */ } ) // Clean up when component unmounts subscription.unsubscribe() - Step 13
Subscribe to real-time database changes
Enable real-time subscriptions on your tables to receive instant updates when data changes. Supabase Realtime uses PostgreSQL's replication feature and WebSockets.
// Subscribe to all changes on the todos table const channel = supabase .channel('todos-changes') .on( 'postgres_changes', { event: '*', // 'INSERT', 'UPDATE', 'DELETE', or '*' for all schema: 'public', table: 'todos' }, (payload) => { console.log('Change detected:', payload) // payload.eventType: 'INSERT' | 'UPDATE' | 'DELETE' // payload.new: new row data (for INSERT/UPDATE) // payload.old: old row data (for UPDATE/DELETE) } ) .subscribe() // Filter changes by specific criteria const userChannel = supabase .channel('my-todos') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'todos', filter: `user_id=eq.${userId}` }, (payload) => { /* ... */ } ) .subscribe() // Unsubscribe when done channel.unsubscribe()⚠ Heads up: Real-time requires replication to be enabled on your table. Run `alter table todos replica identity full;` in your migration. - Step 14
Upload and manage files with Storage
Supabase Storage provides S3-compatible object storage with access control tied to your RLS policies. Create buckets and upload files from the client.
// Upload a file to a bucket const file = event.target.files[0] const { data, error } = await supabase.storage .from('avatars') .upload(`public/${user.id}/avatar.png`, file, { cacheControl: '3600', upsert: true }) // Get public URL for a file const { data: publicUrlData } = supabase.storage .from('avatars') .getPublicUrl(`public/${user.id}/avatar.png`) const publicUrl = publicUrlData.publicUrl // Download a file const { data: blob, error } = await supabase.storage .from('avatars') .download(`public/${user.id}/avatar.png`) // List files in a folder const { data: files, error } = await supabase.storage .from('avatars') .list(`public/${user.id}`, { limit: 100, offset: 0, sortBy: { column: 'name', order: 'asc' } }) // Delete files const { data, error } = await supabase.storage .from('avatars') .remove([`public/${user.id}/avatar.png`]) - Step 15
Create storage buckets with RLS policies
Storage buckets can have custom access policies. Create buckets via the dashboard or SQL, then define policies similar to table RLS.
-- Create a storage bucket insert into storage.buckets (id, name, public) values ('avatars', 'avatars', false); -- Policy: users can upload to their own folder 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] ); -- Policy: anyone can view avatars create policy "Avatars are publicly accessible" on storage.objects for select using (bucket_id = 'avatars'); -- Policy: users can update their own avatar 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] ); - Step 16
Use Database Functions and Triggers
PostgreSQL functions and triggers give you server-side logic that runs automatically on data changes. Common use cases: denormalized counters, computed columns, audit logs, and validation.
-- Example: Auto-update a 'updated_at' timestamp create or replace function update_updated_at_column() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; create trigger update_todos_updated_at before update on todos for each row execute function update_updated_at_column(); -- Example: Increment a counter on insert create or replace function increment_todo_count() returns trigger language plpgsql security definer set search_path = public as $$ begin update profiles set todo_count = todo_count + 1 where id = new.user_id; return new; end; $$; create trigger increment_todo_count_trigger after insert on todos for each row execute function increment_todo_count();⚠ Heads up: Always use `security definer set search_path = public` on trigger functions to prevent privilege escalation attacks. - Step 17
Generate TypeScript types from your schema
Auto-generate TypeScript definitions for type-safe queries. The CLI introspects your database schema and outputs a types file.
# Generate types from local database supabase gen types typescript --local > types/supabase.ts # Generate types from linked cloud project supabase gen types typescript --linked > types/supabase.ts - Step 18
Use generated types with the client
Import the generated types and pass them to the client for full type safety on queries, inserts, and updates.
import { createClient } from '@supabase/supabase-js' import type { Database } from './types/supabase' const supabase = createClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) // Now queries are fully typed const { data: todos } = await supabase .from('todos') // 'todos' is autocompleted .select('*') // return type is inferred as Todo[] // Insert with type checking const { error } = await supabase .from('todos') .insert({ title: 'Typed todo', user_id: userId, is_complete: false }) // TypeScript errors if fields are missing/wrong type - Step 19
Call PostgreSQL functions (RPC)
Expose custom PostgreSQL functions as API endpoints. Useful for complex queries, aggregations, or operations that don't map to simple CRUD.
-- Create a function in your migration create or replace function get_completed_todos_count(p_user_id uuid) returns bigint language sql security definer set search_path = public as $$ select count(*) from todos where user_id = p_user_id and is_complete = true; $$; - Step 20
Call RPC from the client
Invoke PostgreSQL functions from the client using
.rpc(). Parameters are type-checked if you're using generated types.// Call the function from the client const { data, error } = await supabase .rpc('get_completed_todos_count', { p_user_id: userId }) if (!error) { console.log('Completed todos:', data) } - Step 21
Enable full-text search
PostgreSQL's built-in full-text search is powerful. Add a generated
tsvectorcolumn and a GIN index for fast text queries.-- Add a search vector column alter table todos add column search_vector tsvector generated always as ( to_tsvector('english', coalesce(title, '')) ) stored; -- Create GIN index for fast search create index todos_search_idx on todos using gin(search_vector); -- Query from the client -- Use .textSearch() for type-safe full-text queries const { data } = await supabase .from('todos') .select('*') .textSearch('search_vector', 'learn & supabase') - Step 22
Self-host Supabase with Docker
Run the entire Supabase stack locally or in production using Docker Compose. Clone the official repo and start all services.
# Clone the Supabase repository git clone --depth 1 https://github.com/supabase/supabase cd supabase/docker # Copy example env file and customize cp .env.example .env # Edit .env to set passwords, JWT secrets, etc. # Start all services docker compose up -d # Services will be available at: # - Studio: http://localhost:3000 # - API: http://localhost:8000 # - Database: postgresql://postgres:your-password@localhost:5432/postgres⚠ Heads up: Self-hosting requires managing secrets, backups, scaling, and security updates. Supabase Cloud handles all of this automatically. - Step 23
Use Edge Functions for server-side logic
Supabase Edge Functions (powered by Deno Deploy) let you run custom server-side code globally. Write TypeScript functions that run on every request.
# Create a new edge function supabase functions new my-function # Serve functions locally for development supabase functions serve # Deploy function to cloud project supabase functions deploy my-function - Step 24
Write an Edge Function
Edge Functions use Deno runtime with access to the Supabase client. Great for webhooks, scheduled jobs, or custom API endpoints.
// supabase/functions/my-function/index.ts 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) => { const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', { global: { headers: { Authorization: req.headers.get('Authorization')! }, }, } ) // Your custom logic here const { data: todos } = await supabaseClient .from('todos') .select('*') return new Response(JSON.stringify({ todos }), { headers: { 'Content-Type': 'application/json' }, }) }) - Step 25
Invoke Edge Functions from the client
Call your deployed Edge Functions from the client using
.functions.invoke(). Pass headers, body, and query parameters.// Invoke the function const { data, error } = await supabase.functions.invoke('my-function', { body: { message: 'Hello from client' }, headers: { 'Content-Type': 'application/json' } }) if (!error) { console.log('Function response:', data) } - Step 26
Monitor with built-in observability
Supabase Cloud includes logs, metrics, and query performance insights. Access them via the dashboard under Logs, Database > Query Performance, and API > Logs.
# View logs in real-time (CLI) supabase logs --project-ref <your-project-ref> # Filter by service supabase logs --project-ref <your-project-ref> --service postgres # Or access via dashboard: # Dashboard > Logs > Filter by service/level/timestamp - Step 27
Use GraphQL (pg_graphql)
Supabase includes native GraphQL support via the
pg_graphqlextension. Query your database using GraphQL instead of REST if you prefer.# GraphQL endpoint: https://<project-ref>.supabase.co/graphql/v1 query GetTodos { todosCollection { edges { node { id title is_complete created_at } } } } mutation InsertTodo($title: String!, $user_id: UUID!) { insertIntotodosCollection( objects: [{ title: $title, user_id: $user_id }] ) { records { id title } } } - Step 28
Integrate with Next.js
Supabase works seamlessly with Next.js. Use Server Components for data fetching, Server Actions for mutations, and client components for interactivity.
// app/lib/supabase/server.ts (Server Components) import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { 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) ) }, }, } ) } // app/todos/page.tsx (Server Component) import { createClient } from '@/lib/supabase/server' export default async function TodosPage() { const supabase = await createClient() const { data: todos } = await supabase .from('todos') .select('*') return ( <ul> {todos?.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } - Step 29
Database backups and point-in-time recovery
Supabase Cloud automatically backs up your database daily. Pro and higher plans support Point-In-Time Recovery (PITR) for restoring to any second within the retention window.
# Create an on-demand backup (via dashboard) # Dashboard > Database > Backups > Create Backup # Download a backup (CLI) supabase db dump --project-ref <your-project-ref> > backup.sql # Restore from backup psql -h db.<your-project-ref>.supabase.co -U postgres -d postgres -f backup.sql⚠ Heads up: Always test restores. PITR is available on Pro plans and above; Free tier has daily backups with 7-day retention.
Feature requests
Sign in to suggest features or vote on existing ones.
No feature requests yet.
Discussion
Sign in to join the discussion.
No comments yet.