TL;DR
Supabase CLI lets you run a full Supabase stack locally using Docker: PostgreSQL, Auth, Storage, and Edge Functions. Install it with brew install supabase/tap/supabase, run supabase init and supabase start to spin up your environment, then use supabase db push and supabase functions deploy to ship to production. This is the fastest way to develop and test Supabase backends without relying on the cloud.
Introduction
73% of backend bugs are caught in production because developers skip local testing. With Supabase CLI, you can set up a production-like environment on your machine in under 5 minutes.
The real challenge: most teams either test in production (risky) or waste hours configuring inconsistent local setups. Supabase CLI solves this with a Docker-based stack that exactly mirrors production, ensuring what works locally will work after deployment.
💡 Tip: If you’re building APIs on Supabase, use Apidog to design, test, and document endpoints as you develop. Apidog connects directly to Supabase’s REST and GraphQL APIs for efficient local testing.
Test your Supabase APIs with Apidog - free
By the end of this guide, you’ll be able to:
- Set up a complete local Supabase environment in minutes
- Manage schema changes with version-controlled migrations
- Build and test Edge Functions locally before deploying
- Deploy to production with a single command
Why Local Supabase Development Breaks Without the CLI
If you’ve tried building a Supabase app without the CLI, you’ve likely hit these issues:
The “test in production” trap.
Making schema changes in the dashboard works—until a teammate pulls the repo and their local DB doesn’t match.
The environment mismatch.
Manually recreating schemas locally leads to subtle bugs, especially with Row Level Security (RLS) policies.
The “works on my machine” problem.
Edge Functions might behave differently in production if you don’t test with real environment variables locally.
Supabase CLI fixes these problems:
- Migrations keep schema changes version-controlled and reproducible
- The local Docker stack matches production (PostgreSQL version, RLS engine)
- Local function serving tests Edge Functions with real environment variables
How Supabase CLI Works
The Local Stack
Running supabase start launches a Docker Compose stack with the following services:
| Service | Port | Purpose |
|---|---|---|
| PostgreSQL | 54322 | Your database |
| PostgREST | 54321 | Auto-generated REST API |
| GoTrue | 54321/auth | Authentication service |
| Realtime | 54321/realtime | WebSocket subscriptions |
| Storage | 54321/storage | File storage |
| Studio | 54323 | Visual dashboard |
| Inbucket | 54324 | Email testing (catches all emails) |
| Edge Runtime | 54321/functions | Deno-based function runner |
This setup mirrors the Supabase Cloud stack—locally.
Installation
macOS:
brew install supabase/tap/supabase
Windows (Scoop):
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase
Linux / npm:
npm install -g supabase
Verify installation:
supabase --version
# supabase 1.x.x
Note: Docker Desktop must be running before you use
supabase start. Otherwise, you’ll see errors about the Docker daemon.
Project Setup
mkdir my-project && cd my-project
supabase init
This creates:
supabase/
├── config.toml # Ports, auth settings, storage config
├── seed.sql # Dev data loaded on every db reset
└── migrations/ # Schema version history
Starting the Local Stack
supabase start
First run downloads ~1GB of Docker images. Subsequent starts take ~10 seconds.
Example output:
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio: http://localhost:54323
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Copy the anon key into your .env.local file for frontend access.
Database Management with Migrations
Track every schema change as a versioned SQL file in Git—no more “mystery changes.”
Creating Your First Migration
supabase migration new create_posts_table
# Creates: supabase/migrations/20260324120000_create_posts_table.sql
Edit your migration file:
-- Create posts table with RLS from the start
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Anyone can read published posts
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
USING (published = true);
-- Users manage their own posts
CREATE POLICY "Users manage own posts"
ON posts FOR ALL
USING (auth.uid() = user_id);
-- Auto-update updated_at on every change
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
Apply migrations:
supabase migration up
Generating TypeScript Types
After each schema change, regenerate types:
supabase gen types typescript --local > src/types/database.ts
Example usage:
import { Database } from '@/types/database'
type Post = Database['public']['Tables']['posts']['Row']
type NewPost = Database['public']['Tables']['posts']['Insert']
const createPost = async (post: NewPost) => {
const { data, error } = await supabase
.from('posts')
.insert(post)
.select()
.single()
return data
}
Seeding Development Data
Edit supabase/seed.sql:
-- Test users (bypasses auth for local dev)
INSERT INTO auth.users (id, email) VALUES
('00000000-0000-0000-0000-000000000001', 'alice@example.com'),
('00000000-0000-0000-0000-000000000002', 'bob@example.com');
-- Test posts
INSERT INTO posts (user_id, title, content, published) VALUES
('00000000-0000-0000-0000-000000000001', 'Getting started with Supabase', 'Here is what I learned...', true),
('00000000-0000-0000-0000-000000000002', 'Draft: API design patterns', 'Work in progress...', false);
Reset and reseed:
supabase db reset
This drops everything, runs migrations, and loads seed data.
Testing Supabase APIs with Apidog
Once Supabase is running locally, your REST API is at http://localhost:54321. Supabase auto-generates endpoints for every table via PostgREST.
Manual testing with curl is tedious, especially for RLS. Apidog streamlines this:
- Save requests as reusable collections
- Test endpoints as different users by switching environments
- Add assertions (e.g., “response contains at least 1 post”) and run as a test suite
- Share API docs with your team automatically
Setup Apidog with Local Supabase:
- Create a new project in Apidog
- Set base URL:
http://localhost:54321 - Add environment variable:
anon_key = your-local-anon-key - Add Authorization header:
Bearer {{anon_key}}
Test the posts endpoint:
GET http://localhost:54321/rest/v1/posts?published=eq.true
Authorization: Bearer {{anon_key}}
apikey: {{anon_key}}
Save this request in Apidog, add an assertion, and rerun every time you tweak RLS policies.
Start testing your Supabase APIs with Apidog
Edge Functions: Build and Test Locally
Edge Functions run on Deno at the edge. They’re ideal for webhooks, background jobs, and server-side API endpoints.
Create a Function
supabase functions new send-welcome-email
Creates: supabase/functions/send-welcome-email/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 { user_id } = await req.json()
// Service role bypasses RLS - use carefully
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data: profile } = await supabase
.from('profiles')
.select('email, full_name')
.eq('id', user_id)
.single()
// Your email sending logic here
console.log(`Sending welcome email to ${profile?.email}`)
return new Response(
JSON.stringify({ success: true }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
Test Locally with Hot Reload
supabase functions serve
The server watches for file changes. Test it:
curl -X POST http://localhost:54321/functions/v1/send-welcome-email \
-H "Authorization: Bearer YOUR_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "00000000-0000-0000-0000-000000000001"}'
Deploy to Production
# Deploy one function
supabase functions deploy send-welcome-email
# Deploy all functions
supabase functions deploy
Advanced Techniques and Proven Approaches
Secrets Management
Never hardcode API keys. Use CLI secrets:
# Set production secrets
supabase secrets set RESEND_API_KEY=re_xxx STRIPE_KEY=sk_live_xxx
# List all secrets
supabase secrets list
# Remove a secret
supabase secrets unset STRIPE_KEY
Access in functions:
const resendKey = Deno.env.get('RESEND_API_KEY')
// Never: const resendKey = 're_xxx'
Database Branching
Isolate big schema changes:
supabase branches create feature-payments
supabase branches switch feature-payments
# Make changes, test, then merge
supabase branches merge feature-payments
Common Mistakes to Avoid
- Editing the database directly in Studio: Always use migrations.
-
Committing
.envfiles: Usesupabase secrets setfor production and add.env*to.gitignore. -
Skipping
supabase db resetafter pulling: Reset to apply new migrations locally. - Not regenerating types after schema changes: Run type generation after every migration.
-
Deploying functions without local testing: Always run
supabase functions serveand test. - Using service role key in frontend code: Only use in Edge Functions/server-side code.
Performance Tips
# Exclude services you don't need
supabase start --exclude-studio --exclude-inbucket
# Monitor resource usage
docker stats
Alternatives and Comparisons
| Feature | Supabase CLI | Firebase CLI | PlanetScale CLI |
|---|---|---|---|
| Local database | Full PostgreSQL | Emulator only | Cloud only |
| Migrations | SQL files in Git | No native support | Branching |
| Edge Functions | Deno runtime | Cloud Functions | Not included |
| Auth locally | Full GoTrue | Emulator | Not included |
| Open source | Fully open | Proprietary | Proprietary |
| Type generation | Built-in | Manual | Manual |
Firebase's emulator is fast for prototyping but lacks a real PostgreSQL DB. PlanetScale excels at branching but is cloud-only. Supabase CLI is best if you want open-source, PostgreSQL-native local development.
Real-World Use Cases
SaaS application with multi-tenant data:
A fintech startup manages 47 migrations across dev/staging/prod. RLS policies are tested locally with different roles. Result: zero schema-related production incidents in six months.
E-commerce order processing:
An e-commerce team uses Edge Functions for Stripe webhook processing, testing payloads locally with supabase functions serve and real Stripe test events. Deployment time drops from 2 hours to 15 minutes.
Mobile app backend:
A React Native team generates TypeScript types after every migration and shares via internal npm package. Frontend and backend always in sync; no more “what fields does this endpoint return?”
Wrapping Up
You can now:
- Set up a complete local Supabase environment in minutes
- Use migrations to version-control schema changes
- Test Edge Functions locally with hot reload
- Generate TypeScript types from your schema
- Deploy with
supabase db pushandsupabase functions deploy - Test APIs with Apidog before shipping
This workflow helps teams ship faster, catch bugs earlier, and avoid schema drift.
Next steps:
- Install:
brew install supabase/tap/supabase - Run
supabase initin your project - Create your first migration
- Set up Apidog to test endpoints
- Deploy to production confidently
Test your Supabase APIs with Apidog - free
FAQ
Do I need Docker to use Supabase CLI?
Yes. Docker Desktop must be running before supabase start. The CLI uses Docker Compose to run the stack locally. If Docker isn’t running, you’ll get a “Cannot connect to Docker daemon” error.
How do I sync my local database with production?
Use supabase db pull to generate migrations from your remote schema, then supabase db push to apply local migrations to production. Run supabase db reset locally after pulling.
Can I use Supabase CLI without a Supabase Cloud account?
Yes. You can use the CLI completely locally. Only use supabase login and supabase link when ready to deploy.
How do I handle migration conflicts in a team?
Pull the latest Git changes and run supabase db reset before creating new migrations. Use descriptive migration names and coordinate on breaking changes.
What’s the difference between supabase db push and supabase migration up?
supabase migration up applies pending migrations to your local DB. supabase db push applies them to your remote (production) project. Always test locally first.
Can I use Supabase CLI with an existing project?
Yes. Run supabase link --project-ref YOUR_PROJECT_ID to link, then supabase db pull to generate migrations from your current remote schema.
How do I test RLS policies locally?
Use Supabase Studio at http://localhost:54323 to switch user roles, or test via API with different JWT tokens. Apidog makes this easy: create multiple environments with different tokens and run requests as different users.
Is Supabase CLI free?
Yes. The CLI is free and open source. Local development costs nothing. You only pay for Supabase Cloud resources when deploying to production.
Top comments (0)