Back to Writing
Growth status: Seedling SeedlingUpdated: Feb 7, 20264 min read

Environments Are Not Features

I wanted a sandbox. In other words i wanted continuity of identity and discontinuity of data. Production is where mistakes cost money. Sandbox is where mistakes cost pride.

I wanted a sandbox.

Not the decorative kind. Not the one where production data wears a fake name tag and hopes nobody notices. A real sandbox. One where people can test things without accidentally discovering how refunds work.

What sounds simple quickly isn’t.

You want one frontend. You want multiple environments. You want users to move between them without re signing up, re logging in, or re learning where buttons went.

In other words you want continuity of identity and discontinuity of data.

That sentence alone explains why this is harder than it looks.

The first mistake people make is thinking environments are a feature.

They are not.

Environments are boundaries. And boundaries only work when they are enforced by structure, not intention.

A common best practice that actually works is this keep the frontend singular and boring push all environment decisions to the backend edge

The frontend should not know or care where production lives or where sandbox lives. It should talk to one internal endpoint. That endpoint becomes a proxy. The proxy decides where requests go.

This sounds indirect until you realize indirection is the entire point.

The proxy gives you three important things centralized routing decisions a single place to enforce rules and a single source of truth about where reality actually is.

That last part matters more than you think.

State lies. Cookies lie. Local storage lies enthusiastically. So the UI should not trust what it thinks the environment is. It should read what the backend actually did. A header. A response flag. Something factual.

Then show a small indicator. Not a billboard. Just enough signal to keep humans honest.

Switching environments is another trap.

If you let users switch without a reload you will eventually land them on a page that only exists in one environment. Now you have a broken screen and a confused person. That confusion will be blamed on your architecture.

The boring but correct approach is switch hard reload land in a safe default

It feels heavy. It is stable.

Identity is where most designs quietly fall apart.

You want the same user to exist in multiple environments without becoming multiple people. At the same time you absolutely do not want environments sharing business data.

There are two common paths here.

One is to centralize identity. This is clean in theory and terrifying in practice. Shared identity databases get migrated accidentally. Accidental identity migrations are career shaping events.

The other path is copying identity.

Production owns identity. Other environments borrow it.

Same user id. Same credentials. Same minimal profile data required to validate sessions.

Nothing else.

No payments. No domain data. No history.

This works because authentication is not magic. A token is valid if the signature is valid and the user exists. That is the whole contract. Everything else is implementation detail.

If you copy identity, do it incrementally. Track what changed. Sync only what is needed. Support dry runs. Support replaying a time window when something goes wrong. One way sync only. Never write back.

And enforce where this can run. With configuration. Loudly. If a sync command can run in the wrong environment it eventually will.

Environment switching must also respect public and sensitive routes.

Authentication should almost always stay in production. Public endpoints too. Pricing pages do not belong to sandbox. Login flows do not belong to sandbox. Sandbox is for authenticated, intentional actions.

This is less about security and more about sanity.

Observability matters more than cleverness.

When something fails you need to know why. Token rejected because of signature mismatch is very different from token rejected because the user does not exist. Silence here turns debugging into folklore.

Temporary debug logging in lower environments is not a sin. It is a kindness to your future self.

Finally deployments.

Restart does not always mean stop.

If your environment can fail to restart because a port is still in use then you do not have a restart. You have a hope. Deployment scripts should enforce reality. Stop the process. Free the resource. Start again.

This is unglamorous work. Which is how you know it matters.

The best multi environment systems share a few traits.

One frontend. Clear routing boundaries. Strong separation of data. Shared identity without shared databases. Explicit environment rules. Boring reloads. Honest indicators.

And very little magic.

Production is where mistakes cost money. Sandbox is where mistakes cost pride.

Good architecture is making sure pride takes the hit.

Share this writing