Testing Against Production in Seconds: Module-Federation Override Tooling
Built a Webpack 5 runtime-override system and browser extension for a 60-micro-frontend platform where no lower environment matched production — turning hours of setup into seconds.
- Role
- Solutions Architect
- Where
- Multi-billion-dollar equipment-rental company (consulting engagement)
- TypeScript
- Webpack 5 Module Federation
- Browser Extension APIs
- React
Context
A large customer-facing platform composed of 60 micro-frontends owned by 4 teams, stitched together with Webpack 5 Module Federation. The catch: no lower environment matched production — real data, real users, and real auth existed only in prod.
The Problem
To verify a change against realistic conditions, developers burned hours standing up partial environments that still didn't reproduce production behavior. Cross-team bugs were routinely discovered only after release.
The Solution
A runtime-override system plus a custom browser extension: any developer can point any individual micro-frontend at their local or branch build while browsing actual production — real data, real users, real auth — without affecting anyone else.
How It Works: Promise-Based Dynamic Remotes
Researching Webpack's federation internals, I found
promise-based dynamic remotes
— instead of hardcoding each remote's URL at build time, the host resolves the remote
container through a promise evaluated at runtime. I wired that in as the swapping
mechanism: on page load, the promise checks localStorage for an override entry and resolves
to the developer's local or branch remoteEntry.js if one is set — otherwise it falls
through to the production remote.
The browser extension is the write side of that contract. It writes override entries into
localStorage, and it's brand- and environment-aware: it knows which domain it's on and
scopes each override to that specific domain, so pointing MFE #12 at a local build on one
brand's production site doesn't touch the same MFE on another brand or environment.
The scoping falls out of the design for free: localStorage is per-browser, per-origin.
Overrides exist only in the individual developer's browser on the specific domain they set
them — production stays untouched for everyone else, with no server-side changes at all.
flowchart LR
Ext["Browser extension<br/>(brand & environment aware)"] -->|writes overrides| LS[("localStorage<br/>per browser, per domain")]
LS -->|read at runtime| PR["Promise-based dynamic remotes<br/>in each host's webpack config"]
Dev["Developer's local build<br/>serves remoteEntry.js"] --> PR
PR --> MFE1["MFE #12 overridden ✅"]
PR --> MFE2["59 other MFEs untouched —<br/>and untouched for every other user"]
Deployment Tooling
Releases had the same disease as testing: the coordination knowledge lived in people's heads. Deploying one application meant going to Jenkins, kicking off the job, opening the app in the IDE to check whether any of its dependencies were in the same release and could be triggered early, hand-typing everything including the version number (which got mangled frequently), watching Jenkins until the job finished — then starting the whole loop over for the next app downstream.
I built a dependency-aware release orchestrator that collapsed that loop to: pull the release plan from Confluence with the extension, paste it into the release-plan parser, save, deploy.
Under the hood:
- Dependency manifest — a JSON declaring each application's dependencies, used to coalesce the release into a pipeline rendered as nodes and connecting lines across lanes, so the deploy order was visible instead of tribal
- Live release state — the extension cloned the developer's Jenkins session cookie and reused it against the Jenkins API, polling job statuses so every deployment in the release lit up in real time, including what was queued next
- Version awareness — the tool pinged every brand and environment's
version.txtto learn what was actually deployed, then skipped any app whose target version was already live — eliminating both wasted redeploys and hand-typed version numbers
flowchart LR
CONF["Release plan<br/>(Confluence)"] -->|"extension pulls →<br/>parser ingests"| ORCH["Release<br/>orchestrator"]
DEP["Dependency manifest<br/>(JSON per app)"] --> ORCH
J["Jenkins API<br/>(session cookie via extension)"] -->|job statuses| ORCH
V["version.txt per<br/>brand & environment"] -->|deployed versions| ORCH
ORCH --> VIS["Pipeline view: nodes & lanes,<br/>real-time state + queue"]
Outcome
The tooling didn't just make testing faster — it changed what was testable:
- End-to-end and authentication flows became testable at all. Previously, e2e operations and most auth-dependent scenarios couldn't be meaningfully exercised before release. Now they run against real production conditions, locally.
- Cross-app features light up locally. A feature spanning multiple micro-frontends can be overridden all at once — every affected app pointed at a local build in a single browser.
- Shared test branches, no deployment. Deploy a feature to a shared test branch and point a remote override at it — anyone can exercise a working feature against production without touching a release.
- No more container gymnastics. Testing a single app used to mean standing up its container just to get it working. Now it's an override entry in the extension.
- Verifying a change against production conditions went from hours to seconds — with zero effect on other developers or real users, and release mistakes prevented by construction rather than by checklist.