Where Configuration Lives Changes What Your Code Means
Hatched by
Jul 13, 2026
9 min read
2 views
88%
The hidden question behind every environment variable
What if the real difference between a local script and a deployed service is not where the code runs, but how it learns what the world looks like?
That is the strange lesson hiding inside a seemingly small detail: in one runtime, environment values arrive through process.env; in another, they arrive through c.env. On the surface, this looks like a syntax difference. In practice, it reveals something much deeper about software design: configuration is not just data, it is a contract between code and its runtime.
Most developers treat environment variables as plumbing. They are the invisible knobs behind the curtain, the place where secrets, URLs, and feature flags live. But the moment you move from a traditional Node.js setup to a serverless edge runtime, that mental model starts to break. Suddenly, the same app cannot rely on the same global assumptions. The code must ask for its context in a new way, and that changes more than the API. It changes the shape of your application.
The way code receives configuration is a clue to how much it can assume about the world.
process.env is not just convenient, it is a worldview
In Node.js, loading .env files through a package like dotenv feels natural because the runtime presents itself as a long-lived, process-centered environment. There is a global object, process, and process.env becomes the shared mailbox for configuration. This design encourages a simple mental model: the application boots, reads its environment, and then behaves accordingly.
That convenience has consequences. When configuration is globally accessible, any function anywhere can reach into it. This lowers the friction of writing code, but it also hides dependencies. A function might look pure while silently depending on process.env.DB_URL or process.env.API_KEY. That makes local development easy, but it can blur the boundary between what the function does and what the environment happens to provide.
A useful analogy is a kitchen with one central spice rack. Every cook can reach for the same jar of salt, pepper, or cumin whenever needed. That is fast, familiar, and efficient, but it also means no recipe explicitly states which spices it needs. If the rack changes, every dish is suddenly at risk. process.env works the same way: it is a shared pantry hidden behind a global door.
The key point is not that this model is bad. It is that it encodes an assumption: the runtime is stable enough to expose configuration as a universal ambient resource. That assumption often holds in Node.js applications. But it does not hold everywhere.
Edge runtimes force configuration to become explicit
Cloudflare Workers represent a different contract. Instead of leaning on process.env, values are accessed through c.env. That small difference matters because it shifts configuration from a global background detail into a contextual capability.
This is more than a naming convention. It says: your code does not own the environment, it receives it as part of the execution context. The application is no longer imagined as a single process with ambient state. It is a request driven unit of computation, and the runtime passes it exactly what it needs for that invocation.
This matters because edge and serverless systems are optimized for isolation, speed, and portability. There may not even be a meaningful long-lived process in the traditional sense. The code may be instantiated, invoked, and discarded in ways that make process.env feel like an outdated abstraction. By requiring access through c, the platform makes dependency boundaries visible.
Think of it like a backstage pass versus a key ring. In the process.env world, everyone on the team carries the same master key, and doors are opened by convention. In the c.env world, access is granted with a pass attached to the exact job you are performing. You can only open the doors that this request, this deployment, this execution context is allowed to open.
That explicitness has a hidden benefit: it pushes you toward better architecture. Functions become easier to test because they rely on passed context rather than invisible globals. Security posture improves because secrets are scoped more deliberately. And portability gets better because the code advertises what it needs instead of assuming a universal ambient runtime.
The deeper tension: ambient state versus contextual dependency
The real issue is not Node.js versus Workers. It is ambient state versus contextual dependency.
Ambient state is everywhere in software. It includes global variables, singleton clients, hidden caches, and anything else a function can touch without being told. Ambient state feels ergonomic because it removes ceremony. But it also makes systems harder to reason about, because the real inputs are not visible at the call site.
Contextual dependency does the opposite. It forces the code to accept its inputs explicitly, usually through parameters or runtime context objects. This is less magical and often a little more verbose. But it pays off by making the dependency graph legible.
The process.env and c.env distinction is a perfect example of this tradeoff. In Node.js, configuration is often treated as ambient. In Workers, configuration is contextual. Neither is merely a technical implementation detail. Each model encourages a different style of thinking.
Here is the surprising insight: many runtime bugs are really dependency bugs in disguise. A function fails not because the logic is wrong, but because it assumed a global it did not truly control. The more your code depends on invisible ambient state, the more it resembles a guess rather than an argument.
This is why the move from process.env to c.env feels jarring to some developers. It is not just a different syntax. It is a shift from trusting the world is there whenever you reach for it, to accepting that the world must be handed to you.
A practical framework: from hidden assumptions to declared needs
A useful way to think about configuration is to ask a simple question for every dependency: is this a property of the program, or a property of the execution context?
If it is a property of the program, it should likely be encoded directly in code. If it is a property of the execution context, it should be passed in, injected, or accessed through the runtime object that owns that context.
This leads to a helpful four part framework:
-
Constants belong in code. If a value never changes across environments, it should not be hidden in a
.envfile. Keep it in the source where it is visible and reviewable. -
Secrets and deployment specific values belong in runtime context. Database URLs, API keys, and environment specific endpoints are not program logic. They are inputs that vary by deployment.
-
Frequently changing operational flags should be explicit. If a feature flag or setting affects behavior dynamically, treat it like an input to the system, not a magical global.
-
Runtime access should mirror runtime architecture. If the platform gives you context objects like
c, use them. They are telling you how to think about the application.
A concrete example helps. Imagine a simple email service with a route handler that sends a confirmation message. In a Node.js app, the code might read process.env.SMTP_PASSWORD directly inside the handler. That works, but the handler now depends on invisible state. In a Workers app, the same handler would pull the password from c.env, making the requirement visible at the boundary where the request enters the system.
The difference seems small until you test the function. With process.env, you often have to mock globals or manipulate environment state before the test runs. With contextual access, you can pass a fake env object directly into the function. That is not just cleaner testing. It is a sign that your design is more honest about what it needs.
Good software does not merely run correctly. It makes its dependencies hard to misunderstand.
Why this matters beyond configuration
The debate over environment variables is really a rehearsal for a larger design question: should your code rely on invisible context or explicit contracts?
This question shows up everywhere. Database clients can be global singletons or injected per request. Authentication can be inferred from request state or passed into a service layer. Logging can be a hidden side effect or a dependency with a formal interface. The same tradeoff repeats itself: ease now versus clarity later.
Runtime design shapes program design. If the platform encourages ambient access, developers build systems that assume hidden context. If the platform encourages contextual access, developers build systems with clearer boundaries. Over time, this affects maintainability, testability, and even the kinds of bugs the team is likely to create.
There is also a philosophical shift here. process.env suggests a world where the process is central and the environment is merely background. c.env suggests a world where the request or invocation is central and the environment is part of that specific transaction. In modern distributed systems, the latter often better matches reality.
That is why edge runtimes often feel more disciplined. They do not just execute code closer to the user. They force the code to admit that it is living inside a particular moment, with a particular context, under particular constraints. In that sense, c.env is not a limitation. It is a reminder that the runtime is part of the program’s meaning.
Key Takeaways
- Treat environment access as architecture, not boilerplate. The way you read configuration reveals the dependency model of your application.
- Prefer explicit context over invisible globals. Passing
envthrough a runtime object or function parameter makes dependencies easier to test and reason about. - Use
.envfiles for local developer convenience, not as a design principle. They are a bridge, not a substitute for clear runtime boundaries. - Ask whether each value is a constant, a secret, or a contextual input. That classification often tells you where it belongs.
- Let the runtime shape your mental model. If the platform gives you
c.env, it is signaling that configuration is part of request context, not a universal global.
The real lesson: code should know how it knows
The most important thing about process.env versus c.env is not which one is faster, newer, or more fashionable. It is that each one teaches a different lesson about how software relates to its environment.
One says: the process is the center, and configuration is ambient.
The other says: the request is the center, and configuration is contextual.
Once you see that distinction, a lot of programming advice becomes clearer. Testability improves when dependencies are explicit. Portability improves when code does not assume a single runtime shape. Security improves when secrets are scoped rather than globally exposed. And maintainability improves when a reader can tell, just by looking at the function, what it depends on.
The best code does not just do the right thing. It tells the truth about what it needs to do it.
That is the deeper shift hidden inside a tiny API difference: where configuration lives changes what your code means.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