JORVEL
Mental model

Core concepts

JORVEL is opinionated. Before you wire it up it helps to understand what each layer is, what guarantees it makes, and where the seams are. This page is the 10-minute orientation.

Host vs. remote

Every JORVEL workspace contains exactly one host (the shell that owns the URL bar, layout, and global state) and one or more remotes(independently deployable apps that mount sub-trees of the URL). They're both real React apps; the only difference is whose URL the user sees.

text
my-app/
├── apps/
│   ├── shell/             # host  — owns "/", "/login", layout chrome
│   │   └── jorvel.app.json  # { "type": "host", "port": 3000 }
│   ├── dashboard/         # remote — owns "/dashboard/*"
│   └── billing/           # remote — owns "/billing/*"

At runtime, the host fetches each remote's remoteEntry.js only when its URL is hit. Remotes can be deployed to a totally different CDN, on a different release cadence, by a different team.

The federation contract

Module Federation is a runtime contract: a remote exposes module IDs, the host consumes them by name. JORVEL gives you a typed wrapper.

libs/contracts/src/index.ts
ts
import { defineFederationContract } from '@jorvel/types';

export const dashboardContract = defineFederationContract({
  name: 'dashboard',
  exposes: {
    './App': './src/remote.tsx',
    './widgets/UsageChart': './src/widgets/UsageChart.tsx',
  },
  emits: {
    'dashboard:opened': null,
    'dashboard:action': { action: 'string', payload: 'unknown?' },
  },
  listens: {
    'shell:ready': { timestamp: 'number' },
  },
});

Now the host can useEventBus<DashboardContract>() with full IntelliSense.

The runtime contract

Both host and remote import @jorvel/runtime, and the package is configured as a Module Federation singleton. That means navigation, prefetch caches, and the event bus are a single object across the whole page — no duplicate state.

Singleton or you're going to have a bad time

If @jorvel/runtime, @jorvel/event-bus, or @jorvel/statearen't singletons, the host and remote will see different instances and silently lose events. The CLI sets this up by default — only override it if you know exactly why.

Rendering modes

  • SPA — the host bootstraps in the browser, lazy-loads remotes on navigation. Default for dev.
  • SSR@jorvel/ssr renders matched routes on the server, streams to the browser, and hydrates. Edge-runtime safe.
  • SSGstaticExport() pre-renders a fixed list of routes to disk with a content-hash manifest. Worker-pool parallelism by default.

Security primitives

Federation surfaces three threat classes: untrusted remotes, XSS through hydration payloads, and CDN tampering. @jorvel/security ships:

  • Origin allowlist with * single-label and ** multi-label wildcards.
  • SRI for remoteEntry.js — Web Crypto, edge-runtime safe.
  • CSP builder with strict-dynamic + base64url-validated nonce.
  • safeJsonForScript for hydration state injection.

Release model

JORVEL uses Changesets. The configured linked groups are [runtime, ssr, security], [state, event-bus, events], and [adapter-*]. CLI / types / UI / observability / rspack-route-assets bump independently. Examples and docs are ignored.

Lifecycle at a glance

Every interactive frame goes through the same five stages. Drop a breakpoint at any of them when debugging.

DiagramIris stages run in the host; lime stages run in the loaded remote.

Design principles

Two opinions shape every API. Knowing them up front explains the smaller decisions.

  1. Globals beat prop-drilling for cross-app state. @jorvel/runtime, @jorvel/event-bus, and @jorvel/state pin singletons to globalThis — so two bundles of the same package still observe one router, one bus, one store registry. The federation share-scope is the fast path; globalThis is the safety net.
  2. Boundaries are HTTP requests, not React props. A remote crashing should not crash the host. RemoteOutlet wraps each load in an ErrorBoundary, fetchHealth() can gate a load, and RemoteRegistry rejects URLs outside the configured allowlist.

When NOT to use JORVEL

  • One frontend team. Module Federation buys independent deploys; without ownership splits the overhead is pure cost. Pick Next.js / Astro / Vite-React instead.
  • Static marketing sites.SSG-only? Use Astro. JORVEL's SSG is a feature of a federated app, not a destination.
  • Server-driven React (RSC).Not supported — federation + the RSC wire format aren't compatible upstream yet. JORVEL ships islands, streaming SSR, use(promise), and hydratable server stores instead. Need RSC today? Ship Next.js App Router.

What's next?

With the model in your head, the rest of the docs read in any order. Start with Routing if you're building, or Federationif you're wiring up shared deps.