DEV Community

Cover image for Microfrontends Without the Complexity: How Onefold Handles Multi-Team Frontend Architecture
Md. Zahirul Haque
Md. Zahirul Haque

Posted on

Microfrontends Without the Complexity: How Onefold Handles Multi-Team Frontend Architecture

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';
Enter fullscreen mode Exit fullscreen mode

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 .card class stomps Team B's .card class
  • 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)
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Origin Allowlist — Only modules from trustedOrigins can load. All other origins are rejected before any network request.
  2. SRI Integrity — When requireIntegrity is true, fetched content is verified against a SHA hash before execution. A compromised CDN can't serve malicious code.
  3. Timeout — Modules taking longer than the timeout are aborted. Prevents slow-loris attacks.
  4. Isolation — Shadow DOM or iframe sandboxing prevents DOM/CSS leaks between host and remote.
  5. CSP Compatible — No eval(), no Function(), no inline scripts. Works with strict Content-Security-Policy headers.
  6. Credential Isolation — Remote fetches use credentials: 'omit' — cookies are never sent to remote origins.
  7. Kill Switch — Set blockAll: true to 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>
  `;
}
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

'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',
});
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Key points about remotes:

  • Imports from onefold like any normal component
  • Uses css scoped styles (won't leak even without Shadow DOM — double protection)
  • Exports a default function that accepts props and returns a Node
  • 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')!);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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`...`;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  `;
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 onError fallbacks 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 onefold across remotes (eliminates duplication, enables cross-boundary signals)
  • [ ] Mark onefold as external in remote build configs when using shared deps
  • [ ] Pin shared dependency versions in the import map
  • [ ] Test with blockAll: true to 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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
morphoices profile image
MORPHOICΞS.

Microfrontends often add more complexity than they remove, so the interesting part is seeing where Onefold draws that line. ~