Choose on their constraints, not your preference
The delivery mechanism is picked by the host’s build system, CSP header and framework version, not by what you would enjoy writing — and a single CSP directive can delete half the options before the conversation starts.
You have a seam map and a constraint sheet. Now somebody asks which way you want to build it, and the honest answer is that you do not get to want. Six mechanisms are theoretically available — a custom element bundle, Module Federation, single-spa, an iframe with postMessage, a plain script tag writing into a div, and a native rewrite in the host’s own framework — and in any given host, most of them are already dead. The job is to find out which ones, in the right order, before you have formed a preference you will then defend.
The full scoring lives on the delivery-modes reference page, linked in this course’s header, and is meant to be kept open. This lesson is about the order you apply it in, because applying it in the wrong order is how a team spends three weeks on an approach a response header had already ruled out.
Eliminate before you evaluate
Scoring six options across seven axes is a spreadsheet exercise that feels like rigour and mostly produces the answer you already liked. Elimination is faster and harder to fool. Four filters, in this order, because each is cheaper to check than the next:
- Response headers. Ten minutes, no repository access needed.
- The build system and who owns it. One look at the config and one question about approvals.
- The browser floor. Whatever they target is now your floor.
- The multiplier. How many hosts does this have to work in? This one changes the answer more than the other three combined, and it is the one nobody applies.
Filter one: what the CSP has already decided
Three directives do almost all of the eliminating.
frame-ancestors decides whether the iframe option exists. MDN defines it as specifying “valid parents that may embed a page using <frame>, <iframe>, <object>, or <embed>”, and it is worth being careful here because it points the opposite way from the directive people confuse it with. MDN spells the difference out: frame-ancestors says who may embed this page; frame-src says where iframes in this page may load from. If your widget is the iframe, the header that matters is the one your own origin serves, and the host must be listed in it. One more detail worth knowing before you propose a fix: the same MDN page notes the directive “is not supported in the <meta> element,” so it can only be delivered as a real response header.
script-src decides whether the script-tag and custom-element options exist in the form you were imagining. Your origin has to be allowed, and if the bootstrap is inline it needs 'unsafe-inline', a matching hash, or a nonce you can only obtain at request time from their server. There is one rule here that catches experienced people, and MDN states it twice: “If a directive contains nonce or hash expressions, then the unsafe-inline keyword is ignored by browsers.” So a host whose policy lists both is not permissive — the moment they added a nonce, 'unsafe-inline' stopped meaning anything, and an integration that worked last year against the same visible policy string will fail today. 'strict-dynamic' goes further still: MDN notes that with it, “any allowlist or source expressions such as 'self' or 'unsafe-inline' will be ignored,” and trust flows only from a script the host themselves nonced. In that host, a third-party script tag cannot load itself; the host must load you.
connect-src decides whether your component can talk to your own backend once it is running. MDN: it “restricts the URLs which can be loaded using script interfaces” — fetch(), XMLHttpRequest, WebSocket, EventSource, sendBeacon(). The governing policy is the embedding page’s, not yours. An agentic surface streaming from your API into their page is exactly the shape this blocks, and it is the most common reason a demo that worked locally does nothing in production.
Where people get burned
Read the headers off the deployed production host, not a local dev server and not staging. CSP is routinely relaxed in development and frequently added by a CDN or WAF the application repository has no knowledge of. A policy that does not appear anywhere in the codebase can still be the thing that kills your approach, and you will not find it by reading their code.
Filter two: whose build is it
Module Federation and single-spa both require the host to change how it builds and boots. That is not a criticism of either — it is what they are for. Module Federation is, in its own documentation, a way to “share code and resources among multiple JavaScript applications (or micro-frontends)”, with dependency sharing as a first-class feature; single-spa describes itself as a framework for using “multiple frameworks on the same page without page refreshing”, coordinated by a root config that registers applications and decides when each is active. Both are projects documenting their own products, and both are describing an architecture the host adopts — not a thing you can bring with you.
The question is therefore organisational before it is technical. If the host has already adopted one of these, you are in luck: the seam exists, it is documented, and using it is the low-friction path. If they have not, proposing it means asking a team you just met to change their build system so that your one surface can be delivered. That conversation has a predictable outcome, and the seam map you built already recorded it in the approver column.
A plain script tag or a custom-element bundle asks for none of this. That is their entire competitive advantage, and it is a large one.
Filter three: the browser floor is theirs, not yours
Web Components look like a safe default until you check the specific pieces you actually need, several of which are newer than the platform as a whole. Two examples that decide real designs, with dates:
- Form-associated custom elements. If the review gate has to submit inside the host’s existing form, you need
ElementInternals. MDN records it as Baseline widely available since March 2023, with an asterisk noting that parts of the feature have varying support — the asterisk is real, since Firefox shippedElementInternalsin 93 butsetFormValue()only in 98. Safari shipped the whole thing in 16.4 (checked 2026-09-03). adoptedStyleSheets, which is how you share one parsed stylesheet across many instances instead of duplicating a<style>per element. MDN: Baseline widely available since March 2023 (checked 2026-09-03).
March 2023 is recent enough that a host with an enterprise browser floor may sit below it. And one option is simply gone regardless of floor: customized built-in elements, the <button is="my-thing"> form. MDN carries a flat note: “Safari does not plan to support customized built-in elements.” WebKit’s formal standards position, filed November 2022, is “oppose”. Design around autonomous elements only.
Filter four: the multiplier, which is the whole job
For a single host, the native rewrite wins. Write the review gate in their Angular, with their components, their tokens, their test suite. It inherits their styles because it is their styles. It inherits their session because it is their app. It has no bundle overhead, no boundary, no CSP question, no accessibility gap across a shadow root. On six of seven axes it is the best answer available, and pretending otherwise to justify a more interesting approach is the most common form of engineering dishonesty in this space.
It loses on exactly one axis, and the axis is the job. A native rewrite does not amortise. Ninety hosts means ninety implementations in however many frameworks, ninety test suites, and ninety places a bug fix has to be applied by a team that does not report to you. The mechanism that is second-best in one host and identical in all ninety beats the mechanism that is best in one and absent in the rest.
Retrieval check
A host serves `script-src ’self’ ’nonce-r4nd0m’ ’unsafe-inline’` and their platform lead says “we allow inline scripts, you’re fine.” What do you tell them?
Check your answer
That the policy does not say what they think it says. MDN states that when a directive contains a nonce or hash expression, browsers ignore the 'unsafe-inline' keyword entirely. The nonce is present, so 'unsafe-inline' is dead text — it survives in the header only to keep CSP1-era browsers working. Any current browser will refuse an inline script that does not carry the correct per-request nonce.
The practical consequence: your bundle cannot bootstrap itself inline, and it cannot obtain the nonce, because a nonce has to be regenerated per request by their server. The options that remain are an external script from an origin listed in 'self' or added to the allowlist, or having the host render your script tag with their nonce applied. Both are small asks. Discovering the need for them in a review, after proposing an inline bootstrap, is not a small look.
Hands on
Kill four mechanisms on paper before writing any code
Done when: ARTIFACT.md carries a delivery decision for one real host naming one chosen mechanism, at least three eliminated ones, and for each elimination the specific host fact — a header value, a config line, a browser version — that killed it. No elimination may be justified by preference.
- Fetch the production headers for your host and paste the raw
Content-Security-Policyvalue intolearning/grafting-ui/ARTIFACT.mdverbatim. If there is no CSP at all, write that down too — it is a finding, and it means filter one eliminates nothing. - Work the three directives in turn. Does
frame-ancestorson your own serving origin permit this host? Doesscript-srccontain a nonce or'strict-dynamic', and if so what does that do to a self-loading script tag? Doesconnect-srcpermit your API origin? Record a verdict per mechanism, not a general impression. - Open their build config and their lockfile. Is Module Federation or single-spa already present? If not, cross both off and write the approver’s name from your seam map next to them as the reason.
- Find their stated browser support — a browserslist entry, a support policy page, an analytics floor. Check it against March 2023, the Baseline date for both
ElementInternalsandadoptedStyleSheets. Write down whether you are above or below, and the date you checked. - Write the multiplier as a number, then make the call. One sentence: the mechanism, the strongest alternative you rejected, and the single host fact that decided between them. If that sentence contains the word “prefer,” delete it and find the fact.
- Bring the decision into the chat. I will argue for the mechanism you rejected, and the only thing that will move me is a header value or a version number.
What this does not cover
A decision is not permission. Choosing a mechanism tells you what to build; it does nothing about the fact that a team you have just met now has to accept a foreign object into a product they are accountable for. The graft-contract lesson covers what you put in writing to make that a short conversation — what you promise about your tag name, your events, your bundle size and your breaking changes, and the four things you have to ask for in return.
The mechanisms themselves are taken apart in the modules that follow: the shadow boundary and everything it costs, then Module Federation’s singleton problem, single-spa’s mount lifecycle, and the iframe with the origin check you have to write correctly. This lesson only taught the order to eliminate them in.
Read this next — primary source
Content Security Policy (CSP)MDN Web Docs — free
This lesson uses CSP as an elimination filter and takes only the three directives that do the eliminating. The full guide is worth reading because the parts it covers beyond those three are the parts a host team will raise in review: report-only mode and the reporting endpoint, which is how you negotiate a policy change without breaking their site; the difference between a policy delivered by header and by meta element; and how policies combine when more than one is present, which is what happens the moment a CDN or a WAF is in front of the app. Knowing the mechanism well enough to propose a specific, narrow policy amendment is the difference between being told no and being told what to send.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.