Security & Origins
The marketplace embeds untrusted, publisher-authored games. The boundary between your game and the shell is a hard security boundary, and the SDK is built to respect it. This page explains the trust model so you know exactly what crosses the line — and what never does.
The origin model #
Your game is served from its own origin, https://g-{gameId}.carverjs.dev, inside a sandboxed <iframe>. Because every game gets a distinct origin, the browser's same-origin policy isolates each game's storage, cookies, and scripts from every other game and from the shell.
All communication is postMessage:
Outbound (game → shell). The shell validates the sender origin with an exact string match against your game's origin — never a prefix or suffix check — and rebuilds every field from scratch, clamping numbers and truncating strings. A malformed message is silently dropped, never an error.
Inbound (shell → game). The SDK only delivers messages whose
sourceis the direct parent window. Siblings, nested frames, and unrelated windows are ignored.
Outbound messages carry no secrets #
ready, progress, score, event, error, requestFullscreen, and exit are posted with targetOrigin: "*" by default. That is intentional: they carry no secrets, and a sandboxed game cannot know which shell origin embeds it (production, staging, or a local preview). There is nothing to leak.
If your game only ever ships to a single shell, you can still pin the target origin:
carver.configure({ targetOrigin: "https://carverjs.dev" });You can also require inbound messages to come from a specific origin, on top of the always-on parent-window check:
carver.configure({ parentOrigin: "https://carverjs.dev" });Identity tokens #
getIdentity() is the one place a sensitive value crosses into your game — a signed token proving which player is playing. Three properties keep it safe:
The token is minted by the shell from the signed-in session, not assembled by the game. Your game cannot forge one.
The shell delivers it pinned to your exact game origin — never
"*"— so it cannot be intercepted by a different frame.The token is short-lived and signed (RS256). It is useless to anyone who cannot present it before it expires, and tampering breaks the signature.
The token is for your backend. Always verify it server-side against the public JWKS:
https://www.carverjs.dev/.well-known/jwks.jsonAnything the client can read, the client can fake. The signature only means something once your server has checked it against the marketplace's public keys. Use the verified sub claim — not anything the game tells you — to decide who the player is. See Player Identity.
Sandbox and permissions #
The shell runs your game with a deliberately narrow sandbox and permissions policy:
Scripts, pointer lock, fullscreen, and in-frame forms are allowed.
Pop-ups, top-level navigation, and reaching into the parent's storage scope are not.
Cameras, microphones, geolocation, USB, and MIDI are denied — a game cannot prompt for them even if it tries.
Build for those constraints. If your game needs network access to your own backend (for example to save data behind an identity token), make sure your game's hosting and your API allow the request — the marketplace does not proxy it for you.
Treat inbound data as untrusted, too #
The shell is the trusted side of the channel, but defensive code is still good practice: in your subscribe handler, switch on known message type values and ignore everything else. Never feed message data into innerHTML, eval, or a query without sanitizing it.
carver.subscribe((msg) => {
switch (msg.type) {
case "carver:pause":
pauseLoop();
break;
// ignore unknown types — forward-compatible by default
}
});