TL;DR
Supabase CLI runs a full Supabase stack on your machine using Docker: PostgreSQL, Auth, Storage, and Edge Functions. Install it with brew install supabase/tap/supabase, run supabase init and supabase start to create a local environment, then use supabase db push and supabase functions deploy to ship to production.
Introduction
73% of backend bugs get caught in production because developers skip local testing. Supabase CLI gives you a production-equivalent environment on your machine in under five minutes.
The usual alternatives are risky or slow:
- Testing in production: schema changes can break teammates or live users.
- Hand-built local environments: manually configured PostgreSQL instances often drift from the cloud setup.
- Hardcoded function tests: Edge Functions may work with mock values but fail with real environment variables.
Supabase CLI uses Docker to run a local stack that mirrors Supabase Cloud, including PostgreSQL, Auth, Storage, Realtime, Studio, and Edge Functions.
If you are building APIs on top of Supabase, use an API client to test endpoints while you build them. Apidog can connect to Supabase REST and GraphQL APIs so you can test local endpoints, validate RLS behavior, and document requests as part of development.
By the end of this guide, you will be able to:
- Set up a complete local Supabase environment
- Manage schema changes with version-controlled migrations
- Build and test Edge Functions locally
- Generate TypeScript types from your schema
- Deploy database changes and functions to production
Why local Supabase development breaks without the CLI
Without the CLI, local development usually fails in predictable ways.
The “test in production” trap
You add a column in the Supabase dashboard, verify it works, and deploy your frontend. A teammate pulls the repository later, but their database does not contain that column.
The fix: create a migration for every schema change and commit it to Git.
The environment mismatch
You create a local PostgreSQL database and manually recreate your schema. Then Row Level Security behavior appears different from production.
In practice, the RLS engine is not the issue—you likely missed a policy, extension, trigger, or configuration setting.
The fix: run the Supabase stack locally instead of recreating pieces manually.
The “works on my machine” Edge Function
An Edge Function works with hardcoded test data but fails after deployment because production secrets or environment variables are missing.
The fix: serve functions locally and test them with real requests and local environment variables.
Supabase CLI addresses these issues with:
- Version-controlled SQL migrations
- A Docker-based local stack matching Supabase services
- Local Edge Function serving
- Repeatable resets and seed data
How Supabase CLI works
The local stack
Run the following command:
supabase start
The CLI starts a Docker Compose stack with these 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 |
Local email testing |
| Edge Runtime | 54321/functions |
Deno-based function runner |
This is the same stack used by Supabase Cloud, running locally.
Install the CLI
macOS
brew install supabase/tap/supabase
Windows with Scoop
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase
Linux or npm
npm install -g supabase
Verify the installation:
supabase --version
# supabase 1.x.x
Docker Desktop must be running before you run supabase start. Otherwise, the CLI cannot connect to the Docker daemon.
Initialize a project
Create a project directory and initialize Supabase:
mkdir my-project && cd my-project
supabase init
This creates the Supabase project structure:
supabase/
├── config.toml # Ports, auth settings, storage config
├── seed.sql # Development data loaded on db reset
└── migrations/ # Version-controlled schema history
Start the local environment
supabase start
The first run downloads Docker images. Later starts are much faster.
The CLI prints local connection details similar to:
API URL: http://localhost:54321
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio: http://localhost:54323
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Add the local API URL and anon key to your frontend environment file:
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-local-anon-key
Database management with migrations
Migrations are the core CLI workflow. Each schema change becomes a timestamped SQL file that you can review, commit, and apply consistently.
Create a migration
Generate a migration file:
supabase migration new create_posts_table
This creates a file similar to:
supabase/migrations/20260324120000_create_posts_table.sql
Add your schema, policies, and triggers:
-- 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 pending migrations to your local database:
supabase migration up
Generate TypeScript types
Regenerate types after every schema change:
supabase gen types typescript --local > src/types/database.ts
Use the generated types in your app:
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
}
Your editor can now catch invalid column names, missing required fields, and incorrect types before runtime.
Seed development data
Put repeatable local data in supabase/seed.sql:
-- Test users for local development
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 the database, rerun all migrations, and reload seed data:
supabase db reset
Run this after pulling migrations from teammates or whenever you need a clean local state.
Testing Supabase APIs with Apidog
Once Supabase is running locally, PostgREST exposes a REST API at:
http://localhost:54321/rest/v1
Every table gets API endpoints automatically. Testing them with curl works for quick checks, but it becomes tedious when you need to verify RLS policies with multiple users and tokens.
Apidog can connect to your local Supabase instance so you can:
- Save requests as reusable collections
- Switch environments to test as different users
- Add assertions to API responses
- Run API checks after RLS changes
- Share API documentation with your team
Configure Apidog for local Supabase
- Create a project in Apidog.
- Set the base URL to
http://localhost:54321. - Add an environment variable named
anon_key. - Set its value to your local Supabase anon key.
- Add these request headers:
Authorization: Bearer {{anon_key}}
apikey: {{anon_key}}
Test the posts endpoint
Create a request:
GET http://localhost:54321/rest/v1/posts?published=eq.true
Authorization: Bearer {{anon_key}}
apikey: {{anon_key}}
Save the request and add an assertion that the response contains at least one post. Run it whenever you modify RLS policies to catch broken access rules before deployment.
Edge Functions: build and test locally
Supabase Edge Functions run on Deno. Use them for webhooks, background jobs, or endpoints that require server-side logic.
Create a function
Generate a new function:
supabase functions new send-welcome-email
This creates:
supabase/functions/send-welcome-email/index.ts
Implement the function:
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 it only in server-side code.
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' } }
)
})
Serve functions locally
Start the local function server:
supabase functions serve
The server watches files and reloads when your code changes.
Test the function:
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 functions
Deploy one function:
supabase functions deploy send-welcome-email
Deploy all functions:
supabase functions deploy
Advanced techniques and proven approaches
Manage secrets
Do not hardcode API keys in Edge Functions. Store production secrets with the CLI:
# Set production secrets
supabase secrets set RESEND_API_KEY=re_xxx STRIPE_KEY=sk_live_xxx
# List secrets
supabase secrets list
# Remove a secret
supabase secrets unset STRIPE_KEY
Read secrets inside a function:
const resendKey = Deno.env.get('RESEND_API_KEY')
Avoid this:
const resendKey = 're_xxx'
Use database branches
For larger schema work, use an isolated branch:
supabase branches create feature-payments
supabase branches switch feature-payments
# Make changes and test them
supabase branches merge feature-payments
This keeps the main development database clean while you experiment.
Avoid common mistakes
Editing the database directly in Studio
Use migrations for schema changes. Direct Studio edits are not tracked in Git and are easy for teammates to miss.
Committing .env files
Keep local environment files out of Git:
.env*
Use supabase secrets set for production secrets.
Skipping supabase db reset after pulling
After pulling new migrations, reset your local database:
supabase db reset
This ensures your schema and seed data match the repository.
Forgetting to regenerate types
After adding or changing columns, run:
supabase gen types typescript --local > src/types/database.ts
Deploying functions without local tests
Run supabase functions serve and test real request payloads before deploying.
Using the service role key in frontend code
The service role key bypasses RLS. Keep it in Edge Functions and other server-side code only—never expose it in a browser.
Reduce local resource usage
Exclude services you do not need:
supabase start --exclude-studio --exclude-inbucket
Check Docker resource consumption:
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 local emulator is useful for quick prototyping, but it does not provide a full PostgreSQL instance. PlanetScale has a strong branching workflow for schema changes, but development remains cloud-based.
Supabase CLI is a good fit when you need an open-source, PostgreSQL-native local development environment.
Real-world use cases
SaaS applications with multi-tenant data
A fintech startup manages 47 migrations across development, staging, and production. The team tests RLS policies locally with different user roles before deployment, preventing schema-related production incidents.
E-commerce order processing
An e-commerce team uses Edge Functions for Stripe webhook processing. They test webhook payloads locally with supabase functions serve and Stripe test events before deployment.
Mobile app backends
A React Native team generates TypeScript types after every migration and shares them as an internal npm package. Frontend and backend developers stay aligned on field names and API response shapes.
Wrapping up
With Supabase CLI, you can:
- Run a complete Supabase stack locally
- Version-control schema changes with migrations
- Reset and seed local databases reliably
- Generate TypeScript types from your database schema
- Test Edge Functions with hot reload
- Deploy migrations with
supabase db push - Deploy functions with
supabase functions deploy - Test Supabase APIs before shipping
Next steps
- Install the CLI:
brew install supabase/tap/supabase
- Initialize your project:
supabase init
- Start the local stack:
supabase start
- Create your first migration.
- Configure Apidog to test local API endpoints.
- Deploy after validating migrations, RLS policies, and Edge Functions locally.
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 local stack. If Docker is unavailable, you will see an error such as “Cannot connect to Docker daemon.”
How do I sync my local database with production?
Use supabase db pull to generate a migration from your remote schema, then use supabase db push to apply local migrations to production.
supabase db pull
supabase db reset
supabase db push
Run supabase db reset locally after pulling changes so your local environment matches the migration history.
Can I use Supabase CLI without a Supabase Cloud account?
Yes. You can use the CLI entirely locally without a cloud account. You only need supabase login and supabase link when you are ready to connect to or deploy to a Supabase Cloud project.
How do I handle migration conflicts in a team?
Pull the latest Git changes and reset your local database before creating a new migration:
git pull
supabase db reset
Use descriptive migration names and coordinate with teammates when making breaking changes.
What is the difference between supabase db push and supabase migration up?
supabase migration up applies pending migrations to your local database.
supabase migration up
supabase db push applies local migrations to your linked remote project.
supabase db push
Test locally before running supabase db push.
Can I use Supabase CLI with an existing project?
Yes. Link your local repository to the existing project, then pull its schema:
supabase link --project-ref YOUR_PROJECT_ID
supabase db pull
How do I test RLS policies locally?
Use Supabase Studio at http://localhost:54323 to inspect your local database, or test API requests using different JWT tokens.
For repeatable checks, create multiple API environments with different user tokens in Apidog and run the same request under each role.
Is Supabase CLI free?
Yes. The CLI is free and open source. Local development does not cost anything. Supabase Cloud costs apply only when you deploy and use cloud resources.

Top comments (0)