Architecture
Published
Obfuscation has one structural assumption: it can see everything that refers to a name. A bundler satisfies that assumption comfortably, which is why protecting a normal application is mostly a matter of picking a preset. A federated architecture breaks the assumption on purpose — that is the entire point of it — so the interesting work is identifying the seam and writing it down.
Why the project boundary matters
Cross-file global renaming exists because ordinary name mangling is deliberately conservative about globals: renaming a global in one file breaks any other file still referring to the old name. The fix is a single mapping applied consistently across every file protected together, and the documentation is explicit that this only works properly when the related scripts are protected in the same project.
That sentence is the whole article. A host and a remote are not in the same project. They are separate builds, run by separate pipelines, frequently by separate teams, on separate days. Neither can see the other’s generated mapping, and generated names are chosen per build. Two independently protected bundles will assign g9 to entirely unrelated things, and a name that was consistent within each build is meaningless between them.
Inside one bundle this never comes up, because the bundler resolves every reference itself and simply does not care what anything is called. Across the federation boundary nothing resolves anything. The host asks a container for a module by a literal string, the container hands back an object, and the host reads properties off it by literal name. Those literals are a wire format, and a wire format is something you version rather than something you rename.
What is actually on the contract
Work out the seam before touching a preset. In a Module Federation setup it usually consists of:
- The container global. Each remote publishes itself under a global name that the host looks up. If that name is renamed in the remote’s build, the host’s lookup finds nothing.
- The container methods.
init and get are called by the host on an object the remote built. They are property names on a cross-build object, which is the definition of the risky category.
- The shared-scope registry. Host and remotes both read and write the shared scope to deduplicate common libraries. Its structure is agreed by convention between builds, not derived within one.
- Module request strings. The keys in
exposes and shared and the specifiers a host passes to get. These are strings, so the transforms preserve their values — but they must still match what the other side expects.
- Anything else resolved by name at runtime — custom element tag names, event names on a shared bus, keys in a shared store, and callbacks a host registers with a remote by property name.
This is the same category of surface described in obfuscating web components and in the widget and SDK guide: a public boundary the transforms must be told about, because nothing in the build can infer it.
Two ways to keep names stable
Once you have the list, there are two mechanisms, and they answer different questions.
Exclude it — when the name should stay as written
Reserved-name patterns keep matching identifiers untouched, mapping to the engine’s variable exclusion list. This is right for the contract surface itself: container globals, exposed module names, event names. You are declaring “this is public API, leave it alone”, which is exactly what it is. A regular expression covering a naming convention — an agreed prefix for cross-boundary symbols, say — scales better than enumerating names in eleven repositories.
Pin it — when the name should change but change identically everywhere
Custom identities let you assign specific mappings instead of generated ones. Written as checkValue:? the name is renamed to something generated; written as checkValue:d01 it is renamed to exactly d01. That second form is the one that crosses a build boundary, because a fixed mapping checked into a shared configuration is a shared mapping — it just came from you rather than from the generator.
This is genuinely useful when the contract has internal names you would rather not publish in readable form, but which several builds must agree on. Keep the pinned list small and versioned; it is a schema, and it will outlive the people who wrote it.
Member renaming is the sharp edge
If one transform is going to break a federated system, it is property renaming. Everything crossing the boundary does so as an object — the container, the module namespace, the component, the shared store — and property names on those objects are the API.
Within a single build, renaming members is well-behaved because every access site is visible. Across builds it is not, because half the access sites are in someone else’s bundle, compiled last Tuesday. The remote renames getPrice to a; the host, protected separately or not at all, still calls getPrice; the call returns undefined and fails somewhere unrelated.
The practical rule is to scope it. Member renaming belongs to code that stays inside one build. For the exported surface, exclude by pattern, and prefer an explicit adapter module at the boundary over letting internal objects escape directly — a small, deliberately public façade is easier to exclude correctly than a sprawling object graph that grew a public consumer by accident.
The advantage nobody mentions
Federation makes one thing about protection distinctly better, and it is worth saying because most of this article is warnings.
Protection is applied per build, so protection strength becomes a per-remote decision. In a monolith the preset is a single compromise across everything: the pricing engine and the cookie banner get the same treatment, and the overhead of a maximum preset lands on code that did not need it. In a federated system the entitlement remote and the risk-scoring remote can run at maximum strength, the reporting remote at something moderate, and the marketing banner unprotected — with no cross-team coordination beyond the shared exclusions.
That is a real improvement in the cost side of the trade described in the bundle-size article. You stop paying maximum overhead on the whole surface to protect the ten percent that matters.
Caching, hashes and where protection sits
Federation and long-term caching interact closely, since remotes are fetched at runtime by URL and cached hard. The relevant detail is ordering: the webpack integration runs during asset optimisation, before webpack computes content hashes. Protected content is therefore what gets hashed, so filename hashes still describe the bytes actually served and a remote whose source did not change keeps its cached filename.
The caveat is polymorphic output. Without a seed, every build produces different protected bytes, so every build produces a different hash and every remote busts its cache on each release even when nothing meaningful changed. Set a seed per remote and rebuilds of unchanged source stay byte-identical. The caching article covers the mechanics; in a federated system the effect is multiplied by the number of remotes on the page.
Reports, and the operational trap
This is the part teams discover after the first production incident.
Symbolication translates an obfuscated stack trace back to real names using the protection report from the build that produced those exact bytes. In a monolith there is one report per release and the workflow is obvious. In a federated system a single stack trace can pass through the host and three remotes, each protected separately, each on its own release train. Four builds, four reports, one trace.
What that requires:
- A report per remote per release, stored centrally rather than in each team’s build artifacts, where it will be garbage-collected after thirty days by a retention policy nobody reviewed.
- A build identifier available at runtime and attached to error payloads, so a frame can be matched to the report that explains it.
- Frame-level attribution in the symbolication step. A workflow assuming one report per page will happily translate a remote’s frames against the host’s report and produce confident nonsense — worse than an untranslated trace, because it looks like an answer.
- Platform-level retention. If each team decides how long to keep reports, some team’s decision will be the one you need in eighteen months.
The short version
A federated architecture deliberately removes the whole-program view that obfuscation relies on, so the seam has to be declared rather than inferred. Write down the contract — container globals, init and get, shared scope, module request strings, anything else resolved by name at runtime — then exclude it by pattern or pin it with fixed custom identities. Keep member renaming inside build boundaries. Give each remote its own seed so caching keeps working, and treat protection reports as centrally owned infrastructure, because one stack trace will need several of them at once. Do that and per-remote protection becomes a feature rather than a coordination problem: strong where the value is, absent where it is not.
Frequently asked questions
Can a host and a remote share one obfuscation mapping?
Not a generated one. Cross-file global renaming works by applying a single shared mapping to every file protected together in the same project, which is precisely why the documentation describes it as a cross-file feature that only behaves correctly when the related scripts are protected as one unit. A host and a remote are separate builds run by separate pipelines, often by separate teams on separate days, so they never see each other’s mapping. What you can share is a fixed mapping you wrote down yourself, using custom identities to assign explicit names rather than generated ones.
Which identifiers form the contract between federated builds?
Anything resolved by name at runtime rather than at build time. That includes the container globals a remote exposes and the init and get methods on them, the shared-scope registry the host and remotes both write into, the module request strings used in exposes and shared configuration, and any custom element tag names or event names crossing the boundary. Inside a single build a bundler can rename freely because it resolves every reference itself. Across builds nothing resolves anything — the names have to match literally.
Is renaming object members safe in a micro frontend?
It is the transform most likely to break a federated boundary, because property access is how modules talk to each other once the objects are shared. A component exposed by a remote and consumed by a host is an object whose property names are the API. Member renaming rewrites those names within the build that ran it, and the other build never learns about it. Either exclude the exported surface by pattern, or keep member renaming for code that stays inside one build and never crosses the boundary.
Do all teams have to use the same protection settings?
No, and that is one of the genuine advantages of the architecture. Protection is applied per build, so each remote can choose a preset proportional to what it actually contains. A pricing engine or an entitlement module can run at maximum strength while a marketing banner remote runs unprotected, with no coordination beyond agreeing on the shared contract names. The only settings that need to be agreed across teams are the exclusions covering that contract.
How does version skew interact with protection?
It makes an existing problem more visible rather than creating a new one. Federated systems already run a host built weeks ago against a remote deployed this morning, and any incompatibility is a contract mismatch. Protection adds nothing to that as long as the contract names are pinned, because the transforms never touch excluded identifiers. What changes is diagnosis: an error from a skewed pair arrives with obfuscated names, so you need to know which build produced it before the stack trace means anything.
How do I symbolicate a stack trace when eleven remotes ship independently?
Store one protection report per remote per release and key them by something present at runtime, such as the remote name and its build version. A trace can cross several remotes in a single call stack, so a symbolication workflow that assumes one report per page will silently mistranslate frames. Emit a build identifier into your error payloads, keep reports indefinitely rather than for a release window, and make retention a shared platform responsibility rather than each team’s own decision.
Related reading