Files
route-commerce/docs/SUPABASE_DUMP_GUIDE.md
T
tyler cbc8958b55 update dump guide with real lessons from 2026-06-05 capture
- aws-1 pooler (not aws-0)
- pg_dump 17 required for Supabase (PG 17.6 server)
- extensions schema + stubs needed for local PG 16
- auth.users stub for RLS policy references
- Re-apply is idempotent (errors are 'already exists')
2026-06-05 17:31:31 +00:00

7.4 KiB

Supabase Dump & Restore Guide

This guide is the runbook for capturing the real Supabase schema and data and restoring it to a local Postgres database. It documents the exact steps that worked when the spend cap was removed on 2026-06-05.

Connection (this works from the dev box)

The Supabase project wnzkhezyhnfzhkhiflrp (route-commerce) is hosted in East US (North Virginia). The dev box has no IPv6, so we must use the Supavisor pooler (IPv4 only) — and it's on aws-1, not aws-0.

# Working pooler URL (from supabase/.temp/pooler-url)
postgresql://postgres.wnzkhezyhnfzhkhiflrp:YLKzP9jz2yqop7jr@aws-1-us-east-1.pooler.supabase.com:5432/postgres

The aws-0-us-east-1.pooler.supabase.com hostname resolves over IPv4 but Supavisor returns "tenant/user not found" because the project lives on aws-1. The correct region number is non-obvious — always check supabase/.temp/pooler-url for the actual endpoint.

The direct hostname db.wnzkhezyhnfzhkhiflrp.supabase.co only resolves over IPv6, which is unreachable from this dev box.

pg_dump version

Supabase runs PostgreSQL 17.6. Local pg must be ≥ 17 to dump cleanly. The dev box had pg 16 by default — install pg 17 client:

echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt/ noble-pgdg main" \
  | sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update
sudo apt-get install -y --allow-unauthenticated postgresql-client-17
# Use /usr/lib/postgresql/17/bin/pg_dump explicitly (or update PATH)

Capture Schema

export PGPASSWORD="YLKzP9jz2yqop7jr"
export PATH="/usr/lib/postgresql/17/bin:$PATH"

mkdir -p supabase/captured
pg_dump \
  --host=aws-1-us-east-1.pooler.supabase.com \
  --port=5432 \
  --username=postgres.wnzkhezyhnfzhkhiflrp \
  --dbname=postgres \
  --schema-only \
  --no-owner \
  --no-privileges \
  --no-acl \
  --exclude-schema=auth \
  --exclude-schema=storage \
  --exclude-schema=realtime \
  --exclude-schema=supabase_functions \
  --exclude-schema=graphql \
  --exclude-schema=graphql_public \
  --exclude-schema=pgsodium \
  --exclude-schema=pgsodium_masks \
  --exclude-schema=extensions \
  --exclude-schema=pgbouncer \
  --exclude-schema=supabase_migrations \
  --exclude-schema=net \
  --exclude-schema=vault \
  --file=supabase/captured/captured_schema.sql

Captured schema is ~540KB, 65 tables, 252 functions.

Capture Data

pg_dump \
  --host=aws-1-us-east-1.pooler.supabase.com \
  --port=5432 \
  --username=postgres.wnzkhezyhnfzhkhiflrp \
  --dbname=postgres \
  --data-only \
  --no-owner \
  --no-privileges \
  --no-acl \
  --disable-triggers \
  --exclude-schema=auth \
  --exclude-schema=storage \
  --exclude-schema=realtime \
  --exclude-schema=supabase_functions \
  --exclude-schema=graphql \
  --exclude-schema=graphql_public \
  --exclude-schema=pgsodium \
  --exclude-schema=pgsodium_masks \
  --exclude-schema=extensions \
  --exclude-schema=pgbouncer \
  --exclude-schema=supabase_migrations \
  --exclude-schema=net \
  --exclude-schema=vault \
  --file=supabase/captured/captured_data.sql

Captured data is ~130KB for this project's current size.

Restore to Local DB (PostgreSQL 16)

