Guides

How games run

AdminUpdated Sep 22, 2026

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

https://coolgptgames.com

Game pages, the player's account session, the SDK's message partner

The game CDN

https://cdn.coolgptgames.com

Your uploaded files, the SDK script (/sdk/game-sdk.js)

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.png points 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 form https://cdn.coolgptgames.com/sdk/game-sdk.js also 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.html can'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 a 403). 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 eval / new Function

The upload scanner flags eval-style code for human review. It isn't blocked.

WebAssembly

.wasm files are allowed in uploads, and so are pre-compressed .wasm.br / .wasm.gz files (served with the matching Content-Encoding).

ES modules (<script type="module">, import)

Your files are served with Access-Control-Allow-Origin: *, so module imports from your own bundle work. Both .js and .mjs files are allowed and served as JavaScript.

Canvas, WebGL, Web Audio

Loading your own files with fetch / XHR

Use relative URLs.

Pointer lock

allow-pointer-lock

Gamepads

allow="gamepad"

Fullscreen requested by your game

allow="fullscreen". The site also has its own Fullscreen button.

Screen orientation lock

allow-orientation-lock. Browsers generally only honour orientation lock while in fullscreen.

Autoplaying audio

⚠️

allow="autoplay" is delegated, but browser autoplay rules still apply. Start audio after the player's first tap or key press to be safe.

What's blocked

Capability

Why

Use instead

localStorage, sessionStorage, IndexedDB, cookies

Opaque origin: accessing them throws a SecurityError

GameSDK.saveData / loadData (see Saves & cloud progress). Wrap any storage access in try/catch.

alert(), confirm(), prompt()

No allow-modals

Draw your own dialogs

Opening new windows / tabs (window.open, target="_blank" links)

No allow-popups

GameSDK.social.viewProfile(handle) opens profiles for you

Navigating the top page, or redirecting away

No allow-top-navigation

Nothing. Games stay on their page.

Submitting HTML forms

No allow-forms, and CSP form-action 'none'

Handle input in JavaScript

Nested iframes

CSP frame-src 'none'

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 script-src / style-src allow only your own files and inline code

Copy libraries and fonts into your zip

Network requests to hosts you haven't declared

CSP connect-src (see below)

Declare the host in connectHosts, or bundle the data

Content Security Policy

Every file in your build is served with a Content Security Policy. In summary:

Directive

Allows

script-src

Your own files, inline scripts, eval

style-src

Your own files, inline styles

img-src, media-src

Your own files, data: and blob: URLs, plus your declared hosts

font-src

Your own files, data: URLs, plus your declared hosts

connect-src

Your own files, the Cool GPT Games API, the multiplayer relay (only if your game is multiplayer-enabled), plus your declared hosts

frame-src, object-src

Nothing

base-uri, form-action

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

https://api.example.com or a bare api.example.com (treated as HTTPS). http:// is refused.

Exact hosts

No wildcards (*.example.com), no IP addresses, no localhost, and no single-label or internal names (.local, .internal, .lan, .corp, …).

Origins, not URLs

Paths, queries and fragments are dropped. An explicit port is kept (https://api.example.com:8443).

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: *, and

  • not 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

"connectHosts": ["https://api.example.com"] in game.json next to your files. See CLI.

REST API / Publisher SDK

connectHosts on the upload request. See REST API and 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.com and whose data has a string type. 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

.zip with an index.html (plain JS, Phaser, Three.js, Godot, Unity and other web exports)

100 MB

None

PICO-8

.zip of PICO-8's HTML export (EXPORT game.html)

8 MB

The files must look like a PICO-8 web export

Twine

.zip of Twine's "Publish to File" HTML

10 MB

The files must look like a Twine story (Harlowe, SugarCube, …)

Bitsy

.zip of Bitsy's downloaded game.html

8 MB

The files must look like a Bitsy export

Rules shared by every runtime:

  • The entry point is the shallowest index.html (or index.htm) in the zip. A single wrapping folder is fine. Rename a tool's game.html to index.html before 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 stray README.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/runtimes reports the current value as maxUploadBytes.

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

  1. Add <script src="/sdk/game-sdk.js"></script> to the exported HTML before zipping.

  2. Call window.GameSDK from 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 .zip is 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, twine or bitsy). 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 ends

Step

Your call

What the site does

Loading

none

Shows your loading screen image (or cover) with a spinner

First screen visible

GameSDK.ready()

Removes the loading overlay right away. Without it, the overlay goes 1.2 s after index.html finishes loading.

Gameplay begins

GameSDK.start()

Starts the play session on the first call. Later calls reuse it.

Run ends

GameSDK.gameOver(score)

Signals the end of a run. It does not save the score.

Record the score

GameSDK.submitScore(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

pause

The site's browser tab becomes hidden (tab switch, window minimised, phone locked)

resume

The tab becomes visible again

Those are the only triggers. In particular:

  • Ads don't send pause/resume. Pause on adStarted and resume on adComplete or adError. See Ads & rewards.

  • Losing focus while still visible (clicking outside the game, another window on top) sends nothing.

  • The Fullscreen button sends nothing. Handle resize instead.

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

  1. Start. On the first start() call (or replay.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

saveData / loadData / listSaves / deleteSave

✅ 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.

getPlayer()

{ signedIn: false, level: 1, handle: null }

Level and handle

awardXp, unlockAchievement

Nothing recorded (granted: 0). The site nudges the player to sign in.

✅ Subject to caps

submitScore (leaderboards, tournaments)

Dropped. The site nudges the player to sign in.

getMyScore()

{ score: null, rank: null }

Their best and rank

Read the leaderboard, remote config, store items, tournament list

social.addFriend, social.getFriends

status: "error" / []

economy.purchase

{ error: "sign_in_required" }

tournaments.join

null

ranked.* (except the leaderboard)

{ error: "sign_in_required" }

Ads (requestAd)

Analytics (trackEvent)

✅ (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.innerHeight and listen for resize. 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 / pointermove work for mouse and touch), and set touch-action: none on 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

Was this page helpful?