The Vibe Coder's Guide to Supabase Environments

17 Aug 2025

·

13 minute read

Setting up separate development and production environments does not have to be painful. This guide shows you how to build a professional deployment workflow for your Supabase project. You will learn the essential patterns that prevent the 3am "I dropped production" panic attacks while keeping your workflow fun, simple, and safe.

Supabase is the open source Postgres development platform. At its core, it is just Postgres, but with an integrated suite: Auth, Storage, Edge Functions, Realtime, and Vector search. That means you can start hacking in minutes and also scale to millions when your app takes off.

With this post, we'll explore how to setup a professional development and staging environment for our projects to prevent those late night panics.

Rule #1: never work directly on production#

The fastest way to ruin your night is to treat your one Supabase project as both your playground and your live app. One wrong DROP TABLE and your users are gone. The simple fix is to create at least two projects: one for breaking things (development) and one for your users (production). Larger teams often add a staging project as well, but the minimum is two.

Create them in the Supabase Dashboard and give them obvious names like myapp-dev and myapp-prod. Boring names reduce mistakes. Grab the Project Reference IDs from Settings > General and stash them in a safe place.

Then set up the Supabase CLI:


_10
npm install supabase --save-dev
_10
supabase init

This creates a supabase/ directory, your single source of truth for migrations, functions, and seed data. Treat it like a ledger of every database change. Because it is just files, you can track it with Git, roll changes forward, and keep environments in sync. The flow should always be one direction: local development → dev project → production project. That is how you avoid the pain of trying to sync in multiple directions later.

Database migrations are git commits for your database#

Migrations are your safety net. Each one is a timestamped SQL file in supabase/migrations/. They record what changed and when, just like Git commits. This is how you avoid schema drift, where dev and prod quietly diverge until one day you cannot deploy without breaking things.

Here is the basic workflow:

  1. Create a migration whenever you need to change the schema:


    _10
    supabase migration new add_user_profiles

  2. Fill in the SQL, and always enable Row Level Security. Without RLS, anyone with your project URL can read all your data.


    _10
    CREATE TABLE public.profiles (
    _10
    id UUID REFERENCES auth.users ON DELETE CASCADE,
    _10
    username TEXT UNIQUE,
    _10
    avatar_url TEXT,
    _10
    created_at TIMESTAMPTZ DEFAULT NOW()
    _10
    );
    _10
    ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
    _10
    CREATE POLICY "Users can view own profile" ON profiles
    _10
    FOR SELECT USING (auth.uid() = id);

  3. Test locally before touching any remote environment:


    _10
    supabase db reset

  4. If you make changes in the Dashboard instead of SQL, capture them:


    _10
    supabase db diff -f capture_dashboard_changes

Because migrations must run in order on a fresh database, always reset locally to prove they work. If supabase db reset works, production will too. This habit prevents the subtle drift that causes late night panics.

Common pitfalls and how to avoid them#

Every developer hits the same landmines once. Knowing them up front means you only hit them once.

  • Forgetting to enable RLS. Without it, your tables are wide open. Always add ALTER TABLE ... ENABLE ROW LEVEL SECURITY;.
  • Deploying to the wrong environment. Give production a different terminal theme, never store its credentials locally, and run supabase status to check before you push.
  • Migration conflicts. If two migrations collide after a Git merge, rename one with a later timestamp and rerun supabase db reset to verify the order.
  • Exposed service role keys. If one leaks, rotate it immediately in the Dashboard, update every environment, and scrub your Git history.

Mistakes are inevitable. Guardrails keep them from becoming disasters.

GitHub autopilot with CI/CD#

Manual deploys are risky. Automating them with GitHub Actions removes the human error. The idea is simple: push to develop to deploy to staging, merge to main to deploy to production.

Add your secrets in Settings > Secrets and variables > Actions. Then create .github/workflows/deploy.yml:


