Oct 27, 2025·7 min read

Reproducible local dev environment with seed data and fixtures

Set up a reproducible local dev environment with seed data and fixtures so anyone can run the app locally with the same database, fast and reliably.

Reproducible local dev environment with seed data and fixtures

Why local databases become unpredictable

A local database starts clean on day one. Then everyone touches it. A teammate runs a migration, you import a CSV, someone tests a feature that creates 2,000 rows, and suddenly the app behaves differently on every laptop.

That’s how a reproducible local dev environment quietly breaks. The code might be the same, but the data isn’t.

When every developer has different local data, small things get confusing fast. A screen that looks fine for one person shows errors for another. Search feels “slow” only on one machine. A signup flow “works” only because one database already has the right roles, flags, or demo accounts.

A lot of “it works on my machine” starts with the database because the database carries hidden state: migrations applied in a different order, rows created by old experiments, missing records the app assumes exist (like an admin user or default settings), and even secrets or API keys that ended up in the wrong place.

Manual setup becomes a bottleneck as soon as you onboard someone new or need a clean reset for a bug hunt. If your setup doc includes steps like “create these 12 records by hand” or “ask someone for a database dump,” it will fail sooner or later.

Seed data is the baseline data your app needs to run locally (a few users, plans, and feature flags). Fixtures are small, specific datasets meant to cover a scenario (like “user with an expired subscription” or “order with a refund”) so you can reliably test a screen or API.

If you’ve inherited an AI-generated prototype that “mostly works,” this is often where it falls apart. You’ll see an app that only runs on the original creator’s machine because the database was never made rebuildable. The fix starts by making data predictable, not by guessing what’s missing.

Seed data and fixtures: what to use and when

A reproducible local dev environment starts with one decision: are you trying to help people use the app manually, or verify behavior automatically? That choice tells you whether you need seed data, fixtures, or both.

Think of it like this:

  • Migrations change the database shape (tables, columns, indexes). They shouldn’t depend on example users or sample orders.
  • Seed data gives developers a predictable baseline so the UI isn’t empty and common flows are easy to click through.
  • Fixtures give tests known inputs so automated checks run the same way every time.
  • Factories (optional) create records on demand, often as a more flexible alternative to static fixtures.

Keep your dev database and test database separate. Dev data is for humans exploring features. Test data is for automation and should be isolated, reset often, and safe to run in parallel. Mixing them is how you get tests that pass on one laptop and fail everywhere else.

When you’re choosing what should look “real,” aim for realism where it affects logic, not where it adds noise. Keep roles, permissions, and edge-case statuses realistic, but use fake names, emails, and placeholder payment details.

A practical rule set is simple: seed a small number of complete flows (2-3 users, 1 org, a handful of records per screen). Add one or two intentionally odd cases (expired token, disabled account, empty state) to hit UI branches. Avoid huge blobs like images or giant logs; use stubs that still trigger the same code paths. Never seed secrets. And when it helps, make IDs and timestamps deterministic so screenshots and debugging match across machines.

Design a local setup you can rebuild anytime

A setup is only “simple” if you can delete everything and get back to a working app without remembering secret steps. That’s the heart of a reproducible local dev environment: a fresh machine should behave like your machine.

Pick one command that creates the database from scratch, and make it safe to run repeatedly, even if the database already exists. New contributors shouldn’t have to guess which scripts to run in what order.

Most reliable reset commands do the same things every time: wipe or recreate the local database, run migrations, load seed data for everyday development, start any required services, and print the next step (including how to log in).

Keep the schema source of truth in migrations, not in hand-edited SQL in a wiki or one-off “fix” commands people run once. If someone changes a table locally and forgets to capture it in a migration, you’ll get mystery bugs where “it works on my laptop” becomes normal.

Create a dedicated local database user with limited permissions. It sounds like extra work, but it catches real mistakes early. For example, if your app accidentally tries to create tables at runtime, a locked-down user will fail fast instead of silently hiding the issue until production.

