A fast, seeded, disposable real database — on demand.
babystack spins up a real MySQL, loads it with a small seeded baseline, and hands every test its own fresh copy over a normal connection URL — then throws it away. Here's how the whole thing fits together.
pre-alpha · Phase 0 complete (0.1–0.8) · warm-pool speed (0.9) next
Replace the flaky shared test DB.
A fresh, isolated, seeded real MySQL per test run — with zero changes to your test code — instead of hand-rolled compose + reset scripts.
180 lines of docker-compose.test.yml +
setup/teardown, a shared dev DB that flakes, tests that can't run in parallel.
Two lines in vitest.config.ts, one
babystack.config.ts. Tests untouched — they just
read DATABASE_URL, like prod.
Consumers on top, a pure core, adapters below.
Each layer only depends downward; the core does no I/O, so everything above unit-tests with zero Docker.
baby CLI · a coding
agent
@babystack/vitest
built
@babystack/cli
built
baby binary.@babystack/mysql
built
@babystack/docker
built
docker CLI.
mysql:8.4 container
@babystack/core sits beside all
of this: pure config + lifecycle + the
Lease/Pool
types and the injected Clock/CommandRunner
ports. No I/O — which is why every layer above unit-tests with zero
Docker.
Expensive work once; each file does only the cheap part.
This cold-path / hot-path split is the whole performance story.
provision
start mysql:8.4 on an ephemeral 127.0.0.1
port.
waitReady
a real SELECT 1 inside the container — not a
port ping.
buildBaseline
seed → mysqldump → cache the dump + checksum.
acquire lease
key = VITEST_POOL_ID.
fresh DB
babystack_db_wN, loaded from the cached dump.
inject env
set DATABASE_URL before the app
imports; the test runs, reading it exactly like prod.
At the end of the run, teardown disposes the one container (dropping
every worker DB + volume). Killed run? A label-scoped GC to reap orphans exists but
isn't auto-wired yet (it can't safely run beside a live second suite); reap manually
via the babystack label, or
baby sleep a CLI session. Your own containers are
never touched.
A database per worker — across processes.
Isolation is a database per worker inside one
mysqld (not a container each). Vitest workers are
separate processes, so only serializable
coordinates cross the line — each worker rebuilds the adapter and leases its own DB.
createStack
provision → waitReady → buildBaseline, then
provide({ instance, baseline }) — plain JSON.
inject() the coordinates
rebuild the adapter → openLease(POOL_ID) →
babystack_db_w1,
_w2, _w3 …
each isolated.
The cached dump lives on shared disk, so every worker reads it
directly. A fresh reload per file means committed writes, DDL, and
multi-connection behaviour are all real and isolated between files — the correctness
win over BEGIN/ROLLBACK.
Define the seed once. Then it's all cache.
After that, no scripts re-run and no external DB is touched — every copy comes from the cached dump.
build
built
babystack runs your commands (migrate + seed) against its own throwaway container. Dynamic — always your current schema.
sql / dump
Phase 1
babystack loads SQL files you hand it. No migration tooling runs — ORM-agnostic.
from: <url>
Phase 1
babystack dumps an existing DB once. Creds used only at snapshot time — never injected into a test.
for prod-shaped data: a small coherent slice, masked — via Greenmask/Neosync. babystack never builds this.
Pay the expensive cost once, cache it, reuse it.
Never re-run scripts or re-hit a DB per test.
build/load the baseline → mysqldump → cache to
.babystack/cache, keyed by a content hash of
the inputs.
next run: hash matches → skip the build entirely. No scripts, no DB. Rebuild only when an input changes. In CI: cache the dir.
create a DB + load the cached dump (~0.3–2s). Later: warm pool + async reset, then copy-on-write clone (sub-second).
The baseline is lean by design. babystack never dumps a huge production DB (infeasible — and a fat baseline kills the per-file reload, which is ~linear in seed size). It's a fast test-DB factory, not a prod-cloning tool.
Trust cliff: a cache that serves stale data is worse than no cache — so hashing is conservative, and correctness always ships before speed.
The guarantees the whole design is built to keep.
Real, never emulated
Your app talks to a real mysql:8.4. We
orchestrate the engine (or delegate to LocalStack) — we never reimplement a
proprietary API.
Disposable URL, never prod creds
Tests and agents only ever get a throwaway connection URL with minted creds. No real dev/prod credentials reach a test, an agent, or a log.
Lean baseline
Small, representative data — never a clone of prod. It's a performance necessity, not just taste.
Compose, don't compete
babystack consumes Docker & delegates to LocalStack. A platform feature is an upgrade, not an obituary.
Phase 0 complete — the MySQL + Vitest wedge, proven.
One sub-phase at a time, each behind the adversarial review gate.
seam + ports, fake-tested.
the generic muscle.
seeded baseline + leases.
Vitest wiring · parallel isolation.
cache integrity + secret redaction.
baby doctor/wake/home/reset/sleep + a real Express app.
The engine + Vitest wedge is proven end-to-end against real MySQL — a
seeded row survives build → dump → lease, and a stock
pnpm test hands each of N parallel workers its
own fresh, isolated database. Next: warm-pool speed (0.9).
Dig deeper: the docs and roadmap, plus the source on GitHub.