why babystack

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.

01 · the honest comparison

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
yes ~ only if you build & maintain it yourself no
02 · what each one actually costs you

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.

03 · so, why babystack

Four things nothing else gives you together.

real, never emulated

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.

isolated, in parallel

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."

zero test-code changes

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.

agent-native

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.

04 · honest speed

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.

~11.7s

Cold run, end-to-end. No cache: boot real MySQL, build the seeded baseline, run all tests. Image already pulled.

~10.3s

Warm run (cache hit). Baseline restored from the cached dump — the build step is skipped entirely.

~250ms

Per test file. A fresh, seeded, isolated database handed to each file once the engine is warm.

1×

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.

05 · compose, don't compete

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.

consumes

Docker — babystack shells out to the real docker CLI to run the real engine.

rides on

The same idea as Testcontainers — and reserves the seam to adopt a MySQL snapshot driver as a faster reset path if one ships.

delegates to

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.