Security model
use-everywhere has two very different security postures, matching its two worlds. Knowing which one you’re in tells you what the library checks for you and what remains your job — so let’s take them in turn, same-origin first because it’s the short one.
Same origin: everyone is you
Section titled “Same origin: everyone is you”Everything on one origin — every tab, iframe from your domain, worker — runs your code with your cookies. BroadcastChannel is already scoped to the origin by the browser, so the same-origin engines add no authentication at all, on purpose:
- Any code on your origin can join any bus, read state, and write patches.
- A compromised dependency on your page can too — but it could equally read
localStorageor your DOM. The bus does not widen that blast radius.
The one same-origin knob is filtering, not security: scope: 'tabs' (or a
custom accept predicate on createSharedStore) lets a store ignore writes
from workers. That’s a coordination tool — a worker is still your code.
Cross origin: the other page is nobody
Section titled “Cross origin: the other page is nobody”The window channel assumes the opposite: the page on the other side is a
different security principal, and postMessage is a public mailbox — any
window that holds a reference to yours can post to it, and by default a
listener cannot tell who sent what. This is the API where real money gets
lost, so the paranoia is built in rather than left as an exercise.
Every message a channel receives must pass four gates before your handler ever runs:
| Gate | Check | Stops |
|---|---|---|
| 1. Origin | event.origin === peerOrigin (exact match, no wildcards) | Any site you did not name |
| 2. Envelope | payload carries the library brand (__ue: 1) | Unrelated postMessage traffic on the same page |
| 3. Nonce | cid equals the id minted for this openWindow() call, delivered via the ?ue-cid= URL param | Stale windows, replays from earlier sessions, guessed messages |
| 4. Source | event.source is the expected peer — the exact Window we opened, or the opener | A third frame on the correct origin impersonating either side |
Fail any gate, and the message is dropped silently — your handlers never run. Note what gate 4 buys you: even a hostile iframe on the correct origin fails, because it isn’t the exact window object you opened. Both sides run this check — the child validates that a message really came from its opener, not merely from something at the opener’s origin.
The nonce is 128 bits from crypto.getRandomValues. That matters because the
cid is what gate 3 rests on: a predictable nonce is a guessable one.
getRandomValues, deliberately, and not crypto.randomUUID — randomUUID is
restricted to secure contexts and simply does not exist on a plain-http://
origin, which is exactly where an internal app would silently fall back to
something weaker.
One thing to know before shipping a popup flow: Cross-Origin-Opener-Policy
severs the connection. If either page sends COOP: same-origin, the browser
detaches window.opener, and the child cannot reach back — connectToOpener
throws, and the opener’s closed detection stops working. Payment providers
commonly set it. If the child page is yours, same-origin-allow-popups on the
opener keeps the channel intact.
Outbound is symmetric: every postMessage is sent with an explicit
targetOrigin, so the browser itself refuses delivery if the window has
meanwhile navigated somewhere unexpected.
peerOrigin is required, '*' throws
Section titled “peerOrigin is required, '*' throws”openWindow(url, { peerOrigin: 'https://pay.example.com' }); // ✅openWindow(url, { peerOrigin: '*' }); // ❌ throwsThe API is shaped so the insecure thing is not expressible. A wildcard would
disable gate 1 and the outbound targetOrigin check at once — that’s the
textbook postMessage vulnerability, and it doesn’t compile here. For local
prototyping there is an explicit escape hatch — allowAnyOrigin: true —
named so it cannot be mistaken for production configuration. openWindow
also cross-checks that the URL you open actually belongs to peerOrigin,
catching copy-paste mismatches at call time.
Why shared state stops at the origin line
Section titled “Why shared state stops at the origin line”It’s tempting to want useSharedState to just work across the payment
window too. The library refuses by design, and the refusal is worth
spelling out:
- Confused deputy. State merging is automatic — whatever arrives with a newer clock wins, no questions asked. Automatic writes from a foreign principal into your application state is precisely how a compromised or buggy partner page would corrupt yours. Messages, by contrast, only do what your explicit, typed handler does. Across trust boundaries, explicit beats magic.
- Auditability. A typed message map (
{ 'payment-complete': Receipt }) is a reviewable contract between two domains. “Any key, any value, newest clock wins” is not. - The shapes differ anyway. Cross-origin flows are request/result shaped — open a window, exchange a handful of messages, get one result. A convergent replicated object buys nothing there.
What the library does not protect against
Section titled “What the library does not protect against”Honesty about the boundary, because trusting a tool means knowing its edges:
- Your own origin. XSS on your page owns every bus. The library neither helps nor hurts.
- The peer’s content. Gate 1 verifies where a message came from, not that the payment page is honest. Trust in the counterparty is a business decision; validate the payloads you receive.
- Client-side truth. The demo’s payment lock stops accidental double submission. A hostile user can bypass any client-side state — money always needs server-side idempotency keys.
Where to next
Section titled “Where to next”- Cross-origin payments — the flow these gates protect, end to end.
useWindowResult— the hook-level view of the same machinery.- How sync works — the cross-origin handshake in sequence-diagram form.