The Hidden Public Surface Area of Private Code

‎

Hatched by

May 09, 2026

9 min read

84%

0

The illusion of privacy in modern JavaScript apps

It is easy to think of a file as private just because you never import it anywhere obvious. It is easy to think of a variable as safe just because it lives in process.env. Both assumptions feel natural, and both can quietly fail.

That is the deeper tension connecting environment files and server actions: the boundary you see in your code is not always the boundary the runtime sees. In modern JavaScript applications, especially in frameworks that blur server and client responsibilities, privacy is often less about file location and more about execution context, transport, and exposure.

This matters because developers still tend to reason in a filesystem mindset. If a secret sits in a .env file, it feels hidden. If a function is tucked away in a module and never imported into a route, it feels inaccessible. But the runtime does not care about our intuition. It cares about what gets loaded, what gets serialized, what gets exposed, and what gets mapped to a network endpoint.

The result is a dangerous gap between mental model and actual attack surface.


Configuration is not security, it is just convenient indirection

A .env file solves a practical problem: separating configuration from source code. It keeps passwords, API keys, and environment specific values out of hardcoded strings and makes deployment easier. But convenience can be mistaken for protection. When Node.js does not natively load .env files, we bring in a package like dotenv to read the file and expose its values through process.env. That is not a security boundary. It is a loading mechanism.

This distinction sounds subtle, but it changes how you should think. A secret in .env is only as private as the systems that can read the file and the code that can print, forward, or leak the value. Once loaded into process.env, that value exists in memory and can be accessed by any code in the process. In other words, the file is hidden, but the data is now fully available to the running application.

Think of it like putting a spare key in a lockbox on your front porch. The box is more organized than leaving the key on the doormat, but it is not the same thing as making the house secure. The question is not whether the key is stored neatly. The question is: who can access the key once the house is running?

This is why environment files are best understood as a configuration delivery system, not a secrecy system. They help you avoid hardcoding values and support different environments, but they do not absolve you of access control, logging discipline, and least privilege. If a secret should never be visible to the browser, the build output, or a public endpoint, then placing it in .env is only the first step, not the last.


The modern framework trap: private functions that become public interfaces

Now consider server actions. A function can feel deeply internal because it is defined in server code, exported from a module, and maybe never directly referenced anywhere else in your app. Yet by default, when a server action is created and exported, it can create a public HTTP endpoint. That means the function is not private just because it looks internal in your source tree. It is reachable because the framework has turned it into a network-facing surface.

This is where many developers get caught. Traditional application reasoning says, “If no route points here, nobody can call it.” But framework conventions often reshape code into endpoints automatically. The same function that looks like a utility can become an API, and every API is a promise to the outside world, not just a helper for your own codebase.

The surprising connection to .env files is that both cases involve hidden translation layers. In one case, a file becomes process memory. In the other, a function becomes an HTTP endpoint. In both cases, the developer’s intention is filtered through runtime behavior. What feels internal is often externalized by tooling.

In modern app development, “private” often means “not obvious to humans,” not “inaccessible to attackers.”

That is a dangerous difference.

Imagine a restaurant kitchen where ingredients are stored in labeled bins. The label says “flour,” but the bins are placed on a conveyor belt that runs straight to the dining room. You can no longer reason about safety based on the label alone. You have to understand the route the ingredient can take. Likewise, in a Next.js app or any framework that abstracts transport for you, a server action may look like a kitchen tool, but the framework may have already installed a service hatch to the street.

The deeper lesson is that frameworks reduce boilerplate by making important transitions feel automatic. But automatic transitions are exactly where boundaries get blurred. A developer who sees only the source file misses the transport layer. A developer who sees only the env file misses the runtime. The real security question is always about where data and execution can flow, not where they are defined.


The shared mistake: treating representation as containment

Both .env files and exported server actions tempt us into the same fallacy: if something is represented in a special way, it must be contained.

A secret stored outside the code feels contained because it is not sitting in a JavaScript string. A server action feels contained because it is not declared in an API routes folder. But representation is not containment. A secret can be loaded, logged, serialized, or passed to code that sends it elsewhere. A server action can be mapped to an endpoint, invoked by a crafted request, and exposed to unauthenticated callers unless explicitly protected.

