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/:
What synthetic seeding does¶
just seed-branch is end-to-end idempotent and brings a fresh preview branch to a fully-usable state:
- Applies any missing migrations — Supabase's automated migration replay sometimes silently stops partway. This step diffs the branch's
supabase_migrations.schema_migrationsagainst local files and applies whatever's missing viapsql. Skips migrations already on the branch (handles the case where production has migrations newer than your worktree). - Creates dev users from
supabase/seed.sql. Local Docker usesadmin@admin.com/adminanduser@user.com/user. Preview branches inherit hosted password policy;just seed-branchgeneratesBACKOFFICE_ADMIN_PASSWORDinbackend/.env.localand wires dev auto-login. Do not print the password. - Calls
SELECT public.seed_synthetic();once. The function is installed by a migration and inserts deterministic synthetic data in foreign-key order. - Refreshes derived analytics — calls
refresh_mv_widget_visitors_safe()for themv_widget_visitorstable, then refreshes themv_conversation_visitorsandmv_client_stats_30dmaterialized 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, allenvironment = '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_personsandemail_only_personsdepending on the domain).
That's enough to populate Home, Conversations, Visitors, Accounts, Analytics pages without empty states.
Adding a new table or column¶
- Create a new migration replacing
public.seed_synthetic(); updatepublic.seed_synthetic_clear()with cleanup in reverse foreign-key order. - Respect foreign-key order and use deterministic UUIDs such as
md5(i::text || '<tag>')::uuid. - Use
ON CONFLICT … DO NOTHINGso reruns are idempotent. - If adding a NOT NULL column, update the seed function in the same change.
- Apply and rerun
just seed-localorjust seed-branchon the isolated target; verify the seeded rows and dependent materialized views.
Copying event rows¶
- Copying
session_events/visitor_page_viewsrows (clone, replay, backfill)? Remapsource_event_uuidand bound the scan.session_events.source_event_uuidcarries a FULL unique index (idx_session_events_source_event_uuid_full, IX-3716), so re-inserting a value verbatim conflicts on every row andON CONFLICT DO NOTHINGdrops 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 bysite_domain+ the timestamp column too, neversession_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.