The Local Development page covers the AWS-bound loop (ds run backend + ds run ui against your personal SST stage). This page covers the fully-local, no-AWS alternative: a docker-compose stack that runs many independent DevStride instances at once — one per git worktree — on a single machine.
Each instance has its own branch, its own ports, its own isolated dataset, and its own test containers. They all share one set of heavy infra containers, so ~10 instances stay cheap.
.env stage binding, and no sso login required for this flow.docker/ scripts and the ds worktree CLI). master and develop both have it. New worktrees inherit these files from their base branch. ┌─────────────────────────────────────────────┐
shared core │ postgres dynamodb minio cognito-local │ (started once)
(devstride-core)└─────────────────────────────────────────────┘
▲ ▲ ▲
┌────────────────┼────────────┼──────────────┼────────────────┐
│ instance "main"│ │ instance "featx" │ ...
│ backend :4000 │ │ backend :4001 │
│ frontend :8080 │ │ frontend :8081 │
│ soketi :6001 │ │ soketi :6002 │
│ db devstride_main │ db devstride_featx │
│ tables main-devstride-* │ tables featx-devstride-* │
│ buckets main-devstride-* │ buckets featx-devstride-* │
└────────────────────────────┴───────────────────────────────┘
postgres, dynamodb, minio, cognito-local. Heavy and stateful, started once, reused by every instance.backend + frontend + tiny soketi, bind-mounting that worktree's source. Isolation inside the shared infra is logical: each instance gets its own Postgres database, its own DynamoDB tables, its own MinIO buckets, its own Cognito user pool, and its own soketi app.main (index 0, ports 4000/8080), brought up by ds run local. Extra worktrees are indexes 1, 2, 3…IS_LOCAL + DEVSTRIDE_LOCAL_STACK so production, sst dev, and tests are completely untouched. You don't need to know the internals to use the stack, but it's why this flow needs no AWS credentials.On a development machine the set of checkouts is fixed: the main checkout, .worktrees/wt1 and .worktrees/wt2, each a registered instance. Every session — yours or an agent's — works in one of those three. Agents never create another worktree or instance unless the owner asks for one in that conversation; when every member is busy they wait, or ask the session holding one, rather than make a new copy. Each extra instance is a full copy of the repository, its own node_modules and up to six containers, and in practice the session that made one never cleaned it up.
ds pool manages the three. It needs no AWS login.
A clean tree does not mean a checkout is free: a release can hold one detached for hours, another session may be running tests or a dev server there, and two sessions can both see "clean" before either switches branch. So you take a checkout with a lease and hold it until you hand it back:
export DEVSTRIDE_SESSION="<your session name>" # or pass --session on each command
ds pool status # who holds main, wt1, wt2, and what data each holds
ds pool checkout wt1 --purpose "<item or purpose>" # or wt2
checkout prints exactly one outcome:
LEASED — it is yours, and its app is started. Before the app starts, checkout runs the health check (ds machine doctor) on it. That moves a parked checkout up to the latest develop, reinstalls dependencies, and brings demo data up to date, and it reports anything it may not fix, such as newer secrets. While you hold the checkout you can switch branches freely inside it; the lease is what makes the checkout yours, not its branch. Troubleshooting explains each health-check message.BUSY — another session holds the lease, and the line says who and why, and that you do not hold it. Message them or take the other checkout. Never delete someone else's lease; a stale one is the owner's call.IN USE — there is no lease, but the checkout is dirty or on someone's branch, so a session from before the lease rule is working there. Your lease is already released; take the other checkout.BUSY and IN USE exit with code 2. A LEASED on a checkout that had no lease before can still be one an older session is using — tests or a dev server leave the tree clean — so check with any peer that might be working there before you switch its branch. Running checkout again on a checkout you already hold changes nothing and says so.
A lease answers to the session that took it, not just its name. Two sessions can share a name, so each lease also records a holder — the Claude Code session id and/or Codex thread id, which every command in one session shares. Every later command that changes the checkout (reset, client, release-client, cloud, checkin, template refresh, ds work) refuses any other holder. From a plain terminal, which has neither id, checkout prints an export DEVSTRIDE_LEASE_TOKEN=… line: run it in that shell, and the later commands know it is you. A lease taken before holders were recorded is refused by those commands; hand it back by hand only if you are sure it is yours.
main is the owner's own checkout, and they work there without a lease. Agents use wt1 and wt2; main takes --i-know-main-is-free, only after the owner says so. A release always uses wt1 or wt2.
A golden build takes a long time. Instead, capture a golden template once, then clone it whenever you want a fresh dataset:
ds worktree cli wt1 golden build # in a leased wt1 or wt2
ds pool template refresh --from wt1 # capture it as the template
ds pool reset wt1 # later: back to golden in seconds
A reset also re-creates the golden logins in that checkout's own Cognito pool. If your branch carries migrations the template lacks, the reset runs them and checks every one landed; if your branch is behind the template, the reset is refused — nothing downgrades a database.
For a customer repro, copy one organization into a checkout you hold. It needs SOURCE_DB_CONNECTION_STRING, as ds data copy does:
ds pool checkout wt2 --purpose "<item>" --mode client --org <organizationId>
ds pool reset wt2 # snaps back to that customer's copy, not to golden
ds pool release-client wt2 # the only way out: back to golden, snapshot dropped
A checkout holding one customer's data refuses a second organization, and is never used to capture the golden template. Customer data is kept on a laptop no longer than the repro needs it:
ds pool checkin refuses while a customer copy is held, unless you pass --keep-client to keep it on purpose. The next session to check that checkout out is told the copy is there.ds pool status flags a copy older than 14 days. The unattended health check — the hourly schedule, or ds machine doctor run by hand without --check-only — drops such a copy on a checkout nobody holds and resets it to golden. On a checkout a session holds, it only reports it; that session decides.ds pool cloud <checkout> runs one leased checkout against this machine's own cloud stage instead of Docker, by starting ds run backend and ds run ui for it in the background — one checkout per machine. ds pool cloud <checkout> --off brings Docker back. See cloud mode on Local Development.
ds workInside a checkout you hold, ds work puts an item on the branch it belongs to, so you never have to work out the routing rule yourself:
ds work start I12345 # cut its feature branch off the right target
# … build, commit …
ds work land # local checks, then merge onto that target and push
start picks the item's nearest Epic's integration branch, or — with no Epic above it — the support train, train/support, which ships with the next release. land merges the target's newer work in, runs ds verify land — every type-check, eslint on the changed frontend files, and every backend and frontend test that reaches the changed code — then merges the branch onto the target and pushes. Neither ever touches develop or master: anything there reaches production at the next release, so only a fully reviewed batch — an epic's release pull request, or the train's — may merge into it. A one-off that changes deploy configuration or migrations is started with --infra and ships by its own pull request instead. ds work start and ds work land refuse a checkout you have not leased.
The DevStride plugin's build-item follows the same rule; Where work lands explains it, and ds work lists every flag.
When your work has merged or been handed over, stop every process you started in the checkout (test runs, dev servers, watchers), then:
ds pool checkin wt1
It refuses unless you hold the lease and the tree is clean, and refuses a customer copy unless you pass --keep-client. It ends any cloud binding, stops the checkout's app containers (its test containers keep running), leaves it detached at a freshly fetched origin/develop, and removes the lease. Its data is kept.
<session> | <purpose> | <UTC time> | <holder>. Every flag is in the Command Reference.If you only want the main checkout running locally:
ds run local # shared core + the "main" instance (first run: a few minutes)
ds worktree cli main migrations run # apply schema (first time / after new migrations)
ds worktree cli main golden build # seed the Acme golden dataset + Cognito + aggregation recalc
Open http://localhost:8080.
wt1 or wt2. Create an extra instance only when the owner asks for one, name its branch with the usual convention (-b <user>/<YY-MM-DD>/<item>-<slug> --from origin/develop; without -b the branch is named after the instance), and remove it with ds worktree rm when its purpose ends.# 1) Create a new instance on its own new branch + worktree, and start it.
ds worktree create featx
# → git worktree add, allocate ports, ensure the core is up, provision this
# instance's data, start it detached, and print its URLs.
# 2) Apply schema + seed data (each instance starts with an EMPTY database).
ds worktree cli featx migrations run
ds worktree cli featx golden build # optional: seed golden Acme
# 3) Open it — frontend on http://localhost:8081 (backend :4001).
# 4) See everything running.
ds worktree ls
# 5) Tear it down (stops it, drops its data + worktree; KEEPS the branch).
ds worktree rm featx
That's the whole loop.
| Command | What it does |
|---|---|
ds run local | Bring up the shared core + the primary main instance in the foreground. |
ds worktree create <name> | Only when the owner asks — the pool is fixed (above). git worktree add a new branch, allocate ports, ensure the shared core is up, provision this instance's data, start it detached, print URLs. |
ds worktree ls | List instances: name, slug, index, status, frontend/backend URLs, branch, and each instance's test-container pair. |
ds worktree cli <name> <cmd...> | Run a cli/commands/<cmd> inside that instance's backend (e.g. migrations run, golden build, data copy). Not the API generators — see the warning below. |
ds worktree logs <name> [service] | Follow logs (service ∈ backend, frontend, soketi, bootstrap). |
ds worktree up <name> | Start a registered instance (after down). <name> may also be a worktree directory name or a branch; a matching existing git worktree is adopted when needed. |
ds worktree down <name> | Stop an instance (its test containers included), keeping its data. |
ds worktree rm <name> | Stop + drop all of this instance's data (db, tables, buckets, cognito pool, test containers) + remove the worktree. Keeps the git branch; prompts if the worktree has uncommitted changes. A worktree the CLI did not create (see register) is left in place. |
ds worktree register <name> [--dir <path>] [--conductor-port <port>] | (alias adopt) Register a git worktree that already exists — a Conductor workspace, say — as an instance, without taking ownership of it. The directory is found by directory or branch name unless --dir names it. |
ds worktree unregister <name> [--keep-data] | (alias forget) Drop an instance and its data from the registry without removing its git worktree. |
ds worktree gc [--prune] | Report local resources (databases, tables, buckets, user pools, test containers) that belong to no registered instance — a dry run by default; --prune deletes them after asking. ds pool status points here when it finds containers no pool checkout owns. |
ds worktree core up | Start just the shared core. |
ds worktree core down [--wipe] | Stop the shared core (--wipe also deletes all shared data volumes). |
create flags-b, --branch <branch> — branch to create/check out (default: the slug).--from <ref> — base ref for the new branch (default: current HEAD).--rebuild — rebuild the shared devstride-local image first (after dependency changes).--attach — run in the foreground instead of detached.rm flags--keep-data — skip dropping the shared-core resources (just stop + remove the worktree).--delete-branch — also delete the git branch (kept by default).-f, --force — skip the confirmation when the worktree has uncommitted changes.ds worktree cli <name> script generate-api-client and … generate-api-mcp look like they work and do not. The instance's container mounts backend, cli, stacks and docker — but not frontend and not packages, which is where the client and MCP output belongs. Those writes land inside the container and are lost, while backend/…/openapi.json is mounted and updates anyway.
So the command prints its usual success output and exits 0, having written some of its outputs. The result is a committed OpenAPI file that disagrees with the client and MCP contracts generated from it — and nothing tells you. Run them on the host instead, and check the artifacts rather than the exit code: if only the backend OpenAPI file moved, the run was partial.
The generators need no running stack — they build from backend source, so there is nothing for an instance to provide.
Derived from a per-instance index i (index 0 = main):
| Service | Port |
|---|---|
| backend | 4000 + i |
| frontend | 8080 + i |
| soketi | 6001 + i |
| test postgres | 5433 + i |
| test dynamodb | 4567 + i |
Shared core ports are fixed: postgres 5432, dynamodb 8000, minio 9000/9001, cognito-local 9229. ds worktree ls always prints the resolved URLs and each instance's test-container pair. The last two rows are the test databases the vitest suites use, separate from the instance's application databases — see Running Tests Per Instance.
This is the whole point of the setup: every instance owns a completely separate copy of the data, so you can run several branches at once, each backed by a different dataset. The isolation is enforced at the resource level, not by convention:
| Layer | Per-instance resource |
|---|---|
| Postgres | Its own database devstride_<slug> (the CLI patches DB_CONNECTION_STRING inside the instance's DEVSTRIDE_CONFIG secret). |
| DynamoDB | Its own tables <slug>-devstride-{state,timetracking,ai,migrations} inside the shared -sharedDb. |
| MinIO (S3) | Its own buckets <slug>-devstride-* (media bucket is public-read). |
| Cognito | Its own user pool devstride-<slug> in the shared cognito-local. |
| Realtime | Its own soketi container + app, so channels never cross between instances — even two instances holding the same org id. |
Because each instance is a self-contained database, the same data tooling from Local Development works per-instance — you just scope it with ds worktree cli <name>:
ds worktree cli featx migrations run
ds worktree cli featx golden build
golden build seeds the Acme org (ac3e0000-0000-4000-8000-000000000001), creates the Cognito logins in that instance's pool, and runs the final aggregation recalc — all scoped to featx only.
# Copy one org's data from a source database into THIS instance.
# Users land in this instance's cognito pool.
ds worktree cli featx data copy -o <organizationId>
# At a TTY you're prompted to pick a *_CONNECTION_STRING from .env, or pass it:
ds worktree cli featx data copy -o <organizationId> --source GOLDEN_DB_CONNECTION_STRING
The worktree mounts its own ./data directory at /data inside the container, so dumps round-trip through there:
# Import a dump placed under the worktree's ./data.
ds worktree cli featx data import /data/<dump>.json
# Export this instance's database.
ds worktree cli featx data export /data/featx-export
ds worktree cli demo golden build), copy a specific customer org into another (ds worktree cli repro data copy -o <id>), and leave a third empty for a from-scratch flow. They run simultaneously on localhost:8080, :8081, :8082 and never see each other's data.ds worktree cli <name> golden build) — the build ends with an aggregation recalc and is the single source of truth. Never patch rows directly; the next build truncates and regenerates the Acme org, silently discarding manual edits. And prefer golden build (or ds golden import on shared stages) over ds data copy for golden persona orgs — the generic copier is marker-unaware and leaves the golden anchor marker stale.The backend vitest suites do not use an instance's application database. They use two test containers of their own — a Postgres and a DynamoDB — and those are now per instance, just like everything else on this page.
They did not used to be. Until August 2026 a single pair, named drizzle-tests and
dynamodb-tests, was shared by every checkout on the machine. Two concurrent runs corrupt each
other's per-worker databases, so the harness's concurrency lock had to refuse the second one — which
meant worktrees parallelised editing and running, but never testing, and the backend suite is the
slow part of any delivery loop.
Now N instances can run N full suites at the same time. The lock still exists and still aborts a second run, but it is scoped to the pair a run actually resolves, so it only fires when two shells in the same worktree contend for the same containers — the case it was always meant to catch.
What this changes in practice:
ds worktree ls prints each instance's test-container pair, alongside its URLs. That is where
to look — do not assume a name or a port.docker restart drizzle-tests dynamodb-tests is no longer the right command inside a worktree.
It names another instance's containers, or none at all. Restart the pair ds worktree ls names for
the instance you are in.ds worktree rm <name> removes that instance's test containers; ds worktree down <name> stops
them and keeps their data — the same lifecycle as the rest of the instance.Isolation removes the database contention, not the CPU contention. Vitest sizes its worker pool
from the whole machine, so two default runs oversubscribe it and hooks start tripping their timeout —
the same Hook timed out symptom that usually means degraded containers, from a completely
different cause. Give each run roughly half the cores:
# in each worktree, concurrently
pnpm --dir backend test:suite:ci:non-golden --maxWorkers=7 # 16-core machine
Expect around 1.6x throughput, not 2x — the databases are independent, the CPU is not.
The full contract — how a run resolves its pair, and what happens to worktrees created before this
shipped — lives in the repository at docs/agent-worktrees.md, under "Test containers are
per-instance". That section is the authority; everything else points at it rather than restating it.
Local auth uses cognito-local, which signs in by username, not email. After a golden build, use username acme.devstride (Admin) or alex.rte (RTE persona), password Demo@123 for all personas. Each instance has its own pool, so the same credentials exist independently on every instance you seed.
ds worktree down <name> — stop an instance but keep its data; bring it back with ds worktree up <name>.ds worktree rm <name> — stop it, drop all its data, and remove the worktree (keeps the branch; prompts on uncommitted changes).ds worktree core down — stop the shared core. This stops the data layer for all instances; add --wipe to also delete the shared data volumes.ds worktree create --rebuild (or ds run local --rebuild).docker/ scripts are bind-mounted from the worktree, so create instances from a branch that contains docker/instance.compose.yml et al. (master, develop, or any branch cut from them).creates are simplest..git/devstride-local/registry.json) and is shared across all worktrees.ds worktree rm, ds worktree gc --prune, or the test harness recreating one) deletes the unnamed volume that held its data, and ds worktree down, ds pool checkin and ds pool cloud delete the unnamed dependency-folder volumes of the app containers they stop. The named runtime volume and your instance's data are kept. Before this, every recreated test database left a volume behind, and enough of them filled the Docker disk until test runs failed with No space left on device. A machine that ran the older tooling may still hold them: docker system df shows them as reclaimable Local Volumes, and docker volume prune deletes only unnamed volumes no container uses.ds data command surfaceds golden commandsds CLI surface, including every ds pool flagLocal Development
The real day-to-day dev loop: running the backend and UI, scaffolding CQRS code, migrations, and managing local data with the ds CLI.
Golden Dataset
The Acme golden dataset: how to get it onto a local instance or a stage, sign in to it, keep the shared source current, and where the deep-dive docs live.