rwe-core vs whisker-host
An implementation-grounded comparison of rwe-core and whisker-host,
scoped to what actually exists in both today: module federation, iframe
embedding, local development, deployments, and host architecture.
whisker-service-kit, the planned whisker-bff, AI tooling, and
platform-wide CI orchestration are out of scope here.
Each system makes trade-offs appropriate for its operating model; neither is uniformly “better.” Where one is genuinely cleaner for a given concern, this doc says so.
| rwe-core | whisker-host | |
|---|---|---|
| What is it? | A 4-package monorepo: rwe-host, rwe-shell, rwe-demo-app (example only), rwe-internal-shared. Real widgets/apps live in other repos. |
A single host application (this repo also houses kits, WCL, and the manifest-publisher Lambda — but the host is one app). |
| Federation modules today | One demo app, one auth/shell module, three widgets. Most user-facing apps are iframes. | Federation modules and iframe consumers coexist by manifest field. |
| Iframe consumers | Hand-listed in apps.json, hand-routed in App.tsx |
Discovered from the dynamic manifest registry; rendered uniformly by IframeRenderer |
| Module registry source of truth | Three hand-edited files in rwe-host |
src/moduleManifest.ts → S3 → Lambda → DynamoDB, fetched at runtime |
| Operating model | Host team owns the registry; module teams PR against rwe-host |
Module teams own their own repo/manifest/CI — no host PR to register |
Module Federation
Section titled “Module Federation”| rwe-core | whisker-host | |
|---|---|---|
| Plugin | @originjs/vite-plugin-federation v1.4 |
@module-federation/vite (Zack Jackson’s maintained Vite-native plugin) |
| Vite / React | 6.4.x / React 18.3 | 7.x / React 19.2 |
| Shared packages | 2 (react, react-dom), array form — no singleton guarantee |
~17, explicit singleton: true + eager: true for the framework + full MUI/Emotion stack + WCL |
| Host-to-module API | retailUXShell/core exposes ~13 named exports + two export * re-exports |
remoteSafeSelectors.ts exposes exactly 4 named exports |
| Remote wiring | vite.config.ts reads hand-edited registry files at build time |
Generated by scripts/generate-remote-loader.js from the live DynamoDB registry |
Trade-off: rwe-core’s small share list means fewer compatibility
constraints for small widgets, but any provider-shaped pattern (Theme,
Auth, Snackbar) gets duplicated by default. Whisker’s wider, singleton
share list is what makes shared WCL providers, Emotion caches, and MUI’s
styled-engine actually work across modules — the cost is a larger surface
for the host to keep in lockstep.
Trade-off: rwe-core’s ~13-export surface is powerful (a module can
render <Shell>, use raw auth functions) but every export is a
backwards-compat liability. Whisker’s 4-export surface is tighter to evolve
but forces modules through WCL for shared components.
Federation as routing primitive vs registry
Section titled “Federation as routing primitive vs registry”| rwe-core | whisker-host | |
|---|---|---|
| Pages | Hand-listed <Route> in App.tsx from app-registry.ts |
Computed from manifest pages[]; componentPath tells the host which export to load |
| Widgets | WidgetHostProvider + explicit notifyReady/notifyHide SDK; per-operator dismiss persisted in localStorage |
Rendered from manifest widgets[]; no equivalent imperative SDK yet |
rwe-core’s widget SDK (window.rweWidgetHost + notifyReady/notifyHide +
per-operator dismiss) is the strongest single piece of prior art in either
system — worth studying if whisker-host grows its own widget lifecycle
convention (see Risks & improvements #16).
Local development
Section titled “Local development”rwe-core has two distinct scenarios that are easy to conflate:
- Demo (
pnpm install && pnpm devinrwe-core): host + shell +rwe-demo-app, a reference implementation, not a real module. One repo, minutes to running. - A real module: needs
rwe-coreand the module’s own repo, with no scaffolder — copyrwe-demo-app, hand-wire federation and CI yourself, then get a host-team PR merged to register it inapp-registry.ts/widget-registry.ts/apps.json.
whisker-host assumes host and module live in separate repos from the start:
cd my-new-modulepnpm dev# → generate + validate manifest → locate/auto-clone whisker-host# → whisker:prep on host → build module (VITE_IS_LOCAL=true)# → rebuild host's remote loader → build + start host on 3001# → vite --watch + browser-sync proxying the hostwhisker dev (test BFF) and whisker dev:prod (prod BFF) both run the
host + your module locally while loading the rest of the platform from a
BFF; VITE_BFF_URL overrides either to point at a local BFF instead. The
manifest endpoint needs no auth either way.
Honest call-out: rwe-core is simpler only for “run the built-in demo.” For “run the host against a real module,” both systems need two repos — the difference is that whisker scaffolds and orchestrates the module side, while rwe-core gives you a demo app to copy and a registry file to edit by hand (plus a host-team PR).
Deployments
Section titled “Deployments”rwe-core: one Jenkinsfile per package (host/shell/demo-app), all
deploying to a single shared S3 bucket with a Lambda@Edge rewrite per
prefix. External federated modules and iframe apps deploy independently but
still need cross-team IAM coordination to add a new bucket prefix. Adding a
new module/widget to production is 5–8 PRs against rwe-host,
depending on type.
whisker-host: two automated tracks — see Manifest pipeline & deployment for the full mechanism. Federation bundles land in per-module S3 prefixes under one CloudFront; adding a new module to production is zero PRs against whisker-host, ever.
| Concern | rwe-core | whisker-host |
|---|---|---|
| PRs to host to add a module | 5–7 (federation page) / 1–3 (widget) / 4–7 (iframe) | 0 |
| Per-env admin enable/disable | Manual code edit + redeploy | enabled field flips via the (planned) whisker-config-panel; admin merge already preserves it |
| Origin allow-list | Static union of apps.json |
Derived dynamically from the live registry |
| Schema validation at ingest | None — TypeScript at host build time only | ModuleManifestSchema.parse() in the Lambda, same schema the kit uses |
Honest call-out: for a small team with ≤5 modules on the same cadence,
rwe-core’s “edit a file and ship” model is genuinely faster — no Lambda to
debug, no DLQ to triage. For many teams on independent cadences, the
trade-off flips: every whisker module addition is a git push in the
module’s own repo, at the cost of a Lambda + DynamoDB + DLQ the platform
team has to run.
Host architecture
Section titled “Host architecture”Boot order: rwe-core’s Shell (auth + nav) is itself a federated
import — the host’s first paint depends on retailUXShell/remoteEntry.js
being reachable. whisker-host’s auth lives in its own bundle; boot doesn’t
wait on any federated remote, and manifests fetch in parallel.
Auth: both speak the same useAuth() contract shape
(accessToken, loggedIn, login, logout, userInfo) and the same
T.R.U.E-compatible iframe protocol — rwe-core’s auth lives inside the
federated rwe-shell; whisker-host’s lives directly in the host.
Iframe handling: rwe-core wraps each app in a hand-written component and
assigns window.onmessage = globally — which means only one iframe app can
be active at a time; switching apps swaps handlers. whisker-host’s
IframeRenderer is one manifest-driven component with a per-iframe message
handler (useIframeMessageHandler), so multiple iframes can coexist safely.
Targeting in Playwright uses the iframe’s title attribute (set to the
module name), e.g. page.frameLocator('iframe[title="sim-web-ui"]').
Shared components: rwe-core has none — each team picks its own UI library, so federated modules can carry duplicate MUI/EDS copies with no shared theme. whisker-host has WCL, a single published, versioned component + provider library every module shares.
Observability: rwe-core calls NewRelic directly with hand-defined event
names; whisker-host’s useModuleAnalytics(name) abstracts that away and
auto-tags every event with its source module (see
Analytics architecture).
What each system has that the other doesn’t
Section titled “What each system has that the other doesn’t”rwe-core: a genuine widget SDK with per-operator dismiss persistence; a
federated, in-principle-replaceable Shell; standalone auth helpers
(getStoredUserInfo(), getAccessToken()) outside useAuth(); a lower
operational footprint (no Lambda/DynamoDB/DLQ); a working in-repo demo.
whisker-host: a dynamic registry (no host PRs to add a module); a
versioned, queryable host contract artifact; end-to-end schema validation
(scaffold → commit → CI → ingest, one Zod schema); admin-aware
enable/disable without a redeploy; a wider federation share list with
explicit singleton/eager control; unified iframe handling with per-iframe
handlers and Playwright-targetable markup; whisker doctor pre-flight;
an AI-context surface (thundercats init/audit); a first-class shared
component library.
Which system fits which situation
Section titled “Which system fits which situation”- Single team, ≤5 modules, same release cadence, comfortable owning your own federation/CI setup → rwe-core’s model is fine, arguably simpler operationally, and the widget SDK is the most mature piece of architecture in either system.
- Many teams, many modules, independent cadences → whisker’s model is what the scale forces; the Lambda/DynamoDB/contract pipeline pays for itself once host-team PR review becomes the bottleneck.
- You want a federated Shell other apps could swap in → rwe-core supports this; whisker doesn’t.
- You want a host-as-platform with a shared component library, shared analytics, and admin-gated rollout → whisker.
Things both systems should keep
Section titled “Things both systems should keep”T.R.U.E-compatible iframe protocol (interoperability between hosts),
per-route error boundaries with NewRelic context, React Router 7, and
federated modules consuming auth via a single named useAuth import.
Worth whisker-host adopting
Section titled “Worth whisker-host adopting”An imperative widget SDK (notifyReady/notifyHide + per-operator
dismiss) — already on the roadmap, see
Risks & improvements #16.
