Microfrontends solve a real problem: letting multiple teams ship frontend code independently without stepping on each other. But the implementation usually involves a week of webpack config, Module Federation plugins, custom shell apps, and fragile runtime contracts.
What if it was just... an import?
import { loadRemote, configureSecurity } from 'onefold/remote';
Onefold is a reactive UI framework that ships microfrontend support as a first-class feature. No plugins, no extra packages, no build tool gymnastics. In this article, I'll walk through how it works and build a working multi-team dashboard from scratch.
The Problem With Traditional Microfrontend Setups
If you've tried microfrontends before, you've probably dealt with:
- Module Federation — powerful but deeply coupled to webpack, complex config, version mismatches between shared dependencies
- iframe-based isolation — simple but terrible UX (no shared styling, no proper resizing, navigation is painful)
-
Custom loaders — hand-rolled
<script>injection with no security model, no integrity checks, no CSS isolation -
Runtime CSS conflicts — Team A's
.cardclass stomps Team B's.cardclass - No security model — anyone who controls a CDN can inject arbitrary code into your app
Onefold addresses all of these with a small, focused API that handles the hard parts: security, isolation, loading, and error handling.
The Architecture
The mental model is simple:
Host Shell (your main app)
│
├── loadRemote('https://billing.cdn.com/widget.js')
│ └── Team: Payments (deployed independently)
│
└── loadRemote('https://analytics.cdn.com/widget.js')
└── Team: Data (deployed independently)
Each remote is a standard ES module that exports a function returning a DOM Node. The host loads these modules dynamically, verifies their integrity, and mounts them with CSS isolation.
No special bundler plugin. No runtime registry. Just ES modules over HTTP.
Security First: configureSecurity
Before loading any remote code into your app, you establish a security perimeter. Call this once at app startup — before any loadRemote calls:
import { configureSecurity } from 'onefold/remote';
configureSecurity({
trustedOrigins: [
'https://billing.cdn.com',
'https://analytics.cdn.com',
],
requireIntegrity: true,
timeout: 10000,
blockAll: false,
});
Any loadRemote() call that targets an origin not in the trusted list will be blocked immediately — the module never loads, the code never executes.
The 7 Security Layers
Onefold's remote loader enforces multiple layers of protection:
-
Origin Allowlist — Only modules from
trustedOriginscan load. All other origins are rejected before any network request. -
SRI Integrity — When
requireIntegrityis true, fetched content is verified against a SHA hash before execution. A compromised CDN can't serve malicious code. - Timeout — Modules taking longer than the timeout are aborted. Prevents slow-loris attacks.
- Isolation — Shadow DOM or iframe sandboxing prevents DOM/CSS leaks between host and remote.
-
CSP Compatible — No
eval(), noFunction(), no inline scripts. Works with strict Content-Security-Policy headers. -
Credential Isolation — Remote fetches use
credentials: 'omit'— cookies are never sent to remote origins. -
Kill Switch — Set
blockAll: trueto instantly disable all remote loading in production.
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
trustedOrigins |
string[] | [] |
Allowed origins (protocol + host + port). No wildcards. |
requireIntegrity |
boolean | false |
Require SRI hash for every remote load |
blockAll |
boolean | false |
Emergency kill switch — blocks all remotes instantly |
timeout |
number | 10000 |
Maximum load time in ms before failing |
Loading a Remote: loadRemote
Here's the core API:
import { html } from 'onefold';
import { loadRemote } from 'onefold/remote';
const BillingWidget = loadRemote({
url: 'https://billing.cdn.com/widget.js',
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6...',
isolate: 'shadow',
permissions: ['dom'],
fallback: () => html`<div class="spinner">Loading billing...</div>`,
onError: (err) => html`<div class="error">Billing unavailable: ${err.message}</div>`,
});
// Use it like any other component
function Dashboard(): Node {
return html`
<div>
<h1>Dashboard</h1>
${BillingWidget({ accountId: 'ACCT-7291' })}
</div>
`;
}
loadRemote returns a component function. You call it with props, and it returns a Node — just like any other Onefold component. The loading, verification, and mounting happen transparently.
loadRemote Options
| Option | Type | Default | Description |
|---|---|---|---|
url |
string | required | URL to the remote ES module |
exportName |
string | 'default' |
Which export to use as the component factory |
isolate |
`'none' \ | 'shadow' \ | 'iframe'` |
integrity |
string | — | SRI hash (required when requireIntegrity is on) |
permissions |
array | ['dom'] |
What the remote can access |
fallback |
() => Node |
— | UI shown while loading |
onError |
(err: Error) => Node |
— | UI shown on failure |
props |
object | — | Static props (can also pass dynamically at call time) |
timeout |
number | — | Override global timeout for this specific remote |
Isolation Modes
You get three levels of isolation depending on your trust model:
'none' — Direct Mount
The remote's DOM is inserted directly into the host page. Fastest rendering, but CSS can conflict. Use only for code you fully trust and control (same team, same design system).
const InternalWidget = loadRemote({
url: 'https://internal.cdn.com/header.js',
isolate: 'none',
});
'shadow' — Shadow DOM Encapsulation
The remote is mounted inside a closed Shadow DOM. Its CSS cannot affect the host, and the host's CSS cannot bleed in. This is the sweet spot for most multi-team setups:
const TeamWidget = loadRemote({
url: 'https://widgets.company.com/billing.js',
isolate: 'shadow',
});
The Shadow DOM uses mode: 'closed' — external JavaScript cannot pierce into the remote's internals.
'iframe' — Full Sandbox
Complete JS + CSS + DOM isolation. The remote runs in a sandboxed iframe with restricted permissions. It cannot access the host's cookies, localStorage, or DOM:
const UntrustedWidget = loadRemote({
url: 'https://third-party-vendor.com/widget.js',
isolate: 'iframe',
permissions: ['dom'], // no 'storage', no 'navigation'
});
The iframe auto-resizes to content height via ResizeObserver + postMessage. No fixed-height hacks needed.
When to Use Which
| Scenario | Isolation | Why |
|---|---|---|
| Same team, shared design system | 'none' |
No CSS conflicts, maximum performance |
| Different teams, same company | 'shadow' |
CSS isolation without JS overhead |
| Third-party vendor code | 'iframe' |
Full sandbox, zero trust |
Writing a Remote Widget
A remote is just an ES module that exports a default function. Here's a complete billing widget:
// billing-widget.ts — deployed to https://billing.cdn.com/widget.js
import { createSignal, html, css } from 'onefold';
interface BillingProps {
accountId?: string;
}
const styles = css`
.billing-widget { font-family: -apple-system, sans-serif; }
.billing-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.billing-header h3 { margin: 0; font-size: 16px; }
.badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 12px;
background: rgba(34,197,94,0.1);
color: #16a34a;
font-weight: 600;
}
.plan-card {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 12px;
padding: 20px;
color: white;
margin-bottom: 16px;
}
.plan-name { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
.plan-price { font-size: 14px; opacity: 0.8; }
.usage-bar {
height: 6px;
background: rgba(255,255,255,0.3);
border-radius: 3px;
margin-top: 12px;
overflow: hidden;
}
.usage-fill {
height: 100%;
background: white;
border-radius: 3px;
transition: width 0.3s;
}
.invoices { list-style: none; padding: 0; margin: 0; }
.invoices li {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f3f4f6;
font-size: 13px;
}
.invoices li:last-child { border-bottom: none; }
.amount { font-weight: 600; }
button {
width: 100%;
padding: 10px;
background: #4f46e5;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
margin-top: 12px;
}
button:hover { background: #4338ca; }
`;
export default function BillingWidget(props: BillingProps): Node {
const usage = createSignal(67);
const invoices = [
{ date: 'Jul 2026', amount: '$49.00', status: 'Paid' },
{ date: 'Jun 2026', amount: '$49.00', status: 'Paid' },
{ date: 'May 2026', amount: '$39.00', status: 'Paid' },
];
return html`
<div class=${styles.scope}>
<div class="billing-widget">
<div class="billing-header">
<h3>Billing — ${props.accountId ?? 'Default'}</h3>
<span class="badge">Active</span>
</div>
<div class="plan-card">
<div class="plan-name">Pro Plan</div>
<div class="plan-price">$49/month · Renews Aug 1</div>
<div class="usage-bar">
<div class="usage-fill" style=${() => ({ width: `${usage()}%` })}></div>
</div>
</div>
<h4 style=${{ fontSize: '14px', margin: '0 0 8px' }}>Recent Invoices</h4>
<ul class="invoices">
${invoices.map(inv => html`
<li>
<span>${inv.date}</span>
<span class="amount">${inv.amount}</span>
<span>${inv.status}</span>
</li>
`)}
</ul>
<button onclick=${() => usage.set(Math.min(100, Math.floor(Math.random() * 40) + 60))}>
Simulate API Usage
</button>
</div>
</div>
`;
}
Key points about remotes:
- Imports from
onefoldlike any normal component - Uses
cssscoped styles (won't leak even without Shadow DOM — double protection) - Exports a
defaultfunction that accepts props and returns aNode - Gets bundled into a single JS file and deployed to a CDN
- Has its own reactive state (
createSignal) — fully self-contained
Building a Complete Host Shell
Here's a full host application that loads two remote widgets from different teams:
// host/main.ts
import { createSignal, html, css, mount } from 'onefold';
import { loadRemote, configureSecurity, preloadRemote } from 'onefold/remote';
// Step 1: Lock down which origins can load code into this app
configureSecurity({
trustedOrigins: ['http://localhost:3001'],
timeout: 10000,
});
// Remote URLs (in production: different CDNs per team)
const REMOTES = {
billing: 'http://localhost:3001/billing.js',
analytics: 'http://localhost:3001/analytics.js',
};
// Step 2: Define fallback/error UI
function LoadingFallback(): Node {
return html`<div class="spinner">Loading widget...</div>`;
}
function ErrorFallback(err: Error): Node {
return html`<div class="error">Failed: ${err.message}</div>`;
}
// Step 3: Create remote component loaders
const BillingWidget = loadRemote({
url: REMOTES.billing,
isolate: 'shadow',
fallback: LoadingFallback,
onError: ErrorFallback,
});
const AnalyticsWidget = loadRemote({
url: REMOTES.analytics,
isolate: 'shadow',
fallback: LoadingFallback,
onError: ErrorFallback,
});
// Step 4: Build the shell UI
const shell = css`
.shell { max-width: 1100px; margin: 0 auto; padding: 32px 20px; }
.shell-header { text-align: center; margin-bottom: 32px; }
.shell-header h1 { font-size: 28px; margin-bottom: 8px; }
.shell-header p { color: #6b7280; font-size: 14px; }
.widgets {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.widgets { grid-template-columns: 1fr; }
}
.widget-frame {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
overflow: hidden;
}
.widget-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.widget-toolbar span {
font-size: 12px;
font-weight: 600;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.team-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: #eef2ff;
color: #4f46e5;
}
.widget-content { padding: 20px; }
`;
function App(): Node {
return html`
<div class=${shell.scope}>
<div class="shell">
<div class="shell-header">
<h1>Platform Dashboard</h1>
<p>Host shell loading remote widgets from independent teams</p>
</div>
<div class="widgets">
<div class="widget-frame"
onmouseenter=${() => preloadRemote(REMOTES.billing)}>
<div class="widget-toolbar">
<span>Billing Widget</span>
<span class="team-badge">Team: Payments</span>
</div>
<div class="widget-content">
${BillingWidget({ accountId: 'ACCT-7291' })}
</div>
</div>
<div class="widget-frame"
onmouseenter=${() => preloadRemote(REMOTES.analytics)}>
<div class="widget-toolbar">
<span>Analytics Widget</span>
<span class="team-badge">Team: Data</span>
</div>
<div class="widget-content">
${AnalyticsWidget({ dashboardId: 'main' })}
</div>
</div>
</div>
</div>
</div>
`;
}
mount(App(), document.getElementById('app')!);
Notice preloadRemote() on mouseenter — it prefetches the remote module when the user hovers over the widget frame, so subsequent loads are instant from cache.
Prefetching & Caching
Onefold caches loaded modules by URL + integrity hash. You can manage this cache:
import { preloadRemote, clearRemoteCache } from 'onefold/remote';
// Prefetch on route anticipation or hover
preloadRemote('https://billing.cdn.com/widget.js');
// Clear cache for a specific remote (force re-fetch)
clearRemoteCache('https://billing.cdn.com/widget.js');
// Clear all cached remotes
clearRemoteCache();
Use clearRemoteCache when:
- You detect a compromised remote and need to force a fresh load after updating the SRI hash
- A remote has been updated and you want to bust the cache
- You're running integration tests and need clean state
Scaffolding a Full Project
The create-onefold CLI has a dedicated microfrontend template that sets up the entire architecture:
npm create onefold@latest my-platform --template microfrontend
cd my-platform
npm install
npm run dev
This generates:
my-platform/
├── src/
│ ├── host/
│ │ └── main.ts # Shell app — loads remote widgets
│ ├── remotes/
│ │ ├── billing/
│ │ │ └── index.ts # Self-contained billing widget
│ │ └── analytics/
│ │ └── index.ts # Self-contained analytics widget
│ └── shared/
│ └── types.ts # Shared type contracts
├── index.html
├── style.css # Host global styles
├── build.mjs # Builds host + remotes (supports --target)
├── dev.mjs # Dev: host on :3000, remotes on :3001
├── preview.mjs # Serve production build
└── package.json
The dev server runs two ports simultaneously:
-
:3000— Host shell (with livereload) -
:3001— Remote widget server (CORS enabled)
This simulates cross-origin deployment during development.
Independent Builds & Deployment
Each piece can be built and deployed separately:
npm run build # Build everything → dist/
npm run build:host # Build only the host shell
npm run build:billing # Build only the billing widget
npm run build:analytics # Build only the analytics widget
In production, each team deploys their remote to their own CDN:
# Team: Payments
npm run build:billing
# Deploy dist/billing.js → https://billing.cdn.com/widget.js
# Team: Data
npm run build:analytics
# Deploy dist/analytics.js → https://analytics.cdn.com/widget.js
Then update the host's configuration for production URLs:
configureSecurity({
trustedOrigins: [
'https://billing.cdn.com',
'https://analytics.cdn.com',
],
requireIntegrity: true,
});
const BillingWidget = loadRemote({
url: 'https://billing.cdn.com/widget.js',
integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6...',
isolate: 'shadow',
fallback: LoadingFallback,
onError: ErrorFallback,
});
The host doesn't need to rebuild when a remote updates — as long as the export contract (function signature and props interface) stays the same. Update the integrity hash in the host when remotes publish new versions.
The Remote Contract
Every remote must follow a simple contract:
// The remote exports a default function
export default function WidgetName(props: SomeProps): Node {
// ... build UI using onefold
return html`...`;
}
That's it. The function:
- Receives props from the host
- Returns a DOM
Node - Can use any Onefold API internally (signals, css, effects, etc.)
- Is fully self-contained — no implicit dependencies on the host
You can share type definitions between host and remotes via a shared package:
// shared/types.ts
export type RemoteWidget<P = Record<string, unknown>> = (props: P) => Node;
export interface BillingProps {
accountId: string;
plan?: string;
}
export interface AnalyticsProps {
dashboardId: string;
}
Live Data in Remotes
Remotes are full Onefold components — they can have their own reactive state, effects, and even live-updating data:
// analytics-widget.ts
import { createSignal, html, css } from 'onefold';
export default function AnalyticsWidget(props: { dashboardId?: string }): Node {
const visitors = createSignal(1247);
const pageViews = createSignal(3891);
const chartData = createSignal([35, 52, 41, 67, 45, 78, 62, 55, 71, 48, 83, 59]);
// Simulate live data updates
setInterval(() => {
visitors.set(v => v + Math.floor(Math.random() * 10) - 3);
pageViews.set(v => v + Math.floor(Math.random() * 15));
chartData.set(prev => [...prev.slice(1), Math.floor(Math.random() * 60) + 30]);
}, 2000);
const styles = css`
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
.stat { background: #f9fafb; border-radius: 10px; padding: 14px; text-align: center; }
.stat-value { font-size: 22px; font-weight: 700; }
.stat-label { font-size: 12px; color: #6b7280; }
.chart { display: flex; align-items: flex-end; gap: 3px; height: 60px; }
.bar {
flex: 1;
background: #6366f1;
border-radius: 3px 3px 0 0;
transition: height 0.3s;
min-height: 4px;
}
`;
return html`
<div class=${styles.scope}>
<h3>Analytics${props.dashboardId ? ` — ${props.dashboardId}` : ''}</h3>
<div class="stats">
<div class="stat">
<div class="stat-value">${() => visitors().toLocaleString()}</div>
<div class="stat-label">Visitors</div>
</div>
<div class="stat">
<div class="stat-value">${() => pageViews().toLocaleString()}</div>
<div class="stat-label">Page Views</div>
</div>
</div>
<div class="chart">
${() => chartData().map(v => html`<div class="bar" style=${{ height: `${v}%` }}></div>`)}
</div>
</div>
`;
}
Each remote manages its own state. The host doesn't know or care about the remote's internals — it just passes props and renders the returned Node.
Compared to Module Federation
| Onefold loadRemote | Webpack Module Federation | |
|---|---|---|
| Setup | One import, zero config | Plugin config in both host and remote |
| Bundler dependency | None (works with any bundler) | Webpack 5+ only |
| Shared dependencies | Import Maps (declarative) or bundle per remote | Runtime shared scope config |
| Security | Built-in origin allowlist, SRI, sandboxing | None built-in |
| CSS isolation | Shadow DOM or iframe | Manual (CSS Modules, etc.) |
| Runtime overhead | ES module import()
|
Webpack runtime chunk |
| Hot reload | Built into dev server | Requires additional setup |
| Type safety | Shared type packages | Requires extra tooling |
The trade-off: Module Federation handles shared dependency negotiation at runtime (multiple versions can coexist with fallback logic). Onefold's approach is simpler — use Import Maps for shared deps, or let each remote bundle independently. No runtime negotiation, no version mismatch surprises. You decide at build time.
Shared Dependencies: Marking Onefold as External
Here's a practical concern that comes up quickly: if your host and five remote widgets all bundle their own copy of onefold, you're shipping ~6kb × 6 = 36kb of the same framework code. That's wasteful, and it also means each remote has its own signal system — signals can't cross boundaries between host and remotes.
The fix is straightforward. Mark onefold as external in your remote build config, and let the browser resolve it from a single shared source via an Import Map.
Step 1: Build the remote with onefold as external
In your remote's esbuild config (or whichever bundler you use), tell it not to bundle onefold:
// build-remote.mjs
import { build } from 'esbuild';
await build({
entryPoints: ['src/remotes/billing/index.ts'],
bundle: true,
format: 'esm',
external: ['onefold'], // Don't bundle — resolved at runtime
outfile: 'dist/billing.js',
minify: true,
});
The output billing.js will still have import { html, createSignal } from 'onefold' — but as a bare import that the browser resolves at runtime, not bundled code.
Step 2: Add an Import Map to the host HTML
In your host's index.html, declare where the browser should find onefold:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script type="importmap">
{
"imports": {
"onefold": "https://cdn.example.com/onefold@0.1.5/index.js"
}
}
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="./host.js"></script>
</body>
</html>
Now every remote that does import { ... } from 'onefold' gets the same instance from the CDN. One download, one signal system, shared across host and all remotes.
Step 3: The remote code stays clean
Your remote widget doesn't change at all — it uses normal imports:
// billing-widget.ts — bare import, resolved by the host's import map
import { html, createSignal, css } from 'onefold';
export default function BillingWidget(props: { accountId: string }): Node {
const usage = createSignal(67);
// ... rest of the component
}
When this runs in the browser, 'onefold' resolves to the URL declared in the import map. The remote never bundles the framework, and the browser only downloads it once.
When to share vs. when to bundle
| Scenario | Approach | Why |
|---|---|---|
| All remotes are internal, same team/org | Share via Import Map | Single download, signals work across boundaries |
| Third-party vendor remote | Let them bundle their own copy | No version coordination needed, full isolation |
| Local development | Bundle everything (no external) | Simpler dev setup, no CDN dependency |
| Production with few remotes (1–2) | Either works | The duplication is small (~6kb per remote) |
| Production with many remotes (5+) | Share via Import Map | Savings add up, consistent behavior |
Versioning strategy
Pin to a specific version in the import map:
{
"imports": {
"onefold": "https://cdn.example.com/onefold@0.1.5/index.js"
}
}
- Patch updates — safe to update the import map URL without redeploying remotes
- Minor updates — generally safe, but test remotes against the new version first
- Major updates — coordinate with all teams; update remotes before changing the map
The key advantage of this approach over Module Federation's shared scope: there's no runtime negotiation, no "singleton" vs "eager" vs "strict" semantics to configure, and no version mismatch errors at runtime. The import map is declarative — what you see is what the browser loads.
A note on browser support
Import Maps work in all modern browsers (Chrome 89+, Firefox 108+, Safari 16.4+). If you need to support older browsers, the es-module-shims polyfill handles it with minimal overhead.
Production Checklist
Before going to production with Onefold microfrontends:
- [ ]
configureSecurity({ requireIntegrity: true })— always verify remote code - [ ] Generate SRI hashes for every remote build (
sha384-...) - [ ] Set
isolate: 'shadow'for cross-team widgets - [ ] Set
isolate: 'iframe'for third-party vendor code - [ ] Provide
onErrorfallbacks for every remote (graceful degradation) - [ ] Set up CORS headers on remote servers (
Access-Control-Allow-Origin) - [ ] Use
credentials: 'omit'(Onefold does this by default — cookies are never sent to remote origins) - [ ] Consider Import Maps to share
onefoldacross remotes (eliminates duplication, enables cross-boundary signals) - [ ] Mark
onefoldasexternalin remote build configs when using shared deps - [ ] Pin shared dependency versions in the import map
- [ ] Test with
blockAll: trueto verify your app degrades gracefully when remotes are disabled
Wrapping Up
Onefold's microfrontend system gives you the hard parts for free:
- Security — origin allowlisting, SRI verification, credential isolation
- Isolation — Shadow DOM for CSS, iframe for full sandboxing
- Loading — async with fallbacks, error handling, prefetching, caching
- DX — CLI template, dual-port dev server, independent builds
The entire API surface is three functions: configureSecurity, loadRemote, and preloadRemote (plus clearRemoteCache for cache management). That's it.
npm create onefold@latest my-platform --template microfrontend
Links:
Onefold is MIT licensed and open source (v0.1.6). The microfrontend module is part of the core package — no extra installation needed.
Top comments (1)
Microfrontends often add more complexity than they remove, so the interesting part is seeing where Onefold draws that line. ~