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 lazyimport(). 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
| Feature | Status | What to do |
|---|---|---|
| Service workers | Not available | Browsers 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, cookies | In memory only | They 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 API | Not available | Both need a real origin. Feature-detect and fall back. |
| Requests to your own server | Sent with Origin: null | Allow it in your CORS policy and authenticate with a token — see below. Cookies are never sent. |
| Module workers that import other files | Not supported | Import 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 MB | Loaded, not rewritten | Relative 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, downloads | Blocked | A game cannot redirect the learner’s tab or trigger file downloads. Popups open, but stay sandboxed. |
| Embedding edu.games pages | Blocked | Your 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.
// 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 requestWebSockets 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.
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.
// 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: trueas 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.