Hosting & headers

SharedArrayBuffer is only exposed when window.crossOriginIsolated === true. That flag — not CORS — is the gate, and your host must opt the document into it on every response.

This page applies only when your pool uses sharedMemory. A message-only connectWorker/WorkerPool (no sharedMemory contract) needs none of these headers — SharedArrayBuffer is never touched.

Required response headers

Cross-Origin-Opener-Policy:   same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them the SDK can't create the shared buffer: crossOriginIsolated is false and SharedArrayBuffer is undefined.

Per-server configuration

vite.config.ts (dev + preview)
export default defineConfig({
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
      'Cross-Origin-Resource-Policy': 'same-site',
    },
  },
});
angular.json (ng serve)
"serve": {
  "builder": "@angular/build:dev-server",
  "options": {
    "headers": {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
      "Cross-Origin-Resource-Policy": "same-site"
    }
  }
}
next.config.ts (dev + next start)
async headers() {
  return [{
    source: '/:path*',
    headers: [
      { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
      { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
      { key: 'Cross-Origin-Resource-Policy', value: 'same-site' },
    ],
  }];
}
nginx
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-site" always;

Embedding your app in an iframe

Cross-origin embedding requires four independent pieces — if any one is missing, Chrome blocks the navigation (ERR_BLOCKED_BY_RESPONSE) or the child loads without isolation:

  1. The parent page is itself cross-origin isolated.
  2. The iframe element delegates isolation: allow="cross-origin-isolated". Without it, a fully-configured child is still blocked.
  3. The child sends COEP + COOP — a cross-origin document embedded under a require-corp parent must be capable of isolation itself.
  4. The child sends CORP — same-site suffices when embedder and app share a site (e.g. different localhost ports); cross-origin for arbitrary embedders.
<iframe src="https://app.example.com" allow="cross-origin-isolated"></iframe>

Measured requirement matrix

COEP parent embedding cross-origin children; Chrome's block reason for each child header set:

Child headersResultblockedReason
COEP + COOP + CORP same-siteloads, isolated, SharedArrayBuffer live in-frame—
COEP + COOP, no CORPblockedcorp-not-same-origin-after-defaulted-to-same-origin-by-coep
CORP same-site, no COEPblockedcoep-frame-resource-needs-coep-header
no headersblockedcorp-not-same-origin-…

Separately, frame-ancestors CSP controls who may embed your app — an independent axis from CORP:

Content-Security-Policy: frame-ancestors 'self' https://docs.example.com

Static hosts that can't set headers (GitHub Pages)

When the server can't send headers at all, a service worker can inject them — it intercepts every response in its scope and rewrites headers before the document parses. This repo's Pages deploy ships coi-sw.js (same idea as coi-serviceworker): each entry page registers ./coi-sw.js inline and reloads once after first install — a document is only isolated if it was fetched while controlled.

  • The SW uses COEP: credentialless for the islands demo (its map island loads no-cors tile images that require-corp would block) and require-corp elsewhere. Browsers without credentialless support just stay non-isolated.
  • Degrade, don't break: islands can run fully without SAB — mode: 'poll' on mountIsland/<Island> (or doorbell: false on connectIslandWorker) skips the doorbell contract so the pool never touches SharedArrayBuffer. Check window.crossOriginIsolated and pick the mode.
  • Workloads that are shared memory can't degrade — show a notice instead of failing on worker bootstrap.

Gotchas

  • localhost ≠ 127.0.0.1. Chrome treats them as different sites: CORP: same-site between localhost ports passes, the same layout on 127.0.0.1 fails. Match hostnames between embedder and child, or use CORP: cross-origin.
  • Avoid credentialless iframes — a credentialless frame can never be cross-origin isolated, so SharedArrayBuffer is undefined inside it.
  • Dev servers don't hot-reload header config — restart after editing angular.json, next.config.ts, etc.
  • Serve index.html with Cache-Control: no-store (or stamp its URL with a per-build ?v=) — assets are content-hashed, but the stable document URL is exactly what stale caches hold onto.