Skip to content

Save and resume progress ​

Use ctx.saves to keep game progress for the current player. The host binds requests to its authenticated player, keeps a device checkpoint and synchronizes it with the save service. Games never receive login credentials and never pass a player ID. Google and Apple sign-in are host responsibilities.

This API requires SDK 0.3.0 and a host advertising init.saves.version: 1. Check ctx.saves.capabilities inside load. Old hosts and standalone pages report no capability; requests reject with unsupported. Keep ctx.store for device preferences. It does not become account storage when a player signs in.

Restore before play ​

The checkpoint example contains an executable adapter for a turn-based counter. It validates a snapshot before replacing live state, serializes checkpoints, and loads a selected manual slot into the autosave journey without overwriting that manual slot.

js
import { openCheckpoint } from './checkpoint.js';

let checkpoint;
const callbacks = {
  async load(ctx) {
    if (!ctx.saves.capabilities) throw new Error('This game needs a host with managed saves.');
    checkpoint = await openCheckpoint(ctx.saves);
  },
  update(dt, ctx) {
    // After a settled game action, call checkpoint.advance() and handle failure.
    // Keep input held until the returned promise confirms the local checkpoint.
  },
};

The host supplies ctx.saves.intent = { mode: 'continue', slot: 'auto' }, or a selected slot when the player chooses Continue in the progress sheet. A snapshot contains game rules and resumable event cursors. Rebuild Three.js objects, audio and asset data from your content. Keep previews and unconfirmed actions out of a committed snapshot.

start remains synchronous and resets SDK run clocks and score. resume unpauses the current document; it does not load a save. Preserve campaign state in your game when starting another visit. See lifecycle. This API does not change the manifest's arcade acceptance rules.

Snapshot and limits ​

json
{
  "schemaVersion": 1,
  "contentVersion": "tower-1",
  "label": "Autosave",
  "summary": "Floor 12 · 140 HP · 3 keys",
  "payload": { "floor": 12, "hp": 140, "keys": 3 }
}

schemaVersion is an integer from 1 through 2,147,483,647; contentVersion is a nonempty string of at most 100 characters. payload is JSON data. Strings must contain valid Unicode and cannot contain null characters. Optional label and summary are plain text, at most 80 and 160 characters. Do not put tokens, personal information or rendering assets in them. Validate and migrate game data yourself; unsupported content must show a recovery choice, never silently replace an existing campaign.

Slots contain 1–64 ASCII letters, digits, underscores or hyphens and start with a letter or digit. The host supports 32 slot names per game and snapshots up to 256 KiB of UTF-8 JSON. The service keeps one previous snapshot. The player quota is 16 MiB including previous versions, across at most 100 games. Deleting a slot clears both payloads but retains its name and revision fence; reuse that slot for a new journey.

Methods and results ​

MethodResult
list()Current game's visible save summaries; no snapshot payloads
read(slot)Full save or null if the slot never existed
commit(slot, snapshot, { expectedRevision })Checkpoint receipt after device persistence
remove(slot, { expectedRevision })Confirmed server deletion; requires connectivity and no pending upload
restore(slot, { expectedRevision })Previous server snapshot becomes current; requires connectivity and no pending upload
flush(){ pending, conflicts }; attempts eligible queued writes and reports remaining work

A save has gameId, slot, revision, updatedAt (Unix milliseconds), bytes, label, summary, deleted, hasPrevious and sync. A full save also has snapshot. After deletion, read returns a record with deleted: true and snapshot: null. Use its revision to explicitly reuse the slot.

Supply the opaque revision from the most recent read or commit. null means create only if the slot has never existed. Do not compare revisions, invent them, or replace them with timestamps. A cloud acknowledgement does not invalidate the revision already returned to your game.

syncWhat to tell the player
pendingSaved on this device; waiting to sync
syncedConfirmed by the save service; guests still need sign-in for access on other devices
offlineShowing the last device copy; cloud freshness could not be checked
conflictAnother writer changed the cloud save; choose a version in the progress sheet

Unattempted checkpoints in one slot may be coalesced to its latest state. An attempted upload retains its exact operation ID and body until its outcome is known. Checkpoint after settled commands, not render frames. Commit promises must be handled: a device quota failure is not a successful save. A sudden process termination can lose work whose checkpoint receipt has not arrived. Do not rely on asynchronous work in dispose.