_21
name: Deploy Supabase
_21
on:
_21
push:
_21
branches: [main, develop]
_21
jobs:
_21
deploy:
_21
runs-on: ubuntu-latest
_21
steps:
_21
- uses: actions/checkout@v4
_21
- uses: supabase/setup-cli@v1
_21
with: { version: latest }
_21
- name: Deploy to staging
_21
if: github.ref == 'refs/heads/develop'
_21
run: |
_21
supabase link --project-ref ${{ secrets.STAGING_PROJECT_ID }}
_21
supabase db push
_21
- name: Deploy to production
_21
if: github.ref == 'refs/heads/main'
_21
run: |
_21
supabase link --project-ref ${{ secrets.PRODUCTION_PROJECT_ID }}
_21
supabase db push

Now deployments happen automatically with every push. You do not have to remember commands or worry about sending them to the wrong project. Larger teams often extend this with integration tests that run against staging before code can be promoted, but even this simple setup eliminates most accidents.

Backups are your safety net#

Every production app needs a backup plan. The best plans run without you thinking about them. Set up a GitHub Action to dump your database nightly.

Backups are only useful if you know they work. Schedule a monthly drill: restore a backup to a new project, run through your app, and confirm the data is intact. If you cannot restore, you do not have a backup.

For high stakes apps, combine PITR with read replicas and multi region deployments. That way you can recover from mistakes without downtime or lost data.

Environment variables without the oops#

Secrets are a common leak. The rule is simple: anything with NEXT_PUBLIC_ is visible in browser code. Only use anon keys there.

Keep secrets like service role keys in .env.local, and never commit that file. Document what is required in .env.example. Then in your code, create two Supabase clients: one safe for the browser, one for server side code.

For bigger teams, a secrets manager like Doppler, Vault, or GitHub's encrypted environment variables makes rotation and auditing easier.

Branches that match reality#

