Skip to content

Outbox & App Events ​

The engine sends nothing of its own and knows no server address. A game that wants usage statistics, error reports or scores on a server of its own sends them from its scripts, through api.outbox, and hears what happens to the app (launch, background, foreground, close, errors) as events on the script bus.

The outbox ​

api.outbox.post(url, body) hands the engine a plain object or array (at most 32 KB as JSON) for url. The engine keeps it until a send gets through and sends it in batches:

ts
api.outbox.post("https://example.com/api/t", { event: "level", level: 3 });
api.outbox.flush();               // send now, rather than at the next moment below
api.outbox.clear("https://example.com/api/t"); // forget what waits for this URL, unsent
MemberWhat it does
post(url, body) → booleanQueues body. url must be https, or http to localhost, 127.0.0.1 or [::1] for a server on your own computer. Returns whether it was taken; a refused URL or body is logged with the reason.
flush()Sends what waits now.
clear(url)Forgets everything waiting for url (a player who turns statistics off).

What waits is sent:

  • when the app goes to the background, or the page is hidden or closed;
  • once 20 entries wait for one URL;
  • every 30 minutes while the game is open;
  • at launch, when anything was left from before;
  • on flush().

Each request is POST <url> with the body {"batch": [body, ...]}, the entries in the order they were posted, at most 50 and 256 KB to a request. A 2xx answer removes them. A 4xx answer (other than 408, 425 and 429) means the server will never take them, so they are dropped. Anything else (no network, a 5xx, a 429, no answer in 30 seconds) keeps them, and that URL waits 15 seconds before it is tried again, doubling each time up to 30 minutes. At most 500 entries wait for one URL; past that the oldest goes.

Where the game runsWhere entries waitHow they are sent
Mobile appDocuments/awaken-outbox.json, kept by the shell (outbox.ts), so nothing is lost when the app is closed or offlinefetch from the shell, Content-Type: application/json
Web exportthe page's localStoragefetch, or navigator.sendBeacon when the page is hidden or closes, as text/plain (a type a browser sends to another origin without asking the server first)
Editor (Play)nowherenot sent: each post is written to the Console

So a server reads the body as JSON whatever its Content-Type, and answers with Access-Control-Allow-Origin (any origin is enough; no cookies are sent) so a web page can read the status.

App events ​

The engine raises its own events on the same bus as api.emit, named awaken:.... Register for them with api.on in onStart, before any await.

EventWhenPayload
awaken:launchthe first frame after the game starts (once a Play, once a page; a scene load raises none)none
awaken:backgroundthe app went to the background, or the page was hiddennone
awaken:foregroundback from the backgroundnone
awaken:closea web page is going away (a tab closing)none
awaken:erroran error that did not stop the game{kind, message, stack?}

Each handler runs on its own: one that throws is reported against its script, and the others still hear the event.

The background comes first. Nothing runs after awaken:background (no frame, no timer), so a handler does then what must be done: end a session, save. What it posts to the outbox is sent right after, before the app can be closed. In the mobile app the order is fixed: the shell tells the page, the page raises awaken:background for the scripts, then tells the shell it is done, and the shell sends (or sends anyway 300 ms after the app left, if the page says nothing). The mobile app has no close: it goes to the background first and may be closed from there without a word.

The editor's Play raises awaken:launch and awaken:error. The background, foreground and close events come from a shipped game.

Release channel ​

A server that counts players wants to leave out your own test runs. api.app.channel says who this copy of the game is for: "release" (players: a store, a web host), "beta" (a test build: TestFlight) or "dev" (you: the editor, a build run from Xcode or Android Studio, a page on your own computer). It is read-only, and the same from the first frame to the last: each host decides it once, before the game's scripts start.

ts
if (api.app.channel !== "release") api.log("a test run: stats are marked as such");
Where the game runsreleasebetadev
iOS appfrom the App StoreTestFlight, and App Reviewthe Simulator, or a build with a provisioning profile (run from Xcode, ad hoc, enterprise)
Android appinstalled by Google Play, including its internal, closed and open testing tracksnevera debuggable build, or one installed any other way (adb, an APK by hand, another store)
Web exportanywhere elseneverlocalhost, 127.0.0.1 or [::1], a file: page (a game.html opened from disk), a blob: page (Preview in the hosted editor), or a browser driven by automation (navigator.webdriver)
Editor (Play)always

How the mobile app decides, at launch, without waiting on anything or asking the player anything:

  • iOS (AppDelegate.swift). A build with a provisioning profile (embedded.mobileprovision) is dev, and StoreKit is not asked: such a build has no app transaction from the App Store, and asking for one can show an App Store sign-in. The App Store strips the profile, so an App Store or TestFlight build is first what its receipt's name says (sandboxReceipt is beta, otherwise release); StoreKit's AppTransaction is then asked in the background and its environment has the last word (production release, sandbox beta, xcode dev). The shell reads the channel as the page first starts.
  • Android (AppChannelModule.kt). A debuggable build is dev; one Google Play installed (com.android.vending) is release; anything else is dev. Play installs a tester's copy from a testing track the same way as a player's, so Android has no beta.
  • A debug JavaScript bundle (served by Metro) is dev on both.

The shell hands the channel to the page before the page's scripts run. A mobile folder exported before channels hands it none, and the game reads release there; an update into such a folder says so in its log (see Mobile folders made by an older editor).

The Usage Stats package stamps the channel on every entry it sends.

Errors ​

awaken:error carries every error that did not stop the game, once and then at most every 10 seconds with the same text:

kindWhat went wrong
scripta script threw (in onStart, onUpdate, a timer, a handler); message names the script and the call
gputhe GPU device was lost, a GPU validation or runtime error, or the game could not start the last time (no WebGPU, no GPU, a GPU that would not start)
savea save that could not be written, or one over the 5 MB the mobile app keeps
pagean error nothing caught, or a promise nobody waited for

message is at most 1,000 characters and stack at most 2,000. A game that could not start had no script running to hear of it: the page keeps the failure and raises it at the next launch that does start. A device that can never run the game never reports.

A script sends errors to the game's server through the outbox, as the Usage Stats package does. Native crashes of the mobile app are in Xcode's Organizer (Crashes) and Google Play Console's Android vitals; the app needs no SDK for them.

Privacy ​

What a game sends is the game's to declare. In App Store Connect's App Privacy and Google Play's Data safety, say what the game's scripts collect and why. The engine adds nothing to them.

See also ​

Awaken: browser-native WebGPU game engine.