Runtime Boundaries
Published
Nearly every page on this site starts from the same fact: JavaScript that runs in a browser has already been delivered to the person you might be worried about. An edge function inverts that completely. The code is uploaded to a platform, executed on machines you rent, and never sent to a user at all. So the first question is not which options to enable. It is who, precisely, you think is going to read it.
Name the adversary first
There are four candidates, and they are worth separating because three of them have better answers than a build setting.
The first is the platform itself. It compiles and executes your function, which means it can read the program however it is written. There is no configuration that changes this, and any vendor claiming otherwise is describing a different threat. If you cannot accept the platform seeing your code, the decision you need is about where you host, not about how you build.
The second is somebody who obtains a deployment credential. A leaked token, a compromised continuous integration job, an over-scoped machine account. That person can usually download the deployed bundle, and transformation slows them down by an afternoon. Rotating the credential and scoping it properly does considerably more.
The third is an insider with dashboard access, which is an access-control problem wearing a code-reading costume. The fourth is an accident: a source map served alongside the function, a debug route that echoes configuration, an error page that includes a stack trace with file paths. Those are the ones that actually happen, and none of them is prevented by renaming identifiers.
If, after that list, the answer is still yes, keep reading — because the runtime you are targeting breaks about half of what this tool can do.
Every runtime guard assumes a browser
The runtime defenses in this engine are emitted as small self-contained functions that reach for a global object and then look for browser features. In a runtime with no document, no window and frequently no navigator, they do not gracefully skip. They take a defined path, and it is usually the failing one.
The domain lock resolves a global, reads location.hostname if it can, and falls back to an empty string. Its very next act is to check whether that value is empty and, if so, fire the failure action — before the allowlist is consulted. A runtime with no location object therefore does not skip the guard, it fails it. This is the same mechanism that breaks previews from a blank frame and that makes domain locking hostile on embedded devices opened from disk.
The browser and operating system locks resolve the same global, read navigator, and take the failure path when it is absent. Even where a navigator exists with a server-flavoured user agent string, the classifier has a fixed set of families to match and an unrecognised string classifies as unknown, which is never in an allowlist. Two different roads to the same failing state.
The developer-tool key blocking registers a global handler that cancels the context menu event and watches for a handful of key combinations. The debugger detection installs an interval that compares outer and inner window dimensions and periodically evaluates a debugger statement through a constructed function. Neither has anything to attach to off-browser, and the second is dynamic code generation, which many edge platforms forbid outright.
The wrappers you were probably counting on are skipped
Three of the heavier options — the self-defending wrapper, the self-healing recovery wrapper and the anti-tampering monitor — are almost certainly not doing anything in your edge build, for two independent reasons, and the engine tells you both during the build if you read the warnings.
The first is evaluation. All three work by evaluating code at run time, and the engine warns explicitly that they require a content security policy permitting it. Edge runtimes commonly refuse dynamic code generation as a platform rule, which is the same constraint a Trusted Types deployment imposes in a browser.
The second is module format. Those three are classic-script wrappers, and when the engine detects ES module source it skips them and says so, precisely so that import and export linking stays intact. Edge functions are written as modules almost without exception. So the usual outcome is that the options you enabled were never applied, and you would only know from a warning line you may not have looked at.
The anti-tampering monitor deserves one extra note. Its default watch list is full of browser globals: the storage prototype methods, the request object, event target registration, the beacon function. On a server runtime those simply are not functions, so the monitor skips them and ends up watching a much smaller set than you configured. It also builds its pristine baseline by creating a hidden frame, which needs a document; without one it compares against the host's own built-ins, which is exactly the comparison an attacker would tamper with.
A false positive is not one user's problem
This is the argument that should settle it for most teams. In a browser, a guard that trips wrongly affects the session in front of it. The failure actions were written for that world: blank the document, reload the page, redirect somewhere, throw. On a server there is no document and no page, so those branches accomplish nothing and then throw anyway.
A throw inside a request handler is a failed request. The same instance serves many users, so the cost of a false positive is not one broken session, it is an error rate. Combine that with guards that fail closed when a browser feature is missing and you have built a mechanism whose most likely output is an outage in a component with no user-visible symptom to help you diagnose it. If you enable anything at all here, use the degrade action or a callback you control, so the condition is recorded rather than raised.
The one option that gets better on a server
There is a pleasant exception. The date guards compare today's local calendar date against a value you set, with no network call at all. In a browser that makes them build hygiene rather than enforcement, because the comparison runs against the user's clock and the user owns the clock. On your own infrastructure the clock is yours and kept in step by your own systems, so the same guard becomes a workable way to make a preview or a trial deployment stop serving after a date you chose. It still throws when it fires, so wrap it in something that returns a deliberate response instead of a crash.
The risk that is actually real: protecting the wrong half
The failure this article exists to prevent is not an attacker reading your worker. It is a monorepo that builds browser code and server code from the same pipeline, with the protection step pointed at the wrong target.
Protect the server half by accident and your production stack traces become unreadable, for a benefit that was never available. Miss the client half and the code you meant to protect ships in the clear. Both are quiet failures; neither produces a build error.
The build plugins take this seriously enough to model it explicitly. One of the framework plugins exposes a target selection covering client output, server output or both, and returns early for development builds unless you deliberately opt in. Others declare themselves as build-only, so they are inert while a dev server is running. Read what your plugin is matching rather than assuming the defaults suit a mixed repository, and pair it with the monorepo guidance if you have several packages sharing one configuration.
When the answer flips to yes
There is a genuine case, and it is the same one that makes customer-hosted deployment a topic on this site. If you ship a function that a customer or a partner deploys into their account — an edge handler distributed as part of a product, a serverless component installed by an integrator, a worker script a customer pastes into their own platform dashboard — then the code has left your infrastructure. It sits where somebody else can read it at leisure, and your licence terms are the only other thing between them and reuse.
That is ordinary distribution and deserves the ordinary treatment: name and string transformations, a fixed seed so a rollback is provably the same artifact, a watermark if you need to attribute a leaked copy, and no runtime guards at all. The point of restraint here is that you cannot debug that deployment. It runs somewhere you have no dashboard, so an over-eager guard becomes a support case you cannot reproduce.
The short version
For a function running in your own account: protect the browser bundle, not the handler. Spend the effort on credential scope, dashboard access and removing the debug route, because those are the paths anything real would take. For a function you hand to somebody else to run: treat it as a distributed artifact, use the transformations and skip the guards. In both cases read your build warnings, because the options most people assume are doing the heavy lifting are the three most likely to have been skipped before the output was written.
Frequently asked questions
Who is the adversary when the code never leaves your infrastructure?
Name them before you configure anything, because the answer decides whether the work is worth doing. There are four candidates. The platform that executes your function, which can read it however it is written because it has to run it. Somebody who obtains a deployment credential and pulls your bundle from the platform. An insider with dashboard access. And an accident, such as a source map served publicly or a debug route echoing configuration. Against the platform, transformation is not a control at all. Against the other three it is a delay, and a modest one compared with rotating the credential, restricting dashboard access and removing the debug route.
Do the runtime defense options work outside a browser?
Mostly not, and the failure directions differ by option, which is why enabling a preset is a poor idea here. The domain lock reads the location object and treats a missing or empty hostname as failure, firing its action before the allowlist is ever consulted. The browser and operating system locks read the navigator object and take the same failure path when it is absent, and an unrecognised user agent classifies as unknown, which is never in an allowlist. The developer-tool key blocking and debugger detection register document listeners and compare window dimensions, so they have nothing to attach to. Each of those is a guard that either does nothing or fails closed.
What happens when a guard trips inside a request handler?
Everyone gets an error, not one user. In a browser a tripped guard affects the session in front of it, and the failure actions are written for that world: blanking the document, reloading the page, redirecting. On a server there is no document to blank and no page to reload, so those branches do nothing useful and then throw. A throw inside a request handler is a failed request, and because the same instance serves many users, the blast radius of a false positive is your whole traffic rather than one visitor. If you enable anything here, use the degrade action or your own callback.
Are the self-defending and anti-tampering wrappers usable on an edge platform?
Usually not, for two independent reasons that both show up as build warnings. Those three wrappers evaluate code at run time, and the engine warns that they need a policy permitting dynamic evaluation. Edge runtimes commonly forbid it, in the same way a strict content security policy or an extension manifest does. Separately, the same three are classic-script wrappers and are skipped automatically when the source is an ES module, with a warning, so that import and export linking stays valid. Edge functions are almost always modules, which means the usual outcome is that they were never applied at all.
Is there an option that works better on a server than in a browser?
The date guards, and it is a neat inversion. In a browser the expiry check compares the local calendar date against a value you set, with no network call, so it is exactly as trustworthy as the user's clock. That makes it a build-hygiene tool rather than enforcement. On a server the clock is yours, kept in step by your own infrastructure, so the same guard becomes a reasonable way to make a preview deployment stop working after a date you chose. It still throws when it fires, so route that through a handler that returns a sensible response rather than letting it surface as a crash.
Does console suppression make sense on the server?
It is close to the worst option you could choose there. The setting replaces the console methods with empty functions, and on an edge platform those methods are how your logs reach the dashboard you use to debug production. Turning it on deletes your observability in exchange for hiding output that only you and your platform could see anyway. The same reasoning applies to any environment where the console is a diagnostic channel you rely on rather than a window an end user might open.
What is the real confidentiality risk in a serverless build?
Targeting, not readability. The common accident is a repository that builds browser code and server code together, with a protection step configured against the wrong half or against both. Protecting server code you did not mean to protect makes your production stack traces unreadable for no benefit. Failing to protect client code you did meant to protect ships your work in the clear. The build plugins take this seriously enough to model it: one of them exposes a target selection covering client, server or both, and several are inert outside a production build by default. Read what your plugin is actually matching before you trust the outcome.
When does protecting server-side JavaScript genuinely make sense?
When the code stops being yours to host. If you ship a function that a customer or partner deploys into their own account, you are back in the ordinary distribution situation this site is about: the artifact sits on somebody else's infrastructure, they can read it whenever they like, and the licence terms are the only other thing standing between them and reuse. That case deserves the same treatment as any distributed bundle. A function running in your own account, behind your own credentials, is a different question and usually answered with access control instead.
What configuration would you use if you decide to do it anyway?
Keep it narrow and reversible. Name transformations and string handling give you most of the readability cost with the least runtime risk. Leave every runtime guard off, since they assume a browser and the ones that do fire, fail closed against everyone at once. Pin a seed so two deployments of unchanged source are identical and a rollback is provably the same artifact. Retain the symbol map for every deploy, because a server stack trace you cannot read is worse than one your users never see. And confirm the build warnings, which tell you which options were skipped.
Related reading