PostgreSQL 16 doesn't support transaction_timeout (added in PG 17) and doesn't have the supabase_vault extension. Pre-create the stub objects:

export PGPASSWORD=routecommerce_dev_password
psql -U routecommerce -h 127.0.0.1 -d route_commerce <<'SQL'
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO routecommerce;

-- extensions schema (referenced by dump as extensions.uuid_generate_v4())
CREATE SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" SCHEMA extensions;

-- Stub functions in extensions schema (real Supabase has pgcrypto.crypt etc.)
CREATE OR REPLACE FUNCTION extensions.uuid_generate_v4()
  RETURNS uuid LANGUAGE sql AS $$ SELECT gen_random_uuid(); $$;
CREATE OR REPLACE FUNCTION extensions.gen_salt(text)
  RETURNS text LANGUAGE sql AS $$ SELECT '$2a$06$' || repeat('A', 53); $$;
CREATE OR REPLACE FUNCTION extensions.crypt(text, text)
  RETURNS text LANGUAGE sql AS $$ SELECT $1; $$;

GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA extensions TO routecommerce;

-- auth stub (we excluded auth schema from dump, but RLS policies reference auth.uid())
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT,
  raw_user_meta_data JSONB,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE OR REPLACE FUNCTION auth.uid() RETURNS UUID
  LANGUAGE sql STABLE AS $$ SELECT NULL::UUID; $$;
GRANT USAGE ON SCHEMA auth TO routecommerce;
GRANT ALL ON auth.users TO routecommerce;
GRANT EXECUTE ON FUNCTION auth.uid() TO routecommerce;

-- Drop Supabase-specific publications (we don't have realtime / vault)
DROP PUBLICATION IF EXISTS supabase_realtime;
DROP PUBLICATION IF EXISTS supabase_realtime_messages_publication;
SQL

Strip the PG17-only SET transaction_timeout line from the dump:

sed -i 's/^SET transaction_timeout = 0;$/-- transaction_timeout requires PG17 (we are on PG16); skipped/' \
  supabase/captured/captured_schema.sql

Apply schema and data (idempotent — re-runs are safe):

psql -U routecommerce -h 127.0.0.1 -d route_commerce -v ON_ERROR_STOP=0 \
  -f supabase/captured/captured_schema.sql 2>&1 | tail -20

psql -U routecommerce -h 127.0.0.1 -d route_commerce -v ON_ERROR_STOP=0 \
  -f supabase/captured/captured_data.sql 2>&1 | tail -20

Most errors on re-run are "already exists" — that's expected because the dump is idempotent for CREATE statements (we used IF NOT EXISTS where possible, but pg_dump doesn't add it for everything).

Verify

psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT count(*) FROM pg_tables WHERE schemaname='public';"
# Expect: 65

psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT count(*) FROM pg_proc WHERE pronamespace=(SELECT oid FROM pg_namespace WHERE nspname='public');"
# Expect: ~250+

psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT name, slug, created_at FROM brands ORDER BY created_at;"
# Expect 5 brands: Tuxedo Corn, Indian River Direct, Sunrise Farms, Green Valley Organics, Orchard Fresh

What Didn't Work (don't try these)

Approach Why it failed
psql ... db.wnzkhezyhnfzhkhiflrp.supabase.co:5432 Hostname is IPv6-only, dev box has no IPv6
aws-0-us-east-1.pooler.supabase.com "tenant/user not found" — wrong region (project is on aws-1)
pg_dump 16.x against PG 17 server "server version mismatch" — fatal error
supabase db dump --linked Requires Docker (we don't have it)

Notes

  • The captured data includes 3 test brands (Sunrise, Green Valley, Orchard) created on 2026-06-03 — these were test data the user added during the migration work, not real customers. They can be deleted safely.
  • Tuxedo Corn and Indian River Direct are the real production brands.
  • The schema dump is committed to git at supabase/captured/captured_schema.sql.gz for reproducibility. The data dump is gitignored (regenerate as needed).
  • After dump, the local PostgREST needs a schema cache reload — restart the postgrest process or it'll serve stale metadata for ~30 seconds.