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 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:

ts
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:

ts
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 for your backend. Always verify it server-side against the public JWKS:

text
https://www.carverjs.dev/.well-known/jwks.json

Sandbox and permissions #

The shell runs your game with a deliberately narrow sandbox and permissions policy:

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.

ts
carver.subscribe((msg) => {
  switch (msg.type) {
    case "carver:pause":
      pauseLoop();
      break;
    // ignore unknown types — forward-compatible by default
  }
});