Saves
The SDK stores one save per player per game: a JSON value up to 64 kB. It writes localStorage first and
pushes to the host after, so a save never waits on the network. The reference is save;
this page is about how to design yours.
One blob
Section titled “One blob”Keep everything in one object: progress, settings, unlocks. Several keys were a habit of portal data
modules; the SDK has one slot and resolves conflicts on it as a whole. Put a schema field inside the blob
(schema: 3) and fill defaults for every field an older blob lacks when you load. That field has nothing to
do with the SDK’s version.
const defaults = () => ({ schema: 3, level: 1, stars: [], sound: true });
async function loadSave() { const read = await sdk.save.get(); version = read.version; return { ...defaults(), ...(read.data ?? {}) };}interface Save { schema: 3; level: number; stars: number[]; sound: boolean }const defaults = (): Save => ({ schema: 3, level: 1, stars: [], sound: true });
async function loadSave(): Promise<Save> { const read = await sdk.save.get<Partial<Save>>(); version = read.version; return { ...defaults(), ...(read.data ?? {}) };}The version
Section titled “The version”The SDK’s version is a counter the game increments on every write: set(data, { version: last + 1 })
where last is the version of the latest get() or saved result. Keep the writes in order: one at a
time, each with the version the previous one returned. A debounce (write at most every second) keeps the
count low and the API quiet.
Conflicts
Section titled “Conflicts”Two devices, one player. The device with the higher version wins. When the server is ahead, set() returns
conflict with the server copy: take it, merge it if your game can (keep the higher star count per level),
and write again with server.version + 1. When a newer copy arrives on its own, replaced fires: reload
your state from it. Equal versions with different content: the server wins, because another device has
seen it.
function merge(mine, theirs) { return { ...theirs, level: Math.max(mine.level, theirs.level), stars: mine.stars.map((s, i) => Math.max(s, theirs.stars[i] ?? 0)), };}
const result = await sdk.save.set(state, { version: version + 1 });if (result.status === 'conflict') { state = merge(state, result.server.data); version = result.server.version; await sdk.save.set(state, { version: version + 1 });}function merge(mine: Save, theirs: Save): Save { return { ...theirs, level: Math.max(mine.level, theirs.level), stars: mine.stars.map((s, i) => Math.max(s, theirs.stars[i] ?? 0)), };}
const result = await sdk.save.set(state, { version: version + 1 });if (result.status === 'conflict') { state = merge(state, result.server.data as Save); version = result.server.version; await sdk.save.set(state, { version: version + 1 });}Migrating an old save
Section titled “Migrating an old save”A game that moves to the SDK has players with a save under the old key: the portal’s data module, or
localStorage. Read it when the SDK has nothing, write it through the SDK, and leave the old key where it
is: a rollback to the previous build must still find it.
async function loadSave() { const read = await sdk.save.get(); version = read.version; if (read.data !== null) return { ...defaults(), ...read.data }; const legacy = readLegacySave(); // the old key, exactly as the old code read it if (legacy === null) return defaults(); await sdk.save.set(legacy, { version: version + 1 }); version += 1; return { ...defaults(), ...legacy };}
function readLegacySave() { try { const data = window.CrazyGames?.SDK?.data; const raw = data?.getItem('gloop-save') ?? localStorage.getItem('gloop-save'); return raw ? JSON.parse(raw) : null; } catch { return null; }}async function loadSave(): Promise<Save> { const read = await sdk.save.get<Partial<Save>>(); version = read.version; if (read.data !== null) return { ...defaults(), ...read.data }; const legacy = readLegacySave(); // the old key, exactly as the old code read it if (legacy === null) return defaults(); await sdk.save.set(legacy, { version: version + 1 }); version += 1; return { ...defaults(), ...legacy };}
function readLegacySave(): Partial<Save> | null { try { const data = (window as { CrazyGames?: { SDK?: { data?: { getItem(k: string): string | null } } } }) .CrazyGames?.SDK?.data; const raw = data?.getItem('gloop-save') ?? localStorage.getItem('gloop-save'); return raw ? (JSON.parse(raw) as Partial<Save>) : null; } catch { return null; }}What the hub shows
Section titled “What the hub shows”Add a _summary object at the top level of the blob and the game’s page on filudi.com draws your progress
without knowing your game: line (“World 3 of 5”, under 60 characters), progress (0 to 1), streak (days,
for a daily game, counted by you) and share (today’s result as a text grid, under 400 characters, the one
place emoji are welcome). Update it on every write.
Per host
Section titled “Per host”| Host | Where the push goes | features.saves |
|---|---|---|
| filudi.com | The API, PUT /v1/sdk/saves/:gameId with If-Match | remote |
| CrazyGames | The data module, one key filudi:save holding { v, d }; localStorage when the module is off | remote or local |
| Playgama | bridge.storage, same key and shape | remote |
| Poki, mock | localStorage only | local |