How games run
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
How games run
This page explains what happens between a player clicking your game and your code running: where your files are served from, the sandbox your game runs in, how the SDK talks to the page, what network access you have, how plays and play time are measured, and what guests get compared with signed-in players.
If you just want to ship something, start with Build and publish your first game and come back here when something behaves unexpectedly.
The big picture
┌──────────────────────── coolgptgames.com ────────────────────────┐
│ Game page (the site) │
│ • player account & sign-in • ads, toasts, loading screen │
│ • play-session metering • talks to the API │
│ │
│ ┌──────────── sandboxed iframe ─────────────┐ │
│ │ Your game, served from the game CDN │ │
│ │ https://cdn.coolgptgames.com/g/<game>/<version>/index.html │
│ │ │ │
│ │ GameSDK ── postMessage ──► the site │ │
│ │ ◄── postMessage ── │ │
│ └───────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘Your game never talks to player accounts directly. It asks the site through the SDK, and the site decides what is allowed, calls the Cool GPT Games API with the player's credentials, and sends the answer back. That's how saves, XP, scores and purchases work without your game ever handling a login token.
Two origins: the site and the game CDN
Origin | What lives there | |
|---|---|---|
The site |
| Game pages, the player's account session, the SDK's message partner |
The game CDN |
| Your uploaded files, the SDK script ( |
Your build is stored under a version-specific path, for example https://cdn.coolgptgames.com/g/<gameId>/<versionId>/index.html. That has some practical consequences:
Use relative paths for your own files (
sprites/hero.png,./levels.json). A root-relative path like/sprites/hero.pngpoints at the root of the CDN, not your build, and will 404.The one exception is the SDK, which really does live at the CDN root:
<script src="/sdk/game-sdk.js"></script>. The absolute formhttps://cdn.coolgptgames.com/sdk/game-sdk.jsalso works and passes the upload scanner, but the relative path is recommended.Don't write absolute URLs to your own files. The version-specific folder isn't known until you upload, so an absolute URL can't point at the right build. Absolute URLs to hosts other than the platform's own (and any you declared) are treated as external network access by the upload scanner and get the build rejected.
Each uploaded version gets a new path, and your files are served with long-lived caching. You never need cache-busting query strings between versions.
Your
index.htmlcan't be opened by pasting its CDN URL into a browser. The site adds a short-lived access ticket (valid for 15 minutes) when it renders the player, and the CDN refuses to serve the entry page without one (you get a403). Always test through the game's page on the site.
Because the game is on a different origin from the site, your code can't read the site's cookies, storage or DOM, and the site can't read yours. The two sides communicate only through the SDK.
The sandbox
Your game runs in an <iframe> with these attributes:
<iframe sandbox="allow-scripts allow-pointer-lock allow-orientation-lock"
allow="autoplay; fullscreen; gamepad"
referrerpolicy="no-referrer">The sandbox deliberately leaves out allow-same-origin, so your game runs with an opaque origin (its origin is the string "null"). This is the most important fact about the environment.
What's allowed
Capability | Status | Notes |
|---|---|---|
JavaScript, including | ✅ | The upload scanner flags |
WebAssembly | ✅ |
|
ES modules ( | ✅ | Your files are served with |
Canvas, WebGL, Web Audio | ✅ | |
Loading your own files with | ✅ | Use relative URLs. |
Pointer lock | ✅ |
|
Gamepads | ✅ |
|
Fullscreen requested by your game | ✅ |
|
Screen orientation lock | ✅ |
|
Autoplaying audio | ⚠️ |
|
What's blocked
Capability | Why | Use instead |
|---|---|---|
| Opaque origin: accessing them throws a |
|
| No | Draw your own dialogs |
Opening new windows / tabs ( | No |
|
Navigating the top page, or redirecting away | No | Nothing. Games stay on their page. |
Submitting HTML forms | No | Handle input in JavaScript |
Nested iframes | CSP | Bundle everything into your page |
Service workers | Opaque origin, and flagged by the upload scanner | Not supported |
Scripts or stylesheets from other hosts (public JS CDNs, Google Fonts CSS) | CSP | Copy libraries and fonts into your zip |
Network requests to hosts you haven't declared | CSP | Declare the host in |
Content Security Policy
Every file in your build is served with a Content Security Policy. In summary:
Directive | Allows |
|---|---|
| Your own files, inline scripts, |
| Your own files, inline styles |
| Your own files, |
| Your own files, |
| Your own files, the Cool GPT Games API, the multiplayer relay (only if your game is multiplayer-enabled), plus your declared hosts |
| Nothing |
| Nothing |
The same policy applies whichever runtime you uploaded with.
Network access and connectHosts
Games are sealed by default: apart from its own files, the Cool GPT Games API and (for multiplayer games) the multiplayer relay, a game can't reach the network. The browser enforces this, not your code.
If your game needs an external service (a public API, images from a known host), you can declare up to 10 hosts for a version. The declared hosts are added to that version's connect-src, img-src, media-src and font-src, so one declaration covers fetch/XHR, images, audio/video and web fonts. Declared hosts do not let you load scripts or stylesheets from that host.
Rules for declared hosts
Rule | Detail |
|---|---|
HTTPS only |
|
Exact hosts | No wildcards ( |
Origins, not URLs | Paths, queries and fragments are dropped. An explicit port is kept ( |
No first-party hosts | The site, API, CDN and multiplayer relay are already allowed and can't be declared. |
Maximum 10 | Duplicates are removed first. |
Per version | Declared again with every upload, and reviewed by moderation for each version. |
A declaration that breaks these rules fails the upload immediately with a bad_connect_hosts or too_many_connect_hosts error.
Moderation honours declarations too: a hard-coded fetch("https://api.example.com/…") or <img src="https://api.example.com/…"> to a declared host passes the code scan and the play-test. A request to a host you didn't declare is still an automatic rejection.
Requirements on the external service
Your game's origin is "null", so the service you call must:
respond with
Access-Control-Allow-Origin: *, andnot need cookies or other credentials.
Most public read-only APIs meet this. Anything that needs a secret API key doesn't belong in client-side game code anyway. Put it behind your own server, and declare that server's host.
How to declare hosts
Upload path | How |
|---|---|
Website upload form | Not available. The form has no field for it. |
CLI |
|
REST API / Publisher SDK |
|
If the data never changes, it's simpler to ship it in your zip (for example data.json): no network, no CORS, nothing extra to review.
How the SDK talks to the page
The SDK uses window.postMessage between your iframe and the site:
Outgoing. Every SDK call becomes a message to the parent page, addressed only to
https://coolgptgames.com. If the parent is anything else (another site, or no parent at all), the browser silently discards the message.Incoming. The SDK only accepts messages whose origin is exactly
https://coolgptgames.comand whose data has a stringtype. Anything else is ignored. Other scripts can't impersonate the site to your game.On the site's side. The page only accepts messages from your game's own iframe window, checks each message's type and fields, and ignores unknown types. - Rate cap. The page accepts at most 30 messages per second from a game and silently drops the rest. - Loading order. The site attaches its message listener before your game starts loading, so calls you make as soon as your script runs (like
ready()) are never missed.
The site never trusts what a game claims about the player. It forwards requests to the API with the player's own credentials, and the API decides what actually happens (for example, how much of a requested XP grant lands). See GameSDK core reference for the per-method details and fallbacks.
Supported runtimes
When you upload, your build is tagged with a runtime:
Runtime | Upload | Max upload size | Extra check at upload |
|---|---|---|---|
HTML5 |
| 100 MB | None |
PICO-8 |
| 8 MB | The files must look like a PICO-8 web export |
Twine |
| 10 MB | The files must look like a Twine story (Harlowe, SugarCube, …) |
Bitsy |
| 8 MB | The files must look like a Bitsy export |
Rules shared by every runtime:
The entry point is the shallowest
index.html(orindex.htm) in the zip. A single wrapping folder is fine. Rename a tool'sgame.htmltoindex.htmlbefore zipping.Only web file types are allowed inside the zip:
.html .htm .js .mjs .css .json .map .txt .wasm .png .jpg .jpeg .gif .webp .svg .ico .mp3 .ogg .wav .woff .woff2 .ttf .glb .gltf .bin .pck .data .br .gz. Anything else, such as a strayREADME.md, fails the upload. OS clutter (__MACOSX/,.DS_Store,Thumbs.db,desktop.ini) is skipped silently.The max upload size in the table is enforced per runtime, and
GET /v1/runtimesreports the current value asmaxUploadBytes.Limits: at most 2,000 files, 300 MB uncompressed, and a compression ratio no higher than 100:1.
At runtime, all runtimes are served the same way: your export is the game page. The platform doesn't wrap it in a player or bundle any engine. PICO-8, Twine and Bitsy exports already contain their engine.
See Packaging & publishing for the full packaging rules and error codes.
The SDK in non-HTML5 runtimes
The SDK is not injected automatically for any runtime. If you want saves, scores, XP or ads in a PICO-8, Twine or Bitsy game:
Add
<script src="/sdk/game-sdk.js"></script>to the exported HTML before zipping.Call
window.GameSDKfrom JavaScript your export can run. Twine story formats like SugarCube and Harlowe let you run JavaScript. PICO-8 and Bitsy need a small JavaScript bridge that you add to the exported page yourself.
Choosing the runtime
The website upload form detects the runtime from the file you drop. Any
.zipis uploaded as HTML5, including a zipped PICO-8, Twine or Bitsy export. That works, because those exports are ordinary HTML5 pages, but it doesn't run the engine-specific check, and the 100 MB HTML5 limit applies.To tag a build as PICO-8, Twine or Bitsy, use the REST API or Publisher SDK and pass the runtime explicitly (
pico8,twineorbitsy). The CLI always uploads as HTML5.
Game lifecycle
page opens ──► iframe loads index.html ──► ready() ──► start() ──► … play … ──► gameOver(score)
│ │ + submitScore(score)
│ └─ play session starts (heartbeat every 15 s)
└─ loading overlay removed
start() again for the next round (same session)
player leaves the page ──► play session endsStep | Your call | What the site does |
|---|---|---|
Loading | none | Shows your loading screen image (or cover) with a spinner |
First screen visible |
| Removes the loading overlay right away. Without it, the overlay goes 1.2 s after |
Gameplay begins |
| Starts the play session on the first call. Later calls reuse it. |
Run ends |
| Signals the end of a run. It does not save the score. |
Record the score |
| Sends the score to the leaderboard (see Scores, leaderboards & anti-cheat) |
Player leaves | none | Ends the play session |
If index.html hasn't finished loading about 15 seconds after the player opens the page (plus a short runtime-specific warm-up), the site shows "This game failed to load".
There's no "restart" or "quit" call. Show your own game-over screen and "Play again" button, and call start() again when the next round begins.
Pause and resume
The site sends two lifecycle events to your game, which you receive with GameSDK.on(...):
Event | Sent when |
|---|---|
| The site's browser tab becomes hidden (tab switch, window minimised, phone locked) |
| The tab becomes visible again |
Those are the only triggers. In particular:
Ads don't send
pause/resume. Pause onadStartedand resume onadCompleteoradError. See Ads & rewards.Losing focus while still visible (clicking outside the game, another window on top) sends nothing.
The Fullscreen button sends nothing. Handle
resizeinstead.
A robust game pauses on pause and adStarted, resumes on resume, adComplete and adError, and caps the frame delta so a long gap doesn't become one giant physics step. There's an example in GameSDK core reference.
Play sessions and play time
A play session is how the platform measures that someone actually played your game. Plays on your game page and dashboard, XP and leaderboard eligibility, mid-roll ads and analytics all hang off it.
Lifecycle of a session
Start. On the first
start()call (orreplay.ready()) of a page load, the site asks the API to start a session. This works for guests and signed-in players, but only for published games. 2. Heartbeats. While the game page stays open, the site sends a heartbeat every 15 seconds. A heartbeat only counts if at least 12 seconds of real time have passed since the last counted one, so play time can't be inflated by sending heartbeats faster. 3. End. When the player leaves the game page, including closing the tab, the site ends the session. The end call is sent in a way that survives the page unloading.
There's one session per page load. "Play again" rounds share it.
Heartbeats run on a timer while the page is open. They don't stop when your game pauses. Browsers may slow timers in background tabs.
What counts
Threshold | Requirement |
|---|---|
Valid play (counted in your game's play total) | Session lasted at least 30 seconds with at least 2 counted heartbeats, and wasn't flagged |
A session is flagged (it doesn't count and earns nothing) when the same browser has already started 20 sessions of your game in the last 24 hours.
In practice:
A player who closes your game after 20 seconds generates a session but not a valid play.
In a quick game, the first round's score may arrive before the session has a minute of play. That score isn't recorded. Later rounds in the same visit are. Test with at least a minute of play.
Starting sessions is limited to 60 per hour per IP address.
The play total on your game page is refreshed periodically, not in real time.
Guests vs. signed-in players
Anyone can play without an account. Signing in unlocks everything tied to a player's identity.
Feature | Guest | Signed in |
|---|---|---|
Play the game, play session metering | ✅ | ✅ |
| ✅ Saved in this browser only, on the site | ✅ Saved to their account (cross-device) |
Guest saves carried over on sign-in | n/a | ✅ Local saves are copied into the account at sign-in. Existing cloud slots are never overwritten. |
|
| Level and handle |
| Nothing recorded ( | ✅ Subject to caps |
| Dropped. The site nudges the player to sign in. | ✅ |
|
| Their best and rank |
Read the leaderboard, remote config, store items, tournament list | ✅ | ✅ |
|
| ✅ |
|
| ✅ |
|
| ✅ |
|
| ✅ |
Ads ( | ✅ | ✅ |
Analytics ( | ✅ (needs a session) | ✅ (needs a session) |
Guest saves live in the site's browser storage, so a guest who clears site data or switches browsers loses them. That's one reason to show "Sign in to save your progress" in your UI when getPlayer() says the player is a guest.
Embedded games (played through the Cool GPT Games embed player on another website) always run as guests. See Embedding & the score bridge.
Screen size, mobile and fullscreen
Desktop. The game area is a 16:9 box as wide as the page's main column. - Phones and small screens (narrower than about 720 px). The game area is full width and uses most of the screen height (up to 680 px), so portrait games get real vertical room. The exact aspect ratio depends on the device. - Fullscreen button. The bar under the game has a Fullscreen button that makes the whole player (your game plus the site's in-game notifications) fill the screen. Your game can also request fullscreen itself. - Embed player. The game fills the embedding iframe.
To handle all of these:
Size your canvas from
window.innerWidth/window.innerHeightand listen forresize. Don't assume a fixed resolution or aspect ratio.Add
<meta name="viewport" content="width=device-width, initial-scale=1">so mobile browsers don't render your page zoomed out.Support touch (
pointerdown/pointermovework for mouse and touch), and settouch-action: noneon your canvas so swipes don't scroll the page.Keep important UI away from the top-right corner. The site shows short notifications there (XP, achievements, level-ups, sign-in nudges).
Related
Build and publish your first game: a complete walkthrough. - GameSDK core reference:
ready,start,gameOver,getPlayer, events, fallbacks.Packaging & publishing: zip rules, moderation, versions. - Errors & limits: every limit in one place.