Your Git branches should map to your environments. Keep it simple: main for production, develop for staging, and feat/* for features. Supabase will even create preview branches for you automatically when you open a PR. Each one is a fully isolated Supabase instance with unique credentials, perfect for testing features before they hit staging.

This structure keeps your workflow clean and prevents confusion about which branch is safe to merge.

Your deployment rituals#

With all the pieces in place, you need habits to tie them together.

For daily development:

  • Start Supabase locally with supabase start
  • Branch from develop
  • Make schema changes with migrations
  • Test with supabase db reset
  • Commit and push

For deployments:

  • Open a pull request from your feature branch into develop
  • Test your changes in staging
  • Open a pull request from develop into main
  • Merge to deploy to production
  • Monitor production for 15 minutes to catch issues quickly

Pull requests are not just ceremony. They create an audit trail, trigger preview branches, and give you a chance to test before you touch production. That small delay saves hours of recovery work later.

Build in a weekend. Scale to millions.#

Let's say your weekend project took off and people are using it. Let's build separate dev and prod environments. To start, you will create two Supabase projects instead of one. Use the dev project for breaking things, keep the production project for your users. After that, you'll set up Vercel to automatically use the right database for each environment.

Separate your environments#

Create a development project in the Supabase Dashboard and name it yourapp-dev. Rename your existing project to yourapp-prod for clarity. Now you have a safe place to experiment.

Extract your production schema and turn it into migration files:


_10
npm install supabase --save-dev
_10
supabase init
_10
_10
# Capture your production schema
_10
supabase link --project-ref YOUR_PROD_PROJECT_ID
_10
supabase db pull
_10
_10
# Apply the same schema to development
_10
supabase link --project-ref YOUR_DEV_PROJECT_ID
_10
supabase db push

This creates migration files in supabase/migrations/ that represent your current database structure. These files are your new source of truth for schema changes.

Configure Vercel environments#

Tell Vercel which database to use for each deployment. Go to your Vercel project settings and add environment variables:

Production environment (only main branch):


_10
NEXT_PUBLIC_SUPABASE_URL = https://yourapp-prod.supabase.co
_10
NEXT_PUBLIC_SUPABASE_ANON_KEY = your_prod_anon_key

Preview environment (all other branches):


_10
NEXT_PUBLIC_SUPABASE_URL = https://yourapp-dev.supabase.co
_10
NEXT_PUBLIC_SUPABASE_ANON_KEY = your_dev_anon_key

Update your local .env.local to point to the dev project so you never accidentally test against production data.

Your new daily workflow#

The workflow stays almost identical to what you know, with one key difference: you never touch the production Supabase Dashboard again.

For regular features:

  • Code locally (automatically uses dev database)
  • Push any branch to get a Vercel preview: git push origin feature/new-comments. Vercel automatically creates a preview URL like yourapp-git-feature-new-comments.vercel.app that connects to your dev database with safe test data.
  • Test the preview URL with fake data
  • Merge to main when ready: Create a pull request on GitHub, review your changes, then merge. Vercel automatically deploys to your production domain using the production database.

For database changes:

  • Create a migration: supabase migration new add_comments_table
  • Write SQL in the generated file
  • Test on dev: supabase db push
  • Commit the migration file and push: git add supabase/migrations/ then git commit -m "Add comments table" then git push origin feature/comments. The migration file gets committed to your repo like any other code.
  • Production gets the same changes automatically

Automate production deployments#

Without automation, you would need to manually apply database changes to production every time you merge code. That means remembering to run supabase db push against your production project, which is error-prone and easy to forget.

GitHub Actions solves this by watching your repository and automatically running commands when specific events happen. Set up GitHub Actions to handle production database changes. Create .github/workflows/deploy.yml:


_17
name: Deploy to Production
_17
on:
_17
push:
_17
branches: [main]
_17
_17
jobs:
_17
deploy:
_17
runs-on: ubuntu-latest
_17
steps:
_17
- uses: actions/checkout@v4
_17
- uses: supabase/setup-cli@v1
_17
- name: Apply migrations to production
_17
run: |
_17
supabase link --project-ref ${{ secrets.PRODUCTION_PROJECT_ID }}
_17
supabase db push
_17
env:
_17
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}

This file tells GitHub: "Every time someone merges code into the main branch, automatically connect to the production Supabase project and apply any new migration files." Vercel handles your frontend deployment, but your database changes need this extra step.

Here is what happens when you merge a pull request:

  1. Vercel automatically deploys your new frontend code to production
  2. GitHub Actions triggers and connects to your production Supabase project
  3. Any new migration files get applied to the production database
  4. Your frontend and database stay in perfect sync

Add your project IDs and access token to GitHub secrets. Now every merge to main automatically applies your migrations to production. No more forgetting to update the database. No more manual steps that can go wrong at 2am.

The safety net effect#

Every branch you push creates a preview deployment that uses development data. You can test destructive changes, experiment with new features, and invite others to try things without any risk to production.

The key insight is that your development and production environments stay perfectly in sync through migrations. When a migration works in development, it will work in production. No more schema drift, no more surprise failures.

Enable Row Level Security immediately#

Your weekend project probably skipped RLS. Fix this now before you ship new features:


_10
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
_10
CREATE POLICY "Users can only access their own data" ON your_table
_10
FOR ALL USING (auth.uid() = user_id);

Apply these policies to both environments. RLS is your last line of defense against data breaches.

This setup takes one afternoon to implement but eliminates the fear of breaking production. You can move fast again while your users stay protected. The same tools, the same workflow, just organized safely.

Final word#

The path from vibe coder to confident deployer is not about memorizing every DevOps buzzword. It is about a handful of patterns that keep you safe: separate environments, migrations as save points, automated deployments, tested backups, and strict RLS. Supabase makes this easy because everything is Postgres, deeply integrated, and scalable from weekend project to millions of users.

Share this article

Build in a weekend, scale to millions