Optional services are where setups usually get messy. Decide what’s required and what’s nice to have. If Redis, S3, or email is optional, make the app start without them and show a clear message when a feature is unavailable. A common approach is to support “fake” local versions (file storage instead of S3, a local inbox instead of real email) and enable real integrations only when a developer opts in.

Write seed scripts that always produce the same data

A seed script is only helpful if it’s boring. Run it today, next week, or on a fresh laptop, and you should get the same records, the same logins, and the same demo screens.

Choose a fixed seeding order based on dependencies. If projects belong to orgs, and orgs belong to users, seed users first, then orgs, then projects, then anything attached to projects (tasks, invoices, comments). This avoids “missing parent” errors and keeps foreign keys consistent.

Use stable identifiers or natural keys so the data doesn’t drift. Anything you refer to later should have a permanent identifier (user email, org slug, or a fixed UUID you hardcode in the seed). Avoid random names, random UUIDs, or “insert and hope it gets id 3,” because reruns and different database engines will change outcomes.

Make the script idempotent, meaning you can rerun it without creating duplicates. Instead of always inserting, upsert by the natural key (email, slug, external_id). If you need a full reset, do it intentionally (wipe tables first, or support a --reset flag), not accidentally.

It also helps to keep seed settings in one place: counts (how many orgs/projects/records), feature flags, fixed demo credentials, and any environment toggles (like fast seed vs full seed). When those values are scattered, every machine ends up “close, but different.”

Build fixtures that cover real UI and API cases

Make local setup reproducible
Turn a flaky prototype into a rebuildable dev setup with predictable seeds and fixtures.

Fixtures are small, known sets of data you load so screens and endpoints behave the same every time.

Start with a few realistic records tied to your most-used flows. Think in clicks: log in, land on a dashboard, view a list, open a detail page, save a change. If your app has organizations and projects, one org with two projects, a couple of tasks, and one recent activity item is often enough to light up most UI and API paths without creating a giant dataset.

Then add edge cases on purpose. You don’t need many, but you do want the ones that commonly break:

  • An empty state (a new org with no projects)
  • A long name (layout and truncation)
  • A disabled user (access and messaging)
  • Missing optional fields (null handling)
  • A permission boundary (can view but not edit)

Keep fixtures easy to read and easy to diff in code review. YAML, JSON, or a small TypeScript file is fine. Pick one style and stick to it. Use stable IDs and timestamps when possible, so snapshots, sorting, and “recent activity” widgets don’t randomly change.

Finally, document intent right where the data lives. A short comment like “Used for Settings page empty state” saves time later.

Step by step: one command that sets up everything

A reproducible local dev environment feels almost effortless when onboarding: someone runs one command and doesn’t need “special setup steps” from chat.

Your single setup command should do the same sequence every time:

  1. Reset the local database safely. Use a dedicated dev database name and a loud safety check (for example, refuse to run if NODE_ENV=production).
  2. Apply migrations from scratch. The schema should only be created through migrations so the database matches what CI and production expect.
  3. Load a small, stable baseline dataset. Insert only what the app needs to boot (roles, feature flags, a few products, one org).
  4. Create usable local credentials. Seed a couple of known logins (admin and normal user) and any fake API keys the app expects, for local use only.
  5. Run a quick smoke test. Hit one endpoint, render one key page, or run a tiny test file so failures show up immediately.

A concrete pattern is to wrap this in one script that developers run from the repo root:

./dev/setup

That script can print what it did and what to try next, for example: “Login as admin: [email protected] / password123” and “Run: ./dev/smoke”. Keep the output short and practical.

Make onboarding scripts friendly for new contributors

A good onboarding script feels like a helpful guide, not a puzzle. New contributors should be able to clone the repo, run one command, and get a working app without asking for secret steps.

Make the reset path safe and obvious. If someone runs a reset command, it should target local resources only and say what it will delete before doing it. A simple safety check (or requiring a --yes flag) prevents accidents.

Support environment variables, but don’t make people hunt for them. Provide sensible defaults for common values like database name, port, and admin email. If your app truly needs a real value (like an API key), fail fast and tell them exactly what to do.

