The federation we didn't build
Every surface of pangaea.id grew from one rv-* system — but it was trapped inside a single app. We
wanted to share it. The exciting way to do that was the flashiest; we shipped the most boring one.
Here's why — and what we'd tell ourselves on day one.
The tempting path
"Share the UI across surfaces" almost always leads to one sexy answer:
micro-frontendsⓘ with module
federationⓘ — each component a remote the host loads at runtime, with a semver-gated resolver in
the middle. We spent real time pressure-testing it: runtime vs server-side, version-gated remotes, a
DIY versions.json + maxSatisfying, all the way to Zephyr Cloud.
The decision
We didn't need any of that — not yet, maybe not ever. All we needed was UI that was reusable and
versioned. So we shipped the boring thing: one monorepoⓘ holding one
versioned package per component (@labspangaea/ds-button, ds-card, …), plus a live catalog at
ui.pangaea.id.
Runtime module federation
- Remotes fetched by the host at runtime (client-side)
- Crawlers see an empty page
- Needs a resolver on a server
- Fights the CSP + static deploy
Monorepo of versioned packages
- Components imported at build time, prerendered
- Crawlers & answer engines read everything
- No server, no extra runtime
- Each component versions on its own
The nice part: a per-component monorepo is already ~90% federation-ready. React as a peer dependency, each package an isolated ESM with its own CSS, tokens split into their own package. If a component ever genuinely needs to be federated, it's a bolt-on — no rewrite. We kept the door open at near-zero cost; we just didn't build the machinery (YAGNIⓘ).
The exciting architecture is usually a distribution problem in disguise. Ship the boring thing, as long as it keeps the door open.
A bonus: releases from 2 PRs to 1
The first release took two manual merges: your code PR, then the Version PR that bumps the versions. That second merge was the chore. We fixed it to one run: the bot opens and merges the Version PR, then publishes — all in one CI run. The only manual step left is writing the changeset.
What we'd tell ourselves on day one
Do
- Package components as isolated, versioned units (peer React, own CSS)
- Keep the federation door open through package hygiene, not machinery
- Automate releases until the only manual step is writing the intent
Don't
- Build runtime module federation for pages a crawler must read
- Trade one monolith app for over-orchestrated remotes
- Add a server just to avoid one build step
The full story of how it's built — the monorepo, the deploy, and Changesets — is in the technical write-up: Shipping a design system at ui.pangaea.id.
Sources