Documentation
FOR DEVELOPERS

Browser Runtime & Your Backend

What your browser build can and cannot do once it is running on edu.games, how to talk to your own server, and how to recognise a signed-in edu.games player without a second login.

How your game runs

Your zip is downloaded and unpacked in the learner’s browser, and the game runs in a sandboxed iframe with no origin of its own (an “opaque” origin, which the browser reports as null). Every file in your zip is served from memory. This is what stops any game from reading a learner’s edu.games session, and it is not configurable per game.

You do not need to change paths for this. Relative and root-relative references (./assets/x.png, /assets/x.png) are resolved to the files in your zip automatically.

What works

  • Modern bundler output — Vite, webpack 5, Rollup, esbuild — including ES modules (<script type="module">), code-splitting, circular chunk imports and lazy import(). No need to inline your JavaScript.
  • Assets referenced from import.meta.url — fetch(new URL('./data.json', import.meta.url)), images, audio.
  • Web Workers — new Worker(new URL('./worker.js', import.meta.url)), classic or single-file module workers.
  • WebAssembly including WebAssembly.instantiateStreaming (Unity, Godot, Emscripten).
  • Stylesheets with url() references to images and fonts in your zip.
  • Your own import map — merged with the one edu.games adds, so bare specifiers such as "three" keep working.
  • Engine exports — Unity WebGL, Godot, GDevelop, Phaser and plain HTML/canvas games already run unmodified.
  • Outbound HTTPS and WebSocket connections to your own servers (see below).

Limitations

FeatureStatusWhat to do
Service workersNot availableBrowsers refuse service-worker registration in a sandboxed frame. Disable PWA/offline plugins (e.g. vite-plugin-pwa, Workbox) for your edu.games build.
localStorage, sessionStorage, cookiesIn memory onlyThey work, so code that uses them will not crash — but everything is lost when the tab closes. Save progress with the xAPI State API or on your own server.
IndexedDB, Cache APINot availableBoth need a real origin. Feature-detect and fall back.
Requests to your own serverSent with Origin: nullAllow it in your CORS policy and authenticate with a token — see below. Cookies are never sent.
Module workers that import other filesNot supportedImport maps do not apply inside workers. Bundle each worker into a single file (Vite does this by default; for module workers set worker.format to "iife" or inline it with ?worker&inline).
JavaScript files over 20 MBLoaded, not rewrittenRelative imports inside a single file this large are not resolved. Split large bundles (code-splitting) or keep them free of relative imports.
Top-level navigation, downloadsBlockedA game cannot redirect the learner’s tab or trigger file downloads. Popups open, but stay sandboxed.
Embedding edu.games pagesBlockedYour game cannot frame edu.games itself.

Test before you submit. The edugames.dev sandbox runs your build in exactly the same frame as the live site, so anything that fails there will fail for learners too.

Connecting to your own backend

Because the game has no origin, every request it makes arrives with the header Origin: null, and no cookies are sent. A CORS policy that only allows your own domain will reject it.

Allowing null is safe when your API authenticates with a token in the Authorization header, not with cookies. CORS only controls which web pages may read a response; it never stopped scripts or other clients from calling your API, so token checks were always the real protection. Do not use credentialed CORS (Access-Control-Allow-Credentials) for your game API.

javascript
// Express example: token-authenticated API reachable from the edu.games frame
app.use('/api', cors({
  origin: (origin, cb) => cb(null, origin === 'null' || ALLOWED_ORIGINS.includes(origin)),
  credentials: false,                         // never rely on cookies here
  allowedHeaders: ['Authorization', 'Content-Type'],
}))
app.use('/api', requireBearerToken)           // your own session token, checked on every request

WebSockets are not subject to CORS. The browser sends Origin: null on the handshake; authenticate the connection with a token (in the first message, or a short-lived token in the URL) rather than by checking the origin.

edu.games does not proxy traffic to third-party servers and does not offer a per-game origin today.

Identifying signed-in players (single sign-on)

When a signed-in learner launches your game, it receives a launch object in window.EDUGAMES_LAUNCH (the SDK reads it for you). Its auth field is a signed, short-lived token. Send that token to your server; your server asks edu.games who it belongs to, then issues its own session. The learner never creates a second account.

plaintext
game (in the frame)          your server                     edu.games
       |  POST /login {token}  ->  |                                |
       |                           |  POST /api/launch/verify  ->   |
       |                           |  <- { player_id, ... }         |
       |  <- your session token    |  find-or-create account        |

POST https://edu.games/api/launch/verify

Server-to-server only — the response carries no CORS headers, so a browser cannot read it.

json
// Request
{ "token": "<EDUGAMES_LAUNCH.auth, with or without the 'Bearer ' prefix>",
  "game_id": "<your game's id, from the developer portal URL>" }

// 200 OK
{ "valid": true,
  "game_id": "2fd19c1b-…",
  "player_id": "8c0e…",          // stable edu.games id for this learner; null when anonymous
  "anonymous": false,
  "sandbox": false,              // true for edugames.dev test sessions
  "registration": "a41f…",       // this play session
  "expires_at": "2026-10-20T09:14:00.000Z" }

// 401 — forged, expired, or minted for a different game
{ "valid": false, "error": "invalid_token" }
  • Always send your own game_id. A token is only valid for the game it was issued to, so a token from another game cannot be used to sign in to yours.
  • Exchange the token straight away. It expires two hours after launch; issue your own session token and use that from then on.
  • Treat sandbox: true as test data. Sandbox personas are shared test identities, not real learners.
  • Handle anonymous play. Learners who are not signed in get no player id — offer a guest mode or ask them to sign in to edu.games.
  • Rate limit: 120 requests per minute per server IP.

When a launch token is provided

  • On edu.games: for every browser game — html5, xAPI or SCORM — once it is published, whenever the learner is signed in. (xAPI and SCORM games also get a token for signed-out play, so their statements reach the LRS; it reports anonymous: true.)
  • On edugames.dev: for every sandbox session, with sandbox: true — use it to build and test your login flow before review.

Privacy. Many edu.games players are children. player_id is a pseudonymous identifier — no name or email is shared. Do not combine it with other identifiers, collect personal data beyond what your game needs, or reuse it outside your game. See Data Retention & Privacy.

← Packaging Your GameSCORM Packages →