This is why secure systems thinking must focus on flow rather than placement.

Here is a useful mental model:

  1. Storage boundary: Where does the value live before the app starts?
  2. Runtime boundary: Who can access it after it is loaded?
  3. Transport boundary: Can it cross from server to client, or from internal function to public request?
  4. Authority boundary: What checks determine whether an operation is allowed?

A .env file mostly addresses storage boundary. A server action mostly affects transport boundary. But security failures happen when we assume one boundary covers the others. The existence of a secret file does not enforce authority. The existence of a server action does not limit transport.

This matters even outside of obvious security bugs. For example, a developer might store a payment provider secret in .env, then accidentally include it in a server component prop or return it from a server action to debug an issue. Or a developer might write a server action to update a profile and assume that because the function is only called from a button in the UI, only the intended user can trigger it. That is not a guarantee. If it is exposed over HTTP, it can be called in ways the UI never anticipated.

The practical lesson is blunt: every boundary must be defended where it exists, not where you hope it exists.


A better framework: think in surfaces, not files

The most useful shift is to stop organizing your thinking around files and start organizing it around surfaces.

A surface is any place where outside actors can influence, observe, or invoke your system. A .env file may seem internal, but once values are loaded, the process surface expands. A server action may seem internal, but once exported, the request surface expands. The question becomes: what can touch this value or function now that the runtime has turned it into something reachable?

This surface based model reveals why many accidental leaks happen. They do not happen because developers are careless in the dramatic sense. They happen because the system quietly introduces new surfaces during normal operation.

Consider these examples:

  • A config value is loaded from .env, then used to construct a response header.
  • A server action is exported, then called from an unexpected origin.
  • A secret is kept in environment variables, then copied into a client side bundle by mistake.
  • A utility function is assumed to be internal, but the framework exposes it as a request handler.

In each case, the problem is not that the code is obviously malicious. The problem is that the code crosses a boundary without making the crossing explicit enough for the developer to reason about it.

That is why good architecture should make boundaries visible. You want code that says, in effect, “this value is loaded here, used there, and never crosses into a public context.” You want server actions that begin with a clear authorization check, not as an afterthought but as part of the function’s identity. You want environment handling that distinguishes between values safe for server only use and values intended for public exposure.

A simple rule helps: if a value matters, name its domain. Is it server secret, build time config, public config, or per request data? Is a function internal helper, authenticated mutation, or public operation? Ambiguity is where mistakes breed.


Key Takeaways

  • Do not confuse hidden storage with security. A .env file is a delivery mechanism for configuration, not a protection mechanism for secrets.
  • Do not confuse internal code location with private access. An exported server action can become a public HTTP endpoint and must be treated as externally reachable.
  • Model your app as surfaces, not folders. Ask where data can flow, who can invoke it, and what runtime transformations may expose it.
  • Add explicit authority checks at every public boundary. If a function can be called over the network, assume an attacker can call it too.
  • Separate server only secrets from anything that might reach the client. The safest secret is the one that never crosses a transport boundary at all.

The real boundary is not in your code, it is in the runtime

The most dangerous misconception in modern application development is that the source tree is the security model. It is not. The runtime is. The framework is. The transport is. The deployment is.

Once you see that, .env files and server actions stop looking like unrelated implementation details and start looking like two examples of the same phenomenon: abstraction creates power, but it also creates invisible exposure. The more a tool hides the machinery, the more disciplined your reasoning has to become.

That is the paradox. The same conveniences that make modern frameworks productive also make them easy to misunderstand. But this is not an argument against using them. It is an argument for a sharper mental model. When you store configuration, ask who can read it after loading. When you export a function, ask whether the framework has turned it into a route. When you rely on conventions, ask what assumptions the runtime is now making on your behalf.

Security is not the art of hiding code in the right file. It is the art of knowing exactly where your code becomes reachable.

Once you begin thinking that way, you stop asking, “Is this internal?” and start asking the more useful question: What surfaces has the runtime created, and who can touch them now?

Sources

← Back to Library

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 🐣
The Hidden Public Surface Area of Private Code | Glasp