Journey
MilestoneDesign SystemsdecisionMonorepo

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.

our working rule now

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
Expected — wire up module federationReality — the boring package hygiene that was 90% of it
The exciting part turned out not to be the part that ships value.

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

  1. ui.pangaea.id — the design system catalog
  2. labspangaea/pangaea.ui — the repository