You don’t get a second chance at a plugin ecosystem. The first time an extension reads the wrong tenant’s data or keys your localStorage, you’ve bought yourself a lost quarter, a PR cycle, and a trust deficit you’ll be paying back for years. A recent dev post, “Web Components vs. Iframes: A Hard Lesson in DOM Isolation Barriers,” made the rounds because it exposed a quiet truth: Shadow DOM is not a security boundary. If you’re building a marketplace or “extensions” surface in 2026, treat UI composition and security isolation as separate decisions—because the browser does.
The problem you’re actually solving
“Let third parties extend our app” sounds singular, but it’s four different problems with incompatible solutions:
- Untrusted code execution: Can a plugin run arbitrary JS without exfiltrating data or breaking the host?
- DOM integration: Can a plugin draw inside your UI without your design system imploding?
- Data and capability scoping: Can a plugin do only what the user and tenant authorize—nothing more?
- Performance isolation: Can you prevent one bad plugin from tanking INP, TTI, or memory for everyone?
Mix these up and you’ll pick the wrong primitive. That’s how teams end up with Web Components doing security work they can’t, or iframes bloating a page that didn’t need them.
The short answer
- If you will ever allow third-party developers you don’t directly employ, use cross-origin iframes for execution isolation. Full stop.
- Use Web Components (Shadow DOM) for first-party or vetted-partner UI building blocks where you need heavy DOM integration—not for sandboxing.
- Push heavy or long-running plugin logic into Web Workers and talk over message channels. Workers aren’t a security boundary, but they keep the main thread healthy.
- Wrap everything in capability-scoped host APIs. Never pass raw window, document, or storage handles to plugin code.
That’s the spine. The details below make it production-grade.
Why Web Components are not your sandbox
Shadow DOM and custom elements solve CSS and event leakage. They don’t solve cross-tenant data protection. A malicious or simply buggy plugin in a Web Component can:
- Read any JS-accessible state you hand it (props, context, tokens).
- Call fetch with your ambient credentials if you give it same-origin access to your APIs.
- Exfiltrate via any open host callback it receives.
Yes, you can tighten things with Trusted Types, strict CSP, and rigorous type boundaries. But if the developer is outside your org—and your threat model includes “plugin tries to do more than it should”—Web Components are for composition, not containment.
Why cross-origin iframes are the boring, correct default
Iframes are cheap security. The browser has spent two decades hardening them. The isolation primitives you can rely on in 2026:
- Origin boundary: Different origin, different ambient credentials. Third-party cookies are being deprecated; that’s a feature here.
- Sandbox attribute: Remove
allow-same-originand the iframe becomes an opaque origin. Grant only what you need:allow-scripts,allow-forms, maybeallow-popups-to-escape-sandboxfor OAuth. Disable microphone, camera, USB, payments by default. - Permissions Policy: Deny powerful APIs per-iframe. No geolocation. No window-management. No idle-detector. Default-deny everything.
- PostMessage-based RPC: MessageChannel isolates communications to explicit ports. Libraries like Comlink make it ergonomic without leaking references.
Trade-offs are real. Each iframe costs memory (single-digit MBs is common), layout complexity, and a thinner integration layer. But when a plugin comes from “somebody on the internet,” this is the difference between an incident report and a blog brag.
A pragmatic decision framework
1) Who writes the code and who you’ll blame when it goes wrong
- Only first-party teams: Use Web Components for ergonomics and Shadow DOM for style isolation. Still gate access with narrow host APIs. No ambient global state.
- Vetted strategic partners under contract: Start with iframes for isolation, then selectively expose “DOM-like” surfaces via host APIs. If you must use Web Components, wrap partner code in a Worker and mediate all host calls over a MessageChannel.
- Open marketplace: Cross-origin iframes with sandbox and Permissions Policy. No exceptions.
2) What the plugin needs to do
- Read-only UI widgets (tables, charts, panels): Web Components are fine for first-party. For third-party, render inside an iframe; pass data snapshots, not handles.
- Mutating workflows (create/update/delete in your app): Always capability-scope via signed action tokens minted server-side per tenant-resource-operation with 5–15 minute TTLs. No bearer tokens with global authority.
- Heavy compute (parsing, AI transforms): Offload to a Worker within the iframe origin or to your server-side extension runtime. Prevent long tasks on main thread: budget 50 ms max per interaction; throttle or yield with
scheduler.postTask({priority: "background"}). - Network access: Prefer a host-proxied fetch API that enforces CORS, logs requests, strips cookies, and applies rate limits. Do not grant raw network to untrusted plugins.
3) How deep the visual integration must be
- “Looks part of the app”: For iframes, ship a design-token bridge: expose CSS variables and a light/dark palette via a handshake message when the iframe loads. Don’t share your entire design system runtime; send resolved tokens.
- Events, focus, ARIA: For accessibility, proxy top-level host events (resize, theme-change, locale-change) over a dedicated MessagePort. Inside the iframe, the plugin owns ARIA semantics. You cannot guarantee cross-frame a11y as tightly as in shadow trees; set that expectation.
- Drag/drop, selection, overlay portals: These are pain points. Provide host-managed primitives: host-owned overlay portal API; host-owned drag/drop coordination that sends intents to plugins rather than giving plugins global listeners.
Security controls you should wire in on day one
- CSP on host and plugin origins: Host:
script-src 'self'+ hashes for your scripts, no'unsafe-inline'. Plugin origin: separate CSP with no remote code execution beyond the bundle. For Web Components loads, pin with SRI and exact versions. - Permissions Policy baseline: Deny everything by default:
geolocation=(),camera=(),microphone=(),payment=(),usb=(),battery=(),clipboard-write=()unless a concrete product need exists. - Iframe sandbox contract:
sandbox="allow-scripts allow-forms"at minimum; avoidallow-same-origin. Addallow-popupsandallow-popups-to-escape-sandboxonly for OAuth flows; terminate message channels after auth completes. - Capability tokens, not session cookies: Mint narrow JWTs: {tenant, resource, operation, plugin_id, exp ≤ 15m}. Bind them to the iframe via message-port initialization; never expose global session tokens to plugin JS.
- Rate limiting and egress logging per plugin_id: Every host-bridged call is metered and tagged with tenant and plugin identifiers. You’ll need this when Finance asks why one plugin exploded egress by 3 TB.
- Trusted Types + DOMPurify (strict): For any HTML rendering the host does on behalf of a plugin, require TrustedHTML. Treat cross-frame HTML as hostile until sanitized.
Performance and UX budgets you can enforce
- Memory: Expect 3–8 MB baseline per iframe in modern Chromium. On a dashboard with 6 plugins, you’ve just spent ~30–50 MB. Make it a hard cap: maximum 4 concurrent third-party iframes visible; lazy-load the rest.
- Interaction latency (INP): Budget ≤ 200 ms p95. Any plugin generating long tasks > 50 ms more than 5 times in a minute gets throttled via your host scheduler and flagged in the marketplace.
- Network: Cap plugin outbound requests at 10 RPS per tenant by default with burst ≤ 50. Anything AI-related should move to your server-side extension runtime to keep token spend and latency predictable.
- Cold start: Iframes are slower to boot. Pre-init hidden iframes with
loading="lazy"and an explicit warmup handshake during idle windows. Expect 80–200 ms extra TTI vs in-page code, depending on bundle size.
Developer experience you owe your ecosystem
Security without DX dies on contact with reality. Give plugin developers a contract that’s easy to follow and hard to misuse:
- Manifest + capabilities: A JSON manifest declaring routes, UI mount points, and requested capabilities. Reject at registration time if it asks for more than allowed for its category.
- Typed host API: Ship a TypeScript SDK that wraps postMessage with Comlink, enforces capability scopes at compile-time, and provides mocks for local dev.
- Design tokens, not your CSS: Export tokens for color, spacing, and typography via a stable JSON schema; plugins map their own components. Don’t couple your release cadence to third-parties’ CSS.
- Local dev container: A CLI that spins a localhost iframe host with the same sandbox and policies you run in prod. No “it works locally” drift.
- Observability: Provide a plugin console: logs, network calls, rate-limit hits, capability denials. Expose p95 init time and INP to developers so they can tune.
Marketplace policy that actually shifts liability
If you’re going to let outside code run against your customers’ data, operate like a platform:
- Security review tiers: Bronze (automated checks only), Silver (human review + code scan + penetration test), Gold (annual re-cert, SOC 2 report). Price your revenue share accordingly.
- Key escrow for takedowns: Every plugin release must be addressable: signed bundle with reproducible build metadata. If you need to revoke, you can. Use Subresource Integrity for script delivery when feasible.
- Incident playbook: One-click disable per-tenant and global kill switch. Require plugins to implement a “safe mode” route for data export and uninstall.
- Clear data footprint: Publicly document what a plugin can do and which capabilities it uses. Make it searchable so buyers can filter by risk tolerance.
Server-side extensions: don’t force everything into the browser
A lot of “we need deep DOM access” is self-inflicted. Split the world:
- Client-side visual surfaces: Use iframes or Web Components per the rules above.
- Server-side actions and automations: Run in your extension runtime (Node, Deno, or WASM) with strict tenancy boundaries, audited egress, and signed requests back to your core APIs. The browser UI just kicks off intents.
Doing so slashes the pressure to over-permission the browser plugin just to reach data or GPUs it should never touch.
What’s new in 2026 you should care about
- Third-party cookies deprecation: Works in your favor. Cross-origin iframes won’t inherit your session. Design for token-based capabilities from day one.
- Fenced Frames: Great for ad-like, untrusted content with privacy guarantees, but still too constrained for most app plugins. Track support; don’t bet the platform on it yet.
- Base UI, shadcn/ui churn: Recent component-library migrations remind you: design systems change; security boundaries should not. Keep security orthogonal to your UI stack.
A reference architecture you can copy
- Registry service: Stores plugin manifests, signed bundle metadata, and capabilities. Enforces category policies at publish time.
- Host shell (app): Renders plugin mount points. For untrusted plugins, injects cross-origin iframes with
sandboxandallowattributes per capability. Initializes aMessageChannelper plugin. - Host SDK: Type-safe wrapper around postMessage/Comlink. Exposes APIs: data.read(resource), actions.execute(actionToken), ui.openOverlay(), storage.session.get/set (scoped to plugin_id+tenant).
- Policy gateway: Server-side service that mints short-lived capability tokens and proxies plugin network egress with logging and rate limits. Binds tokens to tenant+plugin_id+operation.
- Server-side extension runtime: Runs heavy or sensitive steps under your control. Communicates with core services via mTLS and signed requests. Exposes webhooks for the plugin iframe to receive updates.
- Observability plane: Per-plugin dashboards: init time, INP, error rate, egress, capability denials. Automatic marketplace flags for outliers.
- Kill switch and feature flags: Centralized toggles to revoke a version or capability instantly across tenants.
Common traps (and the fix)
- Trap: Sharing your Redux store or React context with a Web Component plugin. Fix: Expose read-only snapshots via structured clone; all writes go through capability-gated host actions.
- Trap: Adding
allow-same-originto the iframe because a dev needed cookies. Fix: Don’t. Proxy auth via the host; if you must, isolate that flow in a dedicated auth iframe with a quarantined channel and destroy it after use. - Trap: Letting plugins register global event listeners. Fix: Provide scoped, host-owned event APIs and throttle them.
- Trap: Version drift of your design system breaks plugins. Fix: Version your design tokens and support at least two concurrent majors. The plugin contract is tokens, not class names.
Why this matters for AI-heavy apps
Agent integrations magnify risk. A plugin that can see everything will eventually prompt-inject its way into doing everything. Keep model contexts narrow: when a plugin asks for AI help, the host assembles context slices, redacts secrets, and signs the request for the model. No plugin should ever hold raw API keys or tenant-global embeddings. Expect regulators to care—soon.
What we do for clients at DHD Tech
We’ve shipped plugin platforms for US SaaS teams where the delta between “nice app” and “defensible platform” is weeks, not quarters. Our default stack is:
- Cross-origin iframes for third-party code with sandbox and strict Permissions Policy.
- Typed host APIs over Comlink, with per-tenant, per-capability JWTs expiring in 5–15 minutes.
- Design-token bridges for visual cohesion without CSS entanglement.
- Server-side extension runtimes for heavy/regulated workloads, with budgeted egress and token accounting.
- Automated conformance tests that spin up 50+ synthetic plugins in CI to validate isolation, rate limits, and performance budgets under load.
This is boring on purpose. The alternative is waking up to a breach thread claiming someone’s “private videos” leaked because of your cross-tenant bug. Don’t be that post.
Key Takeaways
- Use cross-origin iframes for any code you don’t control; Web Components are for composition, not containment.
- Expose narrow, capability-scoped host APIs; never pass raw DOM, storage, or global tokens to plugins.
- Enforce budgets: memory per iframe, p95 INP, network RPS, and kill switches for bad actors.
- Design tokens bridge visual integration; do not share your design system runtime.
- Split client UI from server-side extensions; keep sensitive and heavy work off the browser.
- Invest in a manifest, TypeScript SDK, local host, and observability to make the safe path the easy path.