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.
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: trueon 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 generatedsrc/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
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.
// 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)
- Middleware runs first (auth/geo/rewrite) —
runMiddleware→ next / redirect / rewrite / respond. - API routes get a chance (
createApiRouter().handle()); non-match falls through. - ISR cache check (
serveWithISR) — fresh cache short-circuits render; stale serves + background-regenerates. - Loaders run in a per-request context (
runLoaders), data serialized for hydration. - Render —
renderRouteToString/ streaming (renderToReadableStream); remotes SSR'd viassrRenderRemote. - Head + assets injected (CSP nonce, SRI, preloads, critical CSS); response returned by the platform adapter.
What crosses the boundary
| Crosses host↔remote | Stays 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
