Seed Data and Fixtures
Overview
Produce realistic data for demos, dev environments, and tests — before lorem ipsum makes it to a screenshot. Output is .forge/seed-data.md (the entity inventory, distributions, demo scenarios, and the factory contract) plus the seed scripts themselves. Seeds must be realistic (real-feeling names, plausible timestamps, domain-appropriate content), comprehensive (edge cases included), reproducible (seeded RNG), and idempotent (re-runnable without dupes). Consumed by incremental-implementation (factories for tests), demo-narrative (demo scenes), and testing-strategy (golden-test fixtures).
When to Use
- A new dev environment is being set up and there's no seed data
- A demo is coming up and the screenshots have lorem ipsum or "Test User"
- Test fixtures are inconsistent across the test suite — every test rebuilds its own data
- A bug only reproduces with specific data and no seed covers that case
- Multiple developers are stepping on each other's seed data in shared dev DB
When NOT to Use
- Production data migration — that's
database-design + a one-off backfill ADR
- Schema design — that's
database-design
- Loading anonymized production data for analytics — separate concern, distinct safety rules
Common Rationalizations
| Thought |
Reality |
| "Lorem ipsum is fine for now" |
Lorem ipsum in a demo makes the product look fake. The buyer notices in 4 seconds and you've already lost the deal. |
| "Just use 'Test User'" |
Homogeneous seed data hides bugs that only appear with real-world variance — long names, accents, no last name, multi-byte chars, all-caps. |
| "Seeds don't need to be idempotent" |
Non-idempotent seeds crash on the second run and corrupt the dev DB. Every developer ends up with a different shape. |
| "Edge cases in seed data are overkill" |
The bug you ship is always in the edge case you didn't seed. Empty list, single item, 10k items, missing optional field, deleted-but-referenced. |
| "We can use production data, it's anonymized" |
Anonymization is not a checkbox. Re-identification from indirect identifiers is trivial. Use synthesized data. |
| "Timestamps don't matter, just use now()" |
All-now() timestamps hide ordering bugs, time-based logic bugs, and "what does this look like with a year of history" bugs. |
Red Flags
- Lorem ipsum visible in a screenshot, demo, or recording
- Every seeded user named "John Doe", "Test User", or
user1 / user2
- All
created_at timestamps within seconds of each other
- A seed script that errors on a second run (unique constraint violation)
- No edge-case scenarios: no empty states, no overflow, no missing-optional-field
- Production PII (real emails, names, phone numbers) checked into the seed file
- A demo that requires the demo-runner to "set up the data manually first"
- Factories that don't allow overrides — every test gets the same default user
Core Process
Step 1: Inventory entities needing seed
Read .forge/architecture.md and .forge/database-design.md. List every entity that needs seed data, with the minimum count for a realistic dev environment (e.g., 50 users, 200 tasks per user, 20 organizations, 5k events).
Step 2: Define realistic distributions per field
For each entity, decide what "realistic" means:
| Field |
Naive |
Realistic |
| Name |
"John Doe" |
A pool of 200+ first + last names spanning cultures, lengths (1 char to 30+), with hyphens, apostrophes, multi-byte chars |
| Email |
user1@test.com |
firstname.lastname@<plausible-domain> with the same distribution as names |
| Timestamp |
now() |
Spread across the last 90 days with a realistic curve (more recent = denser) |
| Status |
All active |
Weighted: 70% active, 15% pending, 10% archived, 5% suspended |
| Content |
"lorem ipsum" |
Domain-appropriate (e.g., task titles like "Review Q3 budget", "Onboard new hire", "Fix flaky integration test") |
| Count fields |
All 0 or all 100 |
Long-tail distribution: many users with 1-5 items, a few with 50+, one with 500+ |
Step 3: Write factories with overrides
Every entity has a factory: createUser(overrides?: Partial<User>): User. Defaults from the distribution; overrides allow tests and demos to pin specific fields.
const user = createUser({ email: "demo@example.com", status: "active" });
const power = createUser({ taskCount: 500 }); // edge case
Factories use a seeded RNG (e.g., seedrandom) so the same seed produces the same data — critical for reproducible demos.
Step 4: Write demo-specific scenarios
For each demo scene in .forge/demo-narrative.md (or for each test-needed shape):
- Happy path scenario — the buyer-facing flow
- Empty state scenario — fresh user, no data
- Single-item scenario — one of everything
- Overflow scenario — long names, long content, many items
- Error state scenario — failed payment, expired card, locked account
Each scenario is a named function: seedDemoOnboardingScene(), seedDemoPowerUserScene(). The demo script runs the named scene.
Step 5: Make idempotent
Seeds upsert by a stable key — typically a slug or a UUIDv5 derived from a deterministic input. Re-running the script reconciles: missing rows are inserted, existing rows are updated, drift is corrected.
upsert("users", { stable_id: "demo:onboarding:alex" }, { ... });
The seed script can be run before every demo with confidence.
Step 6: Version + document in .forge/seed-data.md
- Inventory of entities + counts
- Distribution rules per field
- Factory API summary
- Named demo scenarios
- The stable-key naming convention
- The PII safety rule (no production data, ever)
Verification
1---2name: seed-data-and-fixtures3description: Use when creating demo data, test fixtures, seed scripts, or factory functions, when preparing for a sales demo and the screenshots are full of "John Doe", when test data is missing edge cases, or when seed scripts crash on a second run.4---56# Seed Data and Fixtures78## Overview910Produce realistic data for demos, dev environments, and tests — *before* lorem ipsum makes it to a screenshot. Output is `.forge/seed-data.md` (the entity inventory, distributions, demo scenarios, and the factory contract) plus the seed scripts themselves. Seeds must be **realistic** (real-feeling names, plausible timestamps, domain-appropriate content), **comprehensive** (edge cases included), **reproducible** (seeded RNG), and **idempotent** (re-runnable without dupes). Consumed by `incremental-implementation` (factories for tests), `demo-narrative` (demo scenes), and `testing-strategy` (golden-test fixtures).1112## When to Use1314- A new dev environment is being set up and there's no seed data15- A demo is coming up and the screenshots have lorem ipsum or "Test User"16- Test fixtures are inconsistent across the test suite — every test rebuilds its own data17- A bug only reproduces with specific data and no seed covers that case18- Multiple developers are stepping on each other's seed data in shared dev DB1920## When NOT to Use2122- Production data migration — that's `database-design` + a one-off backfill ADR23- Schema design — that's `database-design`24- Loading anonymized production data for analytics — separate concern, distinct safety rules2526## Common Rationalizations2728| Thought | Reality |29|---------|---------|30| "Lorem ipsum is fine for now" | Lorem ipsum in a demo makes the product look fake. The buyer notices in 4 seconds and you've already lost the deal. |31| "Just use 'Test User'" | Homogeneous seed data hides bugs that only appear with real-world variance — long names, accents, no last name, multi-byte chars, all-caps. |32| "Seeds don't need to be idempotent" | Non-idempotent seeds crash on the second run and corrupt the dev DB. Every developer ends up with a different shape. |33| "Edge cases in seed data are overkill" | The bug you ship is always in the edge case you didn't seed. Empty list, single item, 10k items, missing optional field, deleted-but-referenced. |34| "We can use production data, it's anonymized" | Anonymization is not a checkbox. Re-identification from indirect identifiers is trivial. Use synthesized data. |35| "Timestamps don't matter, just use now()" | All-now() timestamps hide ordering bugs, time-based logic bugs, and "what does this look like with a year of history" bugs. |3637## Red Flags3839- Lorem ipsum visible in a screenshot, demo, or recording40- Every seeded user named "John Doe", "Test User", or `user1` / `user2`41- All `created_at` timestamps within seconds of each other42- A seed script that errors on a second run (unique constraint violation)43- No edge-case scenarios: no empty states, no overflow, no missing-optional-field44- Production PII (real emails, names, phone numbers) checked into the seed file45- A demo that requires the demo-runner to "set up the data manually first"46- Factories that don't allow overrides — every test gets the same default user4748## Core Process4950### Step 1: Inventory entities needing seed5152Read `.forge/architecture.md` and `.forge/database-design.md`. List every entity that needs seed data, with the minimum count for a realistic dev environment (e.g., 50 users, 200 tasks per user, 20 organizations, 5k events).5354### Step 2: Define realistic distributions per field5556For each entity, decide what "realistic" means:5758| Field | Naive | Realistic |59|---|---|---|60| Name | "John Doe" | A pool of 200+ first + last names spanning cultures, lengths (1 char to 30+), with hyphens, apostrophes, multi-byte chars |61| Email | `user1@test.com` | `firstname.lastname@<plausible-domain>` with the same distribution as names |62| Timestamp | `now()` | Spread across the last 90 days with a realistic curve (more recent = denser) |63| Status | All `active` | Weighted: 70% active, 15% pending, 10% archived, 5% suspended |64| Content | "lorem ipsum" | Domain-appropriate (e.g., task titles like "Review Q3 budget", "Onboard new hire", "Fix flaky integration test") |65| Count fields | All 0 or all 100 | Long-tail distribution: many users with 1-5 items, a few with 50+, one with 500+ |6667### Step 3: Write factories with overrides6869Every entity has a factory: `createUser(overrides?: Partial<User>): User`. Defaults from the distribution; overrides allow tests and demos to pin specific fields.7071```ts72const user = createUser({ email: "demo@example.com", status: "active" });73const power = createUser({ taskCount: 500 }); // edge case74```7576Factories use a seeded RNG (e.g., `seedrandom`) so the same seed produces the same data — critical for reproducible demos.7778### Step 4: Write demo-specific scenarios7980For each demo scene in `.forge/demo-narrative.md` (or for each test-needed shape):8182- **Happy path scenario** — the buyer-facing flow83- **Empty state scenario** — fresh user, no data84- **Single-item scenario** — one of everything85- **Overflow scenario** — long names, long content, many items86- **Error state scenario** — failed payment, expired card, locked account8788Each scenario is a named function: `seedDemoOnboardingScene()`, `seedDemoPowerUserScene()`. The demo script runs the named scene.8990### Step 5: Make idempotent9192Seeds upsert by a **stable key** — typically a slug or a UUIDv5 derived from a deterministic input. Re-running the script reconciles: missing rows are inserted, existing rows are updated, drift is corrected.9394```ts95upsert("users", { stable_id: "demo:onboarding:alex" }, { ... });96```9798The seed script can be run before every demo with confidence.99100### Step 6: Version + document in `.forge/seed-data.md`101102- Inventory of entities + counts103- Distribution rules per field104- Factory API summary105- Named demo scenarios106- The stable-key naming convention107- The PII safety rule (no production data, ever)108109## Verification110111- [ ] `.forge/seed-data.md` written112- [ ] Zero lorem ipsum anywhere in seed output (grep verified)113- [ ] No `John Doe`, `Test User`, or `user1` in any seeded user114- [ ] Names span cultures, lengths, and character sets115- [ ] Timestamps spread across a realistic window with a realistic curve116- [ ] Every demo scene in `.forge/demo-narrative.md` has a corresponding `seedDemo<Scene>()` function117- [ ] Edge cases seeded: empty list, single item, overflow, error state118- [ ] Seed script runs twice without errors (idempotency tested in CI)119- [ ] No production PII (real emails, names, phone numbers) checked into the seed file120- [ ] Factories accept overrides for every field