Save data
Persist per-player saves and per-game shared data — without writing backend code.
If your game needs to remember things between sessions — high scores, unlocked levels, settings, progress — you don't need to set up a backend. umicat ships a save-data system the agent wires into your game with a few requests.
There are two save surfaces:
- Per-player saves (
saves.*) — each signed-in player has their own data per game. Anonymous players save to localStorage automatically. - Per-game shared data (
gameData.*) — one shared bucket per game, visible to all players. Public reads, and writes from any signed-in player — see the warning below before you trust it with anything.
When to use which
| Need | Use |
|---|---|
| Player's high score, progress, settings | Per-player saves |
| Save slots / "load game" UI | Per-player saves |
| Game-wide leaderboard or daily challenge | Per-game shared data |
| Asset paths the game references at runtime | Per-game shared data |
| Anything you set that every player reads | Per-game shared data |
| Anything that must be tamper-proof | Neither — there's no authoritative server |
Asking the agent
You don't write any save code yourself. Ask:
When the player dies, save their final score as the player's high score.
When the game starts, load the saved high score and show it in the HUD.The agent will:
- Pick a save key (e.g.
'highScore'). - Wire a load on scene start.
- Wire a save on the death event.
- Update the HUD widget to read the loaded value.
You'll see the agent's tool calls in the activity strip. On reload, the game persists.
Per-player saves — the contract
The agent has access to a saves skill with three operations:
| Operation | What it does |
|---|---|
saves.get(key) | Read a value for the current player. Returns null if not set. |
saves.set(key, value) | Write a value for the current player. Any JSON-serializable shape. |
saves.delete(key) | Remove a value. |
Values can be any JSON: numbers, strings, objects, arrays. Limits are enforced: 100 KB per value, 1 MB per player per game, and 64 keys — plus a version number per key, so a write can say "only if nothing changed since I read it" and be told if it lost the race.
Signed in vs anonymous
- Signed in: saves go to the umicat backend, keyed by player account + game. Survives device changes — the player's progress travels with their account.
- Anonymous: the SDK's local transport stores saves in the browser's
localStorage. Player keeps progress on this device only. If they later sign in, anonymous saves do not auto-migrate (yet).
Both paths use the same saves.get/set/delete API — the agent writes
code that doesn't know or care which transport is active.
Per-game shared data — the contract
For game-wide values readable by all players — a daily seed, a shared config, a scoreboard:
| Operation | Who can call | What it does |
|---|---|---|
gameData.get(key) | Anyone — public read | Read a value for the game. |
gameData.list() | Anyone — public read | List the game's keys. |
gameData.set(key, value) | Any signed-in player | Write a value. |
gameData.delete(key) | Any signed-in player | Remove a value. |
Read that table again: shared game data is writable by any signed-in player, not just by you. There is no server-side rule about what a value may contain — a write replaces the whole value. So a leaderboard stored here is a leaderboard any player can rewrite, and the platform will not stop them.
Use it for things where that is acceptable: a daily seed, a shared config you set, a friendly high-score list. Do not use it for anything that needs to be trustworthy — there is no authoritative server to enforce it. The same caveat applies to multiplayer.
Because any player can write, a read-modify-write (append my score, keep everyone else's, truncate the list) can collide with another player doing the same thing. That's what the per-key version is for: write with the version you read, and retry if you're told it moved.
Common uses:
- Daily challenge seed. Owner sets
gameData.set('dailyChallenge', { seed: '2026-05-24', enemies: [...] }). Every player reads it on start. - Public leaderboard. Any player appends their own entry — read the list, add yourself, write it back with the version you read, retry on conflict.
- Game-internal config the agent might tune without rebuilding — difficulty curves, item drop tables, level definitions.
Example flow — high-score persistence
Tell the agent:
Remember the player's all-time high score. Show "High score: X" in the
HUD top-right. When the player dies, compare current score to the high
score and update if it's higher.Behind the scenes the agent writes something like:
// On scene start
const highScore = (await saves.get('highScore')) ?? 0;
scene.registry.set('highScore', highScore);
// On player death
const finalScore = scene.registry.get('score');
const currentHigh = scene.registry.get('highScore');
if (finalScore > currentHigh) {
await saves.set('highScore', finalScore);
scene.registry.set('highScore', finalScore);
}The HUD's high-score text widget is bound to registry 'highScore', so
it updates the moment the registry value changes.
Example flow — settings persistence
Add a settings menu accessible from a gear icon in the HUD. Sliders for
master volume (0-100), music volume (0-100), and a toggle for sound
effects. Persist the settings across sessions.Agent wires:
const settings = (await saves.get('settings')) ?? {
masterVolume: 80,
musicVolume: 60,
sfxEnabled: true,
};
// ... apply settings ...
// On change in the settings UI
await saves.set('settings', settings);Anonymous-friendly games
If your game's design assumes players can play without signing up (arcade-style, embedded, one-time experiences), the localStorage fallback gives them a save that works for them — they just lose it if they clear their browser data or switch devices.
If your game's design requires identity (cross-device progress, leaderboards, real names), tell the agent:
Require sign-in to play. Show a "Sign in to play" splash if the player
is anonymous.The agent reads the user from the SDK handshake and gates accordingly.
What about save slots?
For "Save 1 / Save 2 / Save 3" UI, use namespaced keys:
saves.set('slot1', { progress: ... })
saves.set('slot2', { progress: ... })
saves.set('slot3', { progress: ... })The agent can wire a save-slot menu that lists the player's keys, filters for the slot prefix, and lets them overwrite any slot.
Limits and guarantees
- Size: 100 KB per value, 1 MB per player per game, 64 keys. The same limits apply per game to shared game data.
- Key names: letters, digits,
.,_and-, starting with a letter or digit, up to 64 characters. - Durability: signed-in saves are stored server-side. localStorage saves live on the player's device only, and vanish if they clear their browser.
- Privacy: per-player saves are visible only to that player — not to
you, the game owner. If you need to see it, it has to go through
gameData.*, which is public.
Security model
Save calls never pass through the running game code. The game iframe posts an RPC to umicat's host, which validates the call against the player's session and only then hits the backend. Tokens never reach your game code.
This means you don't have to (and shouldn't) try to authenticate things yourself in game logic — trust the SDK's API surface.