DEV Community

Cover image for How to Use Supabase CLI: The Complete Developer Guide (2026)
Preecha
Preecha

Posted on

How to Use Supabase CLI: The Complete Developer Guide (2026)

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.

Try Apidog today

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Windows with Scoop

scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase
Enter fullscreen mode Exit fullscreen mode

Linux or npm

npm install -g supabase
Enter fullscreen mode Exit fullscreen mode

Verify the installation:

supabase --version
# supabase 1.x.x
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Start the local environment

supabase start
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This creates a file similar to:

supabase/migrations/20260324120000_create_posts_table.sql
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Apply pending migrations to your local database:

supabase migration up
Enter fullscreen mode Exit fullscreen mode

Generate TypeScript types

Regenerate types after every schema change:

supabase gen types typescript --local > src/types/database.ts
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  );
Enter fullscreen mode Exit fullscreen mode

Reset the database, rerun all migrations, and reload seed data:

supabase db reset
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Image

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

  1. Create a project in Apidog.
  2. Set the base URL to http://localhost:54321.
  3. Add an environment variable named anon_key.
  4. Set its value to your local Supabase anon key.
  5. Add these request headers:
Authorization: Bearer {{anon_key}}
apikey: {{anon_key}}
Enter fullscreen mode Exit fullscreen mode

Test the posts endpoint

Create a request:

GET http://localhost:54321/rest/v1/posts?published=eq.true
Authorization: Bearer {{anon_key}}
apikey: {{anon_key}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This creates:

supabase/functions/send-welcome-email/index.ts
Enter fullscreen mode Exit fullscreen mode

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' } }
  )
})
Enter fullscreen mode Exit fullscreen mode

Serve functions locally

Start the local function server:

supabase functions serve
Enter fullscreen mode Exit fullscreen mode

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"}'
Enter fullscreen mode Exit fullscreen mode

Deploy functions

Deploy one function:

supabase functions deploy send-welcome-email
Enter fullscreen mode Exit fullscreen mode

Deploy all functions:

supabase functions deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Read secrets inside a function:

const resendKey = Deno.env.get('RESEND_API_KEY')
Enter fullscreen mode Exit fullscreen mode

Avoid this:

const resendKey = 're_xxx'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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*
Enter fullscreen mode Exit fullscreen mode

Use supabase secrets set for production secrets.

Skipping supabase db reset after pulling

After pulling new migrations, reset your local database:

supabase db reset
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check Docker resource consumption:

docker stats
Enter fullscreen mode Exit fullscreen mode

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

  1. Install the CLI:
   brew install supabase/tap/supabase
Enter fullscreen mode Exit fullscreen mode
  1. Initialize your project:
   supabase init
Enter fullscreen mode Exit fullscreen mode
  1. Start the local stack:
   supabase start
Enter fullscreen mode Exit fullscreen mode
  1. Create your first migration.
  2. Configure Apidog to test local API endpoints.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

supabase db push applies local migrations to your linked remote project.

supabase db push
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)