Small details matter. After a successful run, print a short summary: database created, migrations applied, seed users added, and the exact login details (email, password, role). If your app has multiple services, print where each one is running and what to check if a port is busy.

Common mistakes that waste hours

Stop it works on my machine
We repair broken migrations, seed scripts, and fixtures so a fresh clone actually runs.

Most local setup pain comes from small shortcuts that feel fine for one person, then collapse when a second contributor joins.

Avoid these common traps:

  • Manual database poking. A quick SQL edit or a shared database dump goes stale fast, and a fresh install won’t match the “working” machine.
  • Random seed data with no fixed seed. If IDs, usernames, or timestamps change every run, you’ll end up with flaky bugs and flaky tests.
  • Copying real secrets or production data. Passing around .env files, checking in keys, or dumping production rows into local creates security issues and confusing behavior.
  • Fixtures drifting from the schema. Setup “succeeds,” but pages crash later because fixture fields don’t match current migrations.
  • Setup that requires clicking through the UI. “Just sign up and create a project” sounds simple until it becomes 15 clicks in a fragile order.

A common scenario is login breaking because the seed created random passwords while fixtures still reference an old schema field. Someone spends an hour debugging auth when the real problem is inconsistent setup.

Quick checks before you call it reproducible

A setup is only reproducible when someone new can get the same result you get, without reading your mind.

The 5-minute clone test

Take a clean machine (or a fresh folder) and act like you know nothing about the project. Your goal is a working app starting from zero.

You should be able to confirm, quickly:

  • A new person can run setup without guessing missing steps.
  • The database can be wiped and rebuilt from scratch in under 5 minutes on an average laptop.
  • Seed scripts can be rerun without duplicates, broken foreign keys, or random failures.
  • You always end up with at least one working login plus a couple of realistic scenarios (admin user, normal user, and a “no data yet” state).
  • Tests run against a clean, separate database (not the dev database).

If any item fails, write down the exact point of confusion and fix that next.

Check for hidden state

Most “it works on my machine” problems come from state that lives outside your scripts: a manually created user, a local file with secrets, a one-off migration that never ran, or data left over from last week.

A quick way to catch this is to rebuild twice in a row: reset the database, run setup, start the app, then reset and do it again. The second run should look identical to the first.

Concrete example: if your seed creates a user like [email protected], the script should upsert or recreate it cleanly every time, and the password should be documented in one place.

Example: onboarding a new contributor in 30 minutes

Rescue a one-person dev environment
If your app only runs on one laptop, we’ll pinpoint the missing steps and data.

A new contractor joins on Monday morning. They have the repo, but no context, no existing database, and no time to hand-create accounts and sample records. The goal is simple: they should be productive the same day, with the same starting data as everyone else.

They follow the README and run one command, for example make dev-reset or npm run dev:reset. That command drops the local database, recreates it, runs migrations, loads seed data, and installs a small set of fixtures. When it finishes, the app boots with a predictable login.

The contractor signs in using a seeded account like [email protected] with a known password. The account is already linked to an organization, a workspace, and two projects. One project includes “real enough” relationships: a few users, a couple of roles, one paid invoice, one failed payment, and an item with comments. Key screens load immediately without manual setup.

Within 30 minutes, they can run setup, log in successfully, open the dashboard and project detail views, trigger a known edge case (like a “payment failed” banner), and reproduce a reported bug against the same fixture set everyone else uses.

When the schema changes, treat fixtures like code, not sample junk. A simple habit helps: update migrations first, then update seed scripts (keeping IDs and timestamps stable), then refresh fixtures and add a quick smoke check (log in, load the top few screens).

Next steps: keep it stable as the app grows

As features pile up, local setup is often the first thing to rot. The best defense is to keep your “minimum usable data” list short and written down. Think of the smallest set of records needed to use the app end to end: one user who can log in, one workspace or project, a couple of real-looking items, and whatever roles or settings make the main screens work.

Once you know that minimum, protect it with two habits: a reset command people actually use, and a small baseline fixture set that only changes when the product truly changes.

A lightweight maintenance routine helps too: once a month, rebuild from scratch on a clean machine (or a fresh container/VM) and time it. If it takes longer than 10 to 15 minutes or requires hand edits, fix it immediately. This is also a good moment to scan for secrets in seed files and confirm auth works with a brand-new database.

If you’re dealing with an AI-generated codebase that only works on one laptop, it’s usually not just “missing data.” It’s mismatched migrations, brittle auth defaults, and scripts that only succeed once. FixMyMess (fixmymess.ai) focuses on diagnosing and repairing issues like this, especially for prototypes created with tools like Lovable, Bolt, v0, Cursor, and Replit, so teams can rebuild their local and test databases predictably and move forward with confidence.

FAQ

What’s the difference between seed data and fixtures?

Seed data is the small baseline your app needs to feel usable in local development, like roles, a couple of users, and one sample workspace.

Fixtures are specific, known datasets meant to prove a scenario behaves the same every time, usually for tests or for reproducing a bug. If you’re clicking around the UI, start with seed data; if you’re verifying behavior automatically, rely on fixtures (or factories).

How much seed data should we include so the app isn’t empty?

Aim for the smallest set that makes the main flows work end-to-end. A few users (admin and normal), one org/project, and a handful of records per key screen is usually enough.

If you add too much, setup gets slow and debugging gets noisy. Add realism where it changes logic (roles, statuses, permissions), not where it just adds volume (giant logs, tons of rows).

How do we prevent seed scripts from creating duplicate records?

Make the seed script idempotent. That means re-running it should produce the same final state without duplicates.

Use upserts keyed by something stable like email, slug, or a fixed UUID you control. If you want a clean slate, do it intentionally with a reset step instead of silently inserting more rows each run.

Should dev and test databases be separate?

Separate them and make it hard to mix them up. Your dev database is for humans exploring features, while your test database should reset often and run in isolation.

If tests run against your dev database, they’ll fail randomly because a teammate’s leftover rows or your own previous runs changed the state.

Why do people recommend deterministic IDs and timestamps in seeds and fixtures?

Stable IDs and timestamps keep behavior consistent across machines and reruns. They stop problems like sorting changing randomly, “recent activity” widgets shifting, or snapshot tests failing for no real reason.

You don’t need to freeze everything, but anything your UI or tests depend on should be predictable.

What should a single “setup/reset” command do?

A good default is one command that recreates the local database, runs migrations, loads seed data, and prints the next step (including how to log in).

Keep it safe to run repeatedly, and include a loud guardrail so it refuses to run against production settings.

How do we handle optional services like Redis, S3, or email locally?

Treat optional services as optional in code, not just in docs. The app should boot without Redis/S3/email if those aren’t required, and it should show a clear message when a feature is unavailable.

For local work, use safe substitutes like file storage or a local inbox so you can develop without signing up for extra infrastructure.

How do we avoid leaking secrets or using production data in local setup?

Do not seed real secrets, API keys, or production data. Use obviously fake values for local-only flows, and make the app fail fast with a clear error when a real key is truly required.

Also avoid copying shared .env files around casually; that’s how credentials leak and behavior becomes inconsistent across machines.

Our app only runs on the original creator’s laptop. What’s the fastest fix?

Start by rebuilding from scratch on a clean database and watching where it fails. Most “only works on one laptop” issues come from hidden state: missing migrations, manual rows, brittle auth defaults, or scripts that only succeed once.

If you inherited an AI-generated project and the setup is messy, FixMyMess can diagnose the codebase and make the database rebuildable so anyone can run a predictable reset and get the same working login.

What do we do when fixtures or seed data drift from the schema after changes?

First, confirm migrations are the only source of truth for the schema. If someone patched a table manually, capture it in a migration so every machine matches.

Next, update fixtures to match the current schema and keep them small. A quick smoke check after setup (log in, load one key page, hit one endpoint) helps you catch drift immediately.