The host owns the upload after accepting a checkpoint, even when the game iframe is removed. Uploads retry when connectivity returns or the player chooses Retry sync. A conflict keeps device and cloud data separate; the player chooses which progress to keep without an automatic overwrite. Device storage can be removed by the user or browser; pending data has no cloud guarantee. A fresh offline launch may be unable to verify the player session.

Errors and player controls ​

SaveError has a stable code. Handle unsupported, capture-readonly, invalid-slot, invalid-snapshot, expected-revision-required, save-too-large, save-quota, save-limit, save-conflict, device-unavailable, unauthorized, session-changed, unavailable, busy, timeout and disposed. A timeout does not prove that a write failed; read the slot before retrying a conditional write. Never turn a read failure into a new empty game.

Capture mode cannot access player saves. Use deterministic fixture state when ctx.capture is true. Sign-in changes require a fresh game/storage session; queued data remains scoped to its original player. Guest upgrade to a new account retains the same player. Signing into an existing different account does not implicitly merge guest or legacy localStorage data.

Web and iOS players tap the save icon above Like to open the current game's progress sheet. It shows checkpoint and sync state with Continue as the primary action. A secondary menu offers previous-version recovery and confirmed deletion; version choices appear only for conflicts. Bookmarks and progress are separate. Restoring server data cannot guarantee compatibility with a game's current content; your loader must still validate it.

Adopt managed saves without losing local progress ​

Keep existing ctx.store campaign keys intact when you add ctx.saves. Those keys belong to a browser, not a verified player. Do not upload them automatically on sign-in, and do not mirror managed checkpoints back into them: another account may use the same device.

  1. In load, select managed storage only when ctx.saves.capabilities is present. For an older host or standalone build, keep your existing local-only adapter and label it as such. A failed managed read is an error, not permission to switch storage or start an empty campaign.
  2. Read the selected ctx.saves.intent.slot and validate the envelope and game payload before play. Read auto as well when continuing a manual checkpoint, so the next autosave uses its current revision. Keep separate revisions for each slot. A deleted record is a revision fence, not a never-used slot.
  3. Offer an explicit Import local save action with a preview and confirmation. Explain that the local file is not account-bound and importing replaces the selected current player's slot. Validate the local payload and commit it using the revision the player reviewed. Leave the original local keys and backups unchanged, including after a successful import. Do not persist an account-independent “already imported” flag or repeat the import when an account changes.
  4. Serialize settled game actions and freeze their snapshot before awaiting commit. Display a device checkpoint receipt separately from cloud synchronization. Hold further game actions while the local write is pending; on failure retain the live state and provide retry. After an uncertain timeout, read and reconcile the slot before retrying. Never refresh a revision merely to overwrite a conflicting save.
  5. For captures, bypass both local and managed campaign storage and load deterministic fixtures.

For a manual-slot import, the operation after confirmation can be as small as:

js
// reviewedRevision came from the slot displayed in the confirmation UI.
const game = validateAndRestore(localEntry.snapshot);
const receipt = await ctx.saves.commit('manual', {
  schemaVersion: 1,
  contentVersion: game.contentVersion,
  label: 'Save 1',
  summary: `Floor ${game.floor} · ${game.hp} HP`,
  payload: serializeGame(game),
}, { expectedRevision: reviewedRevision });
showSaveStatus(receipt.sync);
// Keep localEntry and its browser backup available.

Only the game can validate its payload or translate its older save formats. The SDK deliberately does not infer ownership or merge campaigns. Use independent manual slots and an autosave slot so continuing one checkpoint does not replace another.

Host transport ​

The optional v1 service shares the established game port but has a separate bounded message envelope: { t: 'save:request', version: 1, requestId, action, slot?, snapshot?, expectedRevision? }. Actions are list, read, commit, remove, restore, flush. The host replies with { t: 'save:result', version: 1, requestId, ok: true, value } or ok: false, error. There are at most 16 pending requests, each bounded to 270,336 bytes and a 30-second SDK timeout. The host derives the game ID from its mounted catalogue entry, checks the source/port, and selects the player from its session. Never accept a game-supplied owner, URL or credential. Lifecycle messages keep their 4 KiB limit.

Advertise { version: 1, maxBytesPerSlot: 262144, maxSlots: 32 } in init.saves only with the service adapter. Deliver save:result directly to the service without lifecycle acknowledgements. Reject requests from disposed documents, keep already accepted journal writes independent of document lifetime, and ignore stale replies.

Wire protocol 2 · Manifest 2 · SDK runtime 0.3.0