---
title: "Cloud saves"
description: "A save is a JSON document stored per user (called players in the API) under a slot key (`slot1`, `profile`, `settings`…): a document, a user's settings, a draft, a game's progress."
url: "/docs/guides/saves/"
lang: en
index: "/llms.txt"
---
# Cloud saves

A save is a JSON document stored per user (called *players* in the API) under a **slot key** (`slot1`, `profile`,
`settings`…): a document, a user's settings, a draft, a game's progress. Saves follow the user to every device they sign in on, and each slot carries a **version** so two devices cannot silently
overwrite each other.

## Read and write

```js
const save = await kumo.saves.get('slot1');        // null if the slot does not exist yet
const state = save?.data ?? { level: 1, hp: 3 };

state.level += 1;
await kumo.saves.set('slot1', state);              // last write wins
```

| Method | Returns |
|---|---|
| `saves.get(key)` | `{ key, version, size, updatedAt, data }` or `null` |
| `saves.set(key, data, { ifVersion? })` | `{ key, version, size, updatedAt }` |
| `saves.list()` | `[{ key, version, size, updatedAt }]` (no data) |
| `saves.delete(key, { ifVersion? })` | — |

## Conflict-safe writes

The simple form overwrites. When a user might use two devices at once, write with `ifVersion`:

```js
try {
  await kumo.saves.set('profile', profile, { ifVersion: 'latest' });
} catch (e) {
  if (e.code !== 'version_conflict') throw e;
  const cloud = await kumo.saves.get('profile');   // someone else wrote first
  profile = merge(cloud.data, profile);            // your app decides how to merge
  await kumo.saves.set('profile', profile, { ifVersion: cloud.version });
}
```

- `ifVersion: 'latest'` — the version this SDK instance last saw for that slot (0 if it never read it)
- `ifVersion: <number>` — write only if the stored version equals it; `0` means "only if the slot does not exist"
- On mismatch the server answers **409 `version_conflict`** with `details.currentVersion`

## Limits

| Limit | Value |
|---|---|
| Slots per user | 32 (`too_many_saves`) |
| Size per slot | 256 KB of JSON (`save_too_large`, HTTP 413, with `details.size` and `details.limit`). The SDK counts the bytes before sending and throws the same error without sending; a raw HTTP body over 1 MB gets 413 `invalid_request` |
| Slot key | lowercase snake_case: starts with a letter, then `a-z`, `0-9`, `_`; up to 48 characters |

Saves are scoped to the environment: development saves never appear in production.

## Saves or your own database?

The user's own browser writes saves, so a modified page can put anything in them. Pick by who may change the data:

**Protect in D1, not in saves.** Anything a user must not be able to change themselves — scores and rankings, coins, credits, items, purchases, badges, anything shared between users — goes in the project's own Functions + D1. `saves` holds only what the user may freely write (settings, drafts, a solo game's progress).

| Put it in | What | Who writes it |
|---|---|---|
| **Saves** | what the user may change freely: settings, progress notes, drafts, favorites, a game's save | the user's browser (a changed value hurts nobody) |
| **Your database** in [Functions](/docs/guides/functions/index.md) (D1) | what must be protected: scores and rankings, coins, credits, items, purchases, a verified badge, anything shared between users | only your server code |

When unsure: if a user changing it by hand would be a problem, keep it in your database. Saves hold *state*, not
*currency*. Your server code cannot read or write a user's saves, so keep the protected copy in your database from the start.
