- 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')
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.gzfor 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.