Skip to content

Supabase Workflow

End-to-end guide for working with Supabase as a Rose developer. Pick a path, follow it.

Pick your environment

flowchart TD A[What are you doing?] --> B{Frontend-only change<br/>against stable schema?} B -->|Yes| C[Local Supabase<br/><code>just dev</code>] B -->|No| D{Touches migrations,<br/>RLS, RPCs, MVs?} D -->|No| E{Reproducing a real<br/>client bug?} D -->|Yes| F[Preview branch<br/><code>./bootstrap.py --branch</code>] E -->|No| C E -->|Yes| G[Local or branch<br/>+ <code>just *-copy</code> seed]
Target Command to start When
Local Supabase (Docker) cd supabase && just dev Frontend dev, isolated tests, no prod credentials needed.
Preview branch (remote) ./bootstrap.py --branch Migrations / RLS / RPC / MV work. Real Postgres, isolated DB, auto-deleted on PR close.
Staging Already wired via frontend/.env.staging Final integration testing before merge.

You never need to develop against production directly. Migrations reach prod via the branch merge workflow.

Standard workflows

A. Pure frontend or backend change (no schema work)

# Start once
cd supabase && just dev      # Boots local Postgres + Auth + Studio at 127.0.0.1:54323
cd ../supabase && just seed-local   # Synthetic data (or seed-local-copy for real)

# Develop
cd ../frontend && just dev
cd ../backend && just dev

Login as admin@admin.com / admin. See Supabase Seeding for the data set you'll see.

B. Schema change (migration, RLS, RPC, materialized view)

# 1. Spin up an isolated branch DB
./bootstrap.py --branch

# 2. Write the migration
date +%Y%m%d%H%M%S       # generate timestamp
$EDITOR supabase/migrations/20260520143000_my_change.sql

# 3. Apply it to the branch DB
cd supabase
set -a; . ../backend/.env.local; set +a
DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
supabase db push --db-url "$DB_URL" --include-all --yes
# OR shortcut that also re-seeds + refreshes MVs:
just seed-branch

# 4. Regenerate frontend types
cd ../frontend && just gen-types
# (commit the generated frontend/shared/src/types/database.generated.ts)

# 5. Type-check + test
cd ../backend && just mypy <changed-files>
cd ../frontend && just lint

Migration is automatically merged into production when the PR closes (via .github/workflows/supabase-branch-cleanup.yml).

C. Reproducing a real client bug

The synthetic data won't match real production shapes. Use the *-copy seed path:

# On a preview branch (recommended — isolated, won't pollute local DB)
./bootstrap.py --branch
cd supabase && just seed-branch-copy --clear

# OR against local Supabase
cd supabase && just seed-local-copy --clear

Requires backend/.env.production (downloaded by just download-env all). The copy is ~60 s and contains real visitor IPs / emails / chat content — treat the resulting DB as sensitive.

D. Editing a materialized view or RPC

MVs and functions follow the "live remote schema is the only source of truth" rule (see supabase/AGENTS.md):

-- Inspect the CURRENT shape on develop (the source of truth):
SELECT pg_get_viewdef('public.mv_client_stats_30d'::regclass);
SELECT pg_get_functiondef('public.get_account_stats_with_intent'::regproc);

Then write a migration that drops + recreates (or uses CREATE OR REPLACE only if the signature is identical — Postgres rejects return-type changes via CREATE OR REPLACE alone, see 20260318103855 for the fix pattern).

MVs are populated by triggers / cron in production. On a fresh branch, the synthetic seed refreshes them in dependency order (mv_widget_visitorsmv_conversation_visitorsmv_client_stats_30d). If you add a new MV, append its REFRESH to supabase/justfile in the seed-local and seed-branch targets.

Don'ts

Don't push to production directly

  • Never supabase db push against the prod project ref (drtzxyuvppalvgczwhne) from your laptop. Migrations reach prod via the branch-merge workflow only.
  • Never supabase migration repair against prod from your laptop.
  • Never edit a migration that has already been applied to prod, except to make it idempotent for fresh replays (e.g. adding DROP FUNCTION IF EXISTS before a CREATE OR REPLACE). Develop's schema_migrations ledger prevents re-application, so the edit only affects new replays.

Don't seed against the wrong target

The 5 seed targets are easy to confuse. Quick reference:

Command Hits
just seed-local Local Supabase :54322
just seed-local-fresh Local Supabase :54322 (--clear first)
just seed-local-copy Local Supabase :54322
just seed-branch Whatever's in backend/.env.local (your preview branch)
just seed-branch-copy Whatever's in backend/.env.local (your preview branch)

Debugging

"Invalid login credentials" on the backoffice

seed.sql didn't run on this DB. Either you skipped just seed / just seed-branch, or the branch is stuck in CREATING_PROJECT / MIGRATIONS_FAILED. See Common errors.

"Access Restricted: admin@admin.com does not have access"

Browser holds a stale JWT signed by a previous (deleted) branch's secret. Clear site data for the backoffice origin and log in again.

Backoffice pages show zero data

  • Conversations / Visitors / Accounts empty → check environment filter (default production) and the domain selector. Synthetic data is on the 4 fake domains (acme.com, contoso.com, fabrikam.com, demo.local).
  • Home dashboard tiles show 0 → materialized views aren't refreshed. Re-run just seed-branch (or REFRESH MATERIALIZED VIEW manually via Studio in the dependency order documented in seeding).

RLS denying queries you expect to work

Most tables gate on has_domain_access(site_domain). Verify:

SELECT has_domain_access('acme.com');   -- should be true for admin@admin.com

Admins bypass via is_backoffice_admin(). Non-admins need a row in backoffice_user_domains. The synthetic seed grants user@user.com access to all 4 fake domains.

Cheat sheet

# Create / refresh isolated branch DB
./bootstrap.py --branch

# Seed (all idempotent)
cd supabase
just seed-local                 # local synthetic
just seed-local-fresh           # local synthetic + --clear
just seed-local-copy            # local prod copy
just seed-branch                # branch synthetic (also pushes missing migrations, refreshes MVs)
just seed-branch --clear        # branch synthetic + --clear
just seed-branch-copy           # branch prod copy

# Branch lifecycle CLI
python3.12 scripts/supabase_branch.py ensure
python3.12 scripts/supabase_branch.py delete <git-branch>
python3.12 scripts/supabase_branch.py merge  <git-branch>
python3.12 scripts/supabase_branch.py reap --dry-run

# Inspect current branch DB
set -a; . backend/.env.local; set +a
DB_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
psql "$DB_URL"

# After any migration
cd frontend && just gen-types   # regen Database TypeScript types
git add frontend/shared/src/types/database.generated.ts

See also