JORVEL

Architecture

For engineers integrating JORVEL into a real system. This is the mental model behind the APIs: where code runs, what crosses the host↔remote boundary, and how a request becomes HTML.

Topology

A host (shell) is a normal app that, at runtime, loads one or more remotes— each its own independently-built, independently-deployed bundle served from its own URL/CDN. The host's route table maps URL prefixes to remotes; each remote resolves its own sub-paths.

DiagramIris = host, lime = remote — the same colors used across the docs.

The share scope

Module Federation keeps singletons(React, ReactDOM, the JORVEL runtime, the event bus) in a shared scope so host and remotes use the same instance — one React, one router, one event bus. JORVEL's defaults:

  • Host sets eager: true on shared deps — it owns and populates the scope before any remote loads.
  • Remote sets eager: false — it lazy-resolves from the host scope via an async boundary (the generated src/main → import('./bootstrap') shim).
  • A version mismatch on a singleton is surfaced by checkVersions (warn/error) so duplicate React copies don't slip in.

Why the async boundary matters

Importing shared deps synchronously before MF initializes the scope causes a loadShareSync failure. The generated main.{tsx,jsx} defers all imports behind import('./bootstrap') — keep that boundary.

The two-tier router

Built on the History API — no react-router. The host router owns top-level URLs; a remote's router owns its subtree. Both read the same usePathname stream, so they stay in sync without a shared provider. Navigation is a jorvel:navigate custom event, so a remote can navigate the host without importing it.

ts
// host maps prefixes → remotes
const HOST_ROUTES = [{ path: '/dashboard/*', remote: 'dashboard', module: './App' }];
// remote resolves the tail with its own file-based routes (jorvel routes)

Request lifecycle (SSR)

DiagramOne request, server side — each hop can short-circuit the next.
  1. Middleware runs first (auth/geo/rewrite) — runMiddleware → next / redirect / rewrite / respond.
  2. API routes get a chance (createApiRouter().handle()); non-match falls through.
  3. ISR cache check (serveWithISR) — fresh cache short-circuits render; stale serves + background-regenerates.
  4. Loaders run in a per-request context (runLoaders), data serialized for hydration.
  5. RenderrenderRouteToString / streaming (renderToReadableStream); remotes SSR'd via ssrRenderRemote.
  6. Head + assets injected (CSP nonce, SRI, preloads, critical CSS); response returned by the platform adapter.

What crosses the boundary

Crosses host↔remoteStays local
Shared singletons (React, runtime, event bus)Component state, refs
Event-bus messages (typed contract)A remote's internal routes/pages
Shared state stores / atoms (globalThis-pinned)CSS (isolate with Shadow DOM / CSS Modules)
The navigation stream (URL)A remote's BFF / server routes

Failure isolation

A remote is a network dependency — treat it like one. RemoteOutlet wraps loads in an error boundary; loadWithFallback retries against the last-good URL; a feature-flag kill-switch + circuit breaker skip a bad remote so the host degrades instead of crashing.

Deep dives

Federation (contracts, SRI, allowlist, diff/impact/canary) · SSR/ISR · runtime API.