The security model
Games here are arbitrary HTML and JavaScript written by strangers and their agents. This is exactly what stops one from reading your account, calling home, or pretending to be us.
- Separate origin
- No network at all
- Human-reviewed
The three guarantees
Everything below is the mechanism behind these. If you only read one section, read this one.
Games are served from a different origin to the app, inside an iframe with no same-origin privileges. There is no cookie, no session token and no API response for a game to read — the browser refuses before any of our code is involved.
connect-src 'none' means fetch, XHR, WebSocket and sendBeacon all fail inside the sandbox. A game has nowhere to send anything, so there is nothing for it to collect.
Containment stops the technical attacks; it says nothing about whether a game is what it claims to be. That is what review is for, and every game goes through it.
What we are defending against
The hostile case is a game written specifically to attack the person playing it: read their session cookie and take over the account, quietly ship what they do to a server somewhere, draw a convincing TipTap login over itself, or break out of the frame and script the app around it. The containment below is designed so that each of those fails at the browser level rather than because we noticed it in review.
Two origins that do not overlap
The product and the games are two separate hosts, and each one refuses to serve the other's routes. Because they are different origins, the browser's same-origin policy blocks a game from reading app cookies, storage or API responses before CSP is even considered.
| Request | App hosthttps://tiptapgames.com | Sandbox hosthttps://swipe-to-play-sandbox-production.up.railway.app |
|---|---|---|
/ /create /admin … | Served normally | 404 |
/play/[id] | Redirect to / | Served here, and only here |
An iframe with no origin at all
The game is embedded with sandbox="allow-scripts" and deliberately without allow-same-origin. The browser then gives the document a null origin: it has no identity, so there is no cookie jar, no localStorage keyed to a real host, and no reference back to the page that embedded it.
The same restriction is applied twice — once as the iframe attribute, once as a sandbox directive in the response headers. Either alone would do it; both are set so the isolation survives one of them being wrong.
The policy, line by line
This is the live header sent with every game on this deployment, read straight from the constant the server uses. It cannot describe a policy we do not actually serve.
sandbox allow-scriptsScripts run, and nothing else does. The document gets a null origin: no cookies, no same-origin storage, no popups, no reach into the page that embedded it.
default-src 'none'Start from nothing. Every capability below is an explicit exception to this line.
script-src 'unsafe-inline' 'unsafe-eval'Inline and generated JavaScript are allowed — a one-file game is nothing but inline script. Safe here because the origin is null and the network is already cut.
style-src 'unsafe-inline'Inline CSS, for the same reason.
img-src data: blob:Images only from bytes already inside the file. No remote image can be fetched — which is also how a tracking pixel would work.
media-src data: blob:Audio and video on the same terms: embedded only.
font-src data:Fonts have to be embedded too.
connect-src 'none'No network, at all. fetch(), XHR, WebSocket and sendBeacon are dead ends. This is the line that turns no phone-home from a promise into a guarantee.
form-action 'none'No form can post anywhere.
base-uri 'none'The document cannot rewrite its own base URL to escape the rules above.
frame-ancestors https://tiptapgames.com https://*.discordsays.comOnly TipTap may embed a game. Anyone hotlinking one into their own page gets a blank frame, which is what stops a game being wrapped in a clickjacking shell.
| Directive | What it does |
|---|---|
sandbox allow-scripts | Scripts run, and nothing else does. The document gets a null origin: no cookies, no same-origin storage, no popups, no reach into the page that embedded it. |
default-src 'none' | Start from nothing. Every capability below is an explicit exception to this line. |
script-src 'unsafe-inline' 'unsafe-eval' | Inline and generated JavaScript are allowed — a one-file game is nothing but inline script. Safe here because the origin is null and the network is already cut. |
style-src 'unsafe-inline' | Inline CSS, for the same reason. |
img-src data: blob: | Images only from bytes already inside the file. No remote image can be fetched — which is also how a tracking pixel would work. |
media-src data: blob: | Audio and video on the same terms: embedded only. |
font-src data: | Fonts have to be embedded too. |
connect-src 'none' | No network, at all. fetch(), XHR, WebSocket and sendBeacon are dead ends. This is the line that turns no phone-home from a promise into a guarantee. |
form-action 'none' | No form can post anywhere. |
base-uri 'none' | The document cannot rewrite its own base URL to escape the rules above. |
frame-ancestors https://tiptapgames.com https://*.discordsays.com | Only TipTap may embed a game. Anyone hotlinking one into their own page gets a blank frame, which is what stops a game being wrapped in a clickjacking shell. |
<iframe sandbox="allow-scripts">The embed itself carries the same restriction, deliberately without allow-same-origin. Both layers are applied so the isolation survives one of them being misconfigured.
Play counts and leaderboard writes still work, because they are sent by the app shell around the iframe rather than from inside it. Nothing in the sandbox is granted network access to make that happen.
What this means if you are building here
Most of the constraints creators run into are this model showing through. None of them are style preferences, and none can be waived.
- One file, everything inlinedNo CDN script tag, no external font, no remote image. Anything not in the file you upload will not load, and it fails silently in the feed rather than loudly on your machine.
- No fetch, everThere is no allowlist to be added to. A game that needs a server is a game that cannot run here.
- Scores go through the SDKThe SDK talks to the app shell across the frame boundary rather than opening its own connection. That is why score submission works while fetch does not.
- Storage does not persistA null origin has no durable storage. Keep run state in memory and hand anything that should outlive the session to the SDK.
The SDK reference has the working API, and the agent spec is the version to hand an agent before it starts writing.
Where people come in
Containment is absolute and indifferent. It cannot tell whether a game is fun, honest, or appropriate — so nothing reaches the feed on containment alone.
- Review gates the feed. Automated risk scoring reads the uploaded HTML for patterns like remote script includes and frame-escape attempts, and orders the queue by what looks worth a close look. It is triage only — it never approves anything, and a high score does not tighten the sandbox, because the sandbox is already as tight as it goes.
- Reporting catches what review missed. Any player can report a game from the feed. A report opens a case a moderator reads rather than pulling the game automatically, and the reasons they can choose from are listed in the content policy.
What this does not cover
Stated plainly, because a security page that only lists its wins is not worth reading.
- Inline and eval'd script are allowed inside the sandboxA single-file game is nothing but inline script, so unsafe-inline and unsafe-eval are permitted. They buy an attacker nothing here: the origin is null, so there is no data to steal, and the network is cut, so there is nowhere to send it.
- Games share a process with the tabA game can still burn CPU or GPU with a busy loop or a shader storm. Nothing in CSP prevents that. The upload cap of 5 MB and human review are the mitigations, and a game that hangs a device is a rejection.
- The guarantee is browser-level, not VM-levelWe do not run games in a virtual machine or a WASM container. Isolation is origin separation plus CSP, enforced by the browser — the same model every web-based game platform relies on.

