Skip to content

Seeding the Supabase DB

There are two seed paths, both available against local Supabase and against preview branches:

Path Source Speed PII Use when
Synthetic (default) public.seed_synthetic() ~1–3 s none Day-to-day dev, CI smoke tests, onboarding new devs without prod access
Copy (opt-in) scripts/seed_local_db.py → production REST API ~60 s yes (real visitors, emails, chat content) Reproducing a real client bug, validating against real-world data shapes

Targets

just target DB target Source
just seed-local Local Supabase (port 54322) Synthetic
just seed-local-fresh Local Supabase Synthetic, --clear first
just seed-local-copy Local Supabase Prod copy (PII)
just seed-branch Linked preview branch Synthetic + migrations push + seed.sql + MV refresh
just seed-branch-copy Linked preview branch Prod copy (PII)

Run from supabase/:

cd supabase
just seed-local         # local
just seed-branch        # preview branch

What synthetic seeding does

just seed-branch is end-to-end idempotent and brings a fresh preview branch to a fully-usable state:

  1. Applies any missing migrations — Supabase's automated migration replay sometimes silently stops partway. This step diffs the branch's supabase_migrations.schema_migrations against local files and applies whatever's missing via psql. Skips migrations already on the branch (handles the case where production has migrations newer than your worktree).
  2. Creates dev users from supabase/seed.sql. Local Docker uses admin@admin.com / admin and user@user.com / user. Preview branches inherit hosted password policy; just seed-branch generates BACKOFFICE_ADMIN_PASSWORD in backend/.env.local and wires dev auto-login. Do not print the password.
  3. Calls SELECT public.seed_synthetic(); once. The function is installed by a migration and inserts deterministic synthetic data in foreign-key order.
  4. Refreshes derived analytics — calls refresh_mv_widget_visitors_safe() for the mv_widget_visitors table, then refreshes the mv_conversation_visitors and mv_client_stats_30d materialized views in dependency order.

just seed-local first applies pending local migrations with supabase migration up --include-all, then calls the seeding function and refreshes derived analytics. It does not use remote db push or create auth users; local auth users come from seed.sql during local database initialization. A database reset is not a prerequisite for seeding. --clear calls public.seed_synthetic_clear() after migrations and before seeding.

If migration up fails with MigrationMissingLocalError listing hundreds of versions, supabase start restored a local database seeded before the squashed baseline. Run just reset (drops local data), then just seed-local.

Implementation

public.seed_synthetic() and public.seed_synthetic_clear() are defined by migrations. The SQL files under supabase/seed_synthetic/ are historical source material; editing them does not update the executed function. See the package's supabase/seed_synthetic/README.md.

The functions also exist in production after migration deployment but are inert there. Run them only through recipes targeting local or preview databases.

Synthetic dataset shape

4 fake domains, each owned by its own client:

Client Domain Default language Brand color
Acme acme.com en #0A42C3
Contoso contoso.com en #1E8E3E
Fabrikam fabrikam.com fr #D93025
Demo demo.local en #9334E6

After a full seed + MV refresh, each domain shows roughly:

  • 25 conversations under last_message_at ≤ 8h ago, all environment = 'production', deployment_target = 'widget'.
  • 31 widget visitors (after the 10s-duration filter in mv_widget_visitors).
  • ~80% engagement rate.
  • 20% conversion rate (5 converted persons per client, split between demo_persons and email_only_persons depending on the domain).

That's enough to populate Home, Conversations, Visitors, Accounts, Analytics pages without empty states.

Adding a new table or column

  1. Create a new migration replacing public.seed_synthetic(); update public.seed_synthetic_clear() with cleanup in reverse foreign-key order.
  2. Respect foreign-key order and use deterministic UUIDs such as md5(i::text || '<tag>')::uuid.
  3. Use ON CONFLICT … DO NOTHING so reruns are idempotent.
  4. If adding a NOT NULL column, update the seed function in the same change.
  5. Apply and rerun just seed-local or just seed-branch on the isolated target; verify the seeded rows and dependent materialized views.

Copying event rows

  • Copying session_events / visitor_page_views rows (clone, replay, backfill)? Remap source_event_uuid and bound the scan. session_events.source_event_uuid carries a FULL unique index (idx_session_events_source_event_uuid_full, IX-3716), so re-inserting a value verbatim conflicts on every row and ON CONFLICT DO NOTHING drops the whole batch with no error — a green run that inserted 0 rows. Remap it (md5(source_event_uuid::text || <salt>)::uuid). Filter the copy by site_domain + the timestamp column too, never session_id IN (…) alone: (site_domain, event_at) is the only index keeping it off a full scan of the largest table in the DB (that omission alone blew the dashboard's upstream timeout). Worked example: supabase/ops/seed_demo_tenant_from_tenant.sql.

When NOT to use the synthetic seed

The synthetic data has lorem-style messages, fake company names, and no LLM-classified topics / keywords. So:

  • Reproducing an ixchat eval bug → use *-copy (real conversations).
  • Reproducing a specific client's quirky data (e.g. Pennylane intent distributions) → use *-copy.
  • Performance / load testing → write a dedicated bigger-volume seed; the 200-visitor default isn't enough.

Configs (config.configs)

The 16 config-slug rows are not part of the seed. They are populated by migrations (20260220150940_seed_config_definitions.sql + ~10 follow-ups), so they exist on every fresh branch automatically. JSONB defaults live in schemas/configs/*.schema.json and reach runtime through the generated SLUG_TO_DEFAULTS constant — don't duplicate them in synthetic SQL.

config.client_configs per-domain overrides come from public.seed_synthetic() (identity + appearance, mirroring what create_domain() does internally).

See also

  • Supabase Preview Branches — lifecycle, branch creation, troubleshooting.
  • supabase/seed_synthetic/README.md — quick reference inside the seed directory.
  • docs/plans/seed-synthetic-data.md — original design doc.
  • docs/plans/seed-pgdump-study.md — study of how to speed up the prod-copy path.