Architecture
How the SDK, the bridge and the GAMEE platform actually talk.
This page explains what flows on the wire and where each piece lives. Read it once and the rest of the SDK should make sense.
System components#
HTML5
GAME
GAMEE
SDK
PLATFORM
- HTML5 game — the code you wrote. Calls
gamee.init(),updateScore(), subscribes togamee.on('pause', …). - GAMEE SDK — the typed public surface plus a platform-specific bridge picked at boot.
- Platform — gamee.com on web, the iOS app, or the Android app. Embeds the game inside an iframe,
WKWebView, orWebView.
The SDK abstracts the transport so you write the same code for all three platforms.
End-to-end session trace#
A real game’s lifetime on the wire, traced top-to-bottom.
- Request
- The game asks the platform to do something. Travels game → platform.
- Response
- The platform's reply to a specific request. Travels platform → game.
- Event
- The platform pushes a notification to the game. Travels platform → game, no reply expected.
initdeclares capabilities + versioninitDataplatform · locale · saveState · missionData …gameReadycanvas painted, ok to showstart{ resetState, gameSeed }gameStartupdateScorefire-and-forget · called every checkpointupdateMissionProgresscap: missions · 0–100logEventcap: logEvents · analytics pingpauseuser backgrounded the appresumerequestPausegame opened its own menumute / unmutepurchaseItemWithGemscap: gems{ success: true }loadRewardedVideo{ videoLoaded: true }showRewardedVideo{ videoPlayed: true }user finished the adgameSaveStatecap: saveStatesubmitplatform asks the game to finalizegameOver{ saveStateData, rewardIds }useExtraLifeplayer redeemed — resume from end stateThe first three steps are the handshake — every game does this. Everything
after is your game’s choice. start / pause / resume are the only events
the platform reliably fires; the rest are opt-in features wired through
capabilities.
Platform detection#
createBridge() decides which transport to use the moment the SDK is constructed.
Rules from bridge/detect.ts:
| User agent contains | In an iframe? | Detected platform |
|---|---|---|
gamee/X.Y.Z | — | android |
iPhone / iPad / iPod | yes | ios |
Mobi/Android/iPhone… | — | mobile_web |
| anything else | — | web |
mobile_web and web both use the postMessage bridge; ios and android
each use a native bridge.
You can override detection by injecting a bridge yourself
(createSdk({ bridge: myBridge })) — that’s how the emulator and
MockBridge work.
The wire format#
There is one envelope that flows in both directions:
{ "request": { "method": "init", "messageId": 0, "data": { ... } } }
{ "response": { "messageId": 0, "data": { ... } } }
{ "response": { "messageId": 0, "error": { "code": "...", "message": "..." } } }
messageId is a non-negative integer that increments by 1 per outbound message
within a single SDK instance (starts at 0). Wire-compatible with legacy
gamee-js clients.
request is what an RPC looks like. response is the matching ack. The
messageId ties them together — the SDK uses it to resolve the right Promise
on your await.
The same envelope shape carries platform → game events. The platform sends
a request, the SDK auto-acks it, and then dispatches the matching
gamee.on('pause', …) listener. The ack just tells the platform “got it, my
game is alive.”
Four traffic patterns#
a) Init handshake#
Game → SDK : await gamee.init({ capabilities: ['saveState'] })
SDK → Platform : { request: { method: 'init', messageId: 0,
data: { version, capabilities: { saveState: true } } } }
Platform → SDK : { response: { messageId: 0,
data: { platform, locale, country, sound, ... } } }
SDK → Game : Promise resolves with the GameInitResponse
init() may only be called once per SDK instance. A second call throws
INVALID_STATE. Use createSdk() if you need a fresh instance (e.g. tests).
b) Fire-and-forget signal (updateScore, gameReady, gameOver, …)#
Game → SDK : gamee.updateScore({ score: 123, playTime: 4.5, checksum: 'sum' })
SDK → Platform : { request: { method: 'updateScore', messageId: 1, data: {…} } }
↑ no Promise tracked, no timeout, platform ack is ignored
Signal calls do not occupy a slot in the pending registry, do not arm a timeout, and the returned Promise resolves immediately. Safe to call every frame.
c) Data-returning request (showRewardedVideo, purchaseItemWithGems, …)#
Game → SDK : const r = await gamee.showRewardedVideo()
SDK → Platform : { request: { method: 'showRewardedVideo', messageId: 2, data: null } }
(...time passes; user watches ad...)
Platform → SDK : { response: { messageId: 2, data: { videoPlayed: true } } }
SDK → Game : Promise resolves with { videoPlayed: true }
If the platform never responds within requestTimeoutMs (default 30 s), the
Promise rejects with BRIDGE_TIMEOUT.
d) Platform → game event (pause, resume, start, …)#
Platform → SDK : { request: { method: 'pause', messageId: 7, data: null } }
SDK → Platform : { response: { messageId: 7 } } ← auto-ack
SDK → Game : every gamee.on('pause', …) handler fires
Listeners can throw without breaking dispatch — errors are caught and logged
to console.error.
The PendingRegistry#
Every data-returning request is parked in an internal PendingRegistry keyed
by messageId until the platform responds. Two safety nets:
- Timeout —
requestTimeoutMs(default 30 000 ms) rejects forgotten requests withBRIDGE_TIMEOUT. - Overflow cap —
maxPendingRequests(default 256) rejects the oldest pending request withBRIDGE_OVERFLOWto make room. Only data-returning methods count; signals are not tracked.
Both are tunable via SdkOptions.
Why this matters in practice#
- The platform is authoritative.
init()resolves with the platform’s view of the world: locale, country, save state, mission. Don’t hard-code these — read them from the resolved value every session. - Capabilities gate at the SDK boundary. Calling a gated method without
the matching capability throws
CAPABILITY_MISSINGsynchronously, before anything hits the wire. - Errors are typed. Every rejection is a
GameeError. Branch onerr.code. See error handling. - Events fire after init. The platform won’t dispatch
start,pause, etc. until the handshake succeeds. init()is one-shot. UsecreateSdk()for repeated init in tests.
Watching the wire#
Open the emulator, point it at any game URL, and the Bridge traffic panel logs every byte. Toggle the verbose switch and each row gets a plain-English annotation. That’s the fastest way to internalize the model.
Common pitfalls#
- “My events don’t fire” — your game has not called
init()yet, or has not yet awaited it. - “My save doesn’t persist” — you forgot to declare the
saveStatecapability ininit({ capabilities: [...] }). - “
inittimed out” — the platform did not respond in 30 s. Confirm the game is loaded inside a real GAMEE platform, or use the emulator /MockBridge. - “Random fields are missing in init data” — the platform only sends fields
that are relevant.
missionDatais only present when the user opened the game from a mission card;saveStateis only present if a save exists.