On this page
“We should have a proper dev environment” is one of those sentences everyone agrees with and nobody budgets for. On an AI-agent platform I work on as fractional CTO, we finally paid the bill: a complete, isolated environment mirroring roughly 12 services — edge Workers, web frontends, databases, queue brokers, a fleet of containerized Node workers, messaging integrations, voice tooling. It took weeks. Not because any single step was hard, but because a staging tier is a product with its own backlog, and every service contributes at least one trap.
These are the traps, so you can budget honestly.
Config doesn’t inherit the way you assume
The first surprise was in the Workers config itself: wrangler environment blocks don’t inherit everything from the top level. Define [env.dev] and you have not created “production, but pointed elsewhere” — bindings, vars, and routes you expected to carry over silently don’t, and the dev Worker boots without pieces the top-level config clearly declares. Every binding the production Worker uses has to be re-declared and re-pointed, deliberately, in the environment block. Treat the env block as a full second config that happens to share a file.
Your “empty” dev resources are full of production
Two versions of the same trap, from two different tools:
- Database branches copy data. Branching the production database is the fastest way to a schema-correct dev DB — and the branch arrives silently carrying production data. A dev environment reachable by more people, with weaker credentials hygiene, holding real customer rows. Scrubbing or reseeding the branch is a required step, not an optional nicety.
- Cloned services keep their volumes. Cloning a queue-broker service in the deploy platform to make its dev twin brought the persisted volume along — and with it, the production credentials stored inside. The clone looked like a fresh broker; it was production’s broker wearing a name tag. Anything cloned with state must have its state wiped or recreated from scratch.
The pattern underneath both: infrastructure tools optimize for “give me a copy,” and a copy of production is exactly what an isolated environment must not contain.
Identity and cookies don’t respect your subdomain plan
Pointing dev. subdomains at the new stack is the easy half. Then:
- OAuth redirects you back to production. The OAuth app registered for the product only knows production callback URLs, so every dev login bounced to the prod frontend. Dev needs its own OAuth client (or added redirect URIs) for every provider — each one a console visit with its own review quirks.
- Cookies collide across the apex and the dev subdomain. Sessions set for the apex domain are visible to
dev.subdomains, so being logged into production contaminated dev sessions and vice versa. Cookie names, domains, and scoping have to be made environment-aware, or the two environments will log each other out and worse.
Networking is different from inside a container
A service that reached the broker fine “on localhost” fails in the platform because Docker-network reachability and published-port reachability are different address spaces. Inside the deploy platform’s network, services talk over container DNS names on internal ports; from outside, only published ports exist. Half the connection strings needed rewriting depending on which side of that line the client sat.
And one from the build side rather than runtime: setting NODE_ENV=development on the build server broke builds outright, because installs skip dev dependencies exactly when the build tooling needs them — I wrote that one up separately in a TIL.
Third parties don’t have a dev mode unless you build one
The external integrations were the longest pole:
- Messaging providers: an isolated environment needs its own provider app, its own phone number, and its own approved message templates — the templates alone have a review cycle measured in days, on the provider’s clock, not yours.
- Voice tooling: platform-side tool definitions are referenced by ID, so dev needs its own set of provisioned tools, mirrored one-for-one from production, with the dev IDs threaded through dev config. Any drift between the two sets becomes a “works in prod, broken in dev” mystery.
None of this is technically difficult. All of it is calendar time, third-party consoles, and bookkeeping.
Budget it like a product
The honest accounting: a real staging tier for a multi-service platform costs weeks, not days, and then it costs maintenance forever — every new service, secret, template, and tool ID now ships twice. The alternative costs more: testing in production, or a half-mirror that lies to you, which combines the expense of staging with the confidence of having none.
Three rules that would have saved us the most time:
- Assume nothing inherits and nothing is empty. Re-declare every binding; scrub every clone.
- Make environment part of identity — OAuth clients, cookies, provider apps, tool IDs — from the first service, not retrofitted across twelve.
- Track the staging gap as a backlog. “Dev doesn’t have X yet” items are real work; invisible ones are how the mirror quietly stops being one.