Every other option makes you
pick two. babystack doesn't.
Real engine. Isolated per test. Parallel-safe. Committed writes, DDL, and constraints. Zero test-code changes. An undo button for agents. Pick the shared DB and you lose isolation; pick rollback and you lose real DDL; pick SQLite and you lose the engine. babystack is the one row that's all yeses.
Honest by rule — no "instant" claims. The speed numbers below are measured, with method.
Six ways to get a test database. One that doesn't compromise.
Each alternative trades away something real. Read across the rows — only the last one keeps every column.
| Approach | Real MySQL engine |
Isolated per test (parallel-safe) |
Committed writes + DDL + constraints |
Zero test-code changes |
Seeded baseline, cached & reused |
Agent reset loop |
|---|---|---|---|---|---|---|
| Shared dev / CI database | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Transaction rollback | ✓ | ~ | ✗ | ✗ | ✓ | ✗ |
| SQLite / in-memory swap | ✗ | ✓ | ~ | ✗ | ✓ | ✗ |
| Mocked data layer | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ |
| Hand-rolled docker-compose | ✓ | ~ | ✓ | ~ | ~ | ✗ |
| Raw Testcontainers | ✓ | ~ | ✓ | ✗ | ~ | ✗ |
| babystack | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
The compromise, spelled out.
Shared dev / CI database
Real, but one copy. Tests race each other, can't run in parallel, and leave state behind — the flaky suite everyone dreads. Reset is a manual script.
Transaction rollback
Fast and isolated within one connection — but your code can't test its own transactions, DDL breaks the wrapper, and anything opening a second connection sees nothing. You're testing a fiction.
SQLite / in-memory
Not your engine. Different SQL dialect, different constraint semantics, different JSON/enum/collation behaviour — bugs that only appear in prod sail through green.
Mocks
You assert against your own assumptions. The moment the real query, constraint, or migration disagrees, the mock still passes. Tests that can't fail for the right reason.
Hand-rolled docker-compose
Real engine — but you write and maintain 150 lines of compose + setup/teardown + per-worker wiring + baseline caching, in every repo. babystack is that, as config.
Raw Testcontainers
Great primitive — but the lifecycle lives in your test code, and per-worker isolation, a seeded+cached baseline, and an agent loop are all yours to build. babystack rides on top of exactly this.
Four things nothing else gives you together.
It's the actual engine.
A real mysql:8.4 over a normal
DATABASE_URL. Committed writes, DDL, triggers,
and a real UNIQUE 409 — all behave exactly as
they will in production, because they are production's engine.
A fresh DB per worker.
One container, a database per Vitest worker, reloaded from the baseline per test file. Suites run in parallel with zero cross-talk — no shared state, no "works alone, fails together."
Your tests don't know.
One babystack.config.ts, three lines in
vitest.config.ts. No test file imports babystack
or manages a database. Rip it out and the tests still read
DATABASE_URL.
An undo button for agents.
baby wake · home · reset · sleep — a persistent
seeded MySQL a coding agent can break across many commands, then
reset to pristine. The URL never changes.
Fast enough for CI — and we'll show our work.
babystack will never claim an "instant" clone: MySQL has no in-server template, so a real engine boots in seconds. The trick is paying that cost once per run, not per test. Here's the real example app suite, measured end-to-end.
Cold run, end-to-end. No cache: boot real MySQL, build the seeded baseline, run all tests. Image already pulled.
Warm run (cache hit). Baseline restored from the cached dump — the build step is skipped entirely.
Per test file. A fresh, seeded, isolated database handed to each file once the engine is warm.
Container boots. Once — not per test, not per file, not per worker. That single boot is most of the wall-clock.
Method. examples/users-api — 3
files / 4 tests, Vitest 4 on forked workers, real
mysql:8.4. Measured on a MacBook via Colima
(2-CPU / 4 GB VM), Docker 29.5, Node 24. The cache's win scales with your
migration + seed size — this example's seed is tiny, so it only saves ~1.4s here; a
real schema saves far more. Numbers are a snapshot, not a leaderboard — reproduce them
with pnpm --filter users-api test.
babystack sits on top of the good primitives.
It's not a rival to Docker, Testcontainers, or LocalStack — it's the opinionated, config-first, zero-test-code, agent-ready layer on top of them.
Docker — babystack shells out to the real
docker CLI to run the real engine.
The same idea as Testcontainers — and reserves the seam to adopt a MySQL snapshot driver as a faster reset path if one ships.
LocalStack for the AWS tail — babystack never reimplements a proprietary API. A platform feature is an upgrade, not an obituary.
The durable value isn't any single primitive — it's config-first, whole-stack, zero-test-code, agent DX, delivered the same way to tests, CI, local dev, and agents.
Delete the test-database plumbing.
One config file, and every run gets a fresh, disposable copy of the real services your app uses — a local baby of your production stack. Real MySQL today; more engines next.