Skip to content

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.

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.

Load with defaults

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 ?? {}) };
}

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.

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.

Merge on conflict

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 });
}

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.

Read the old key once

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;
}
}

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.

HostWhere the push goesfeatures.saves
filudi.comThe API, PUT /v1/sdk/saves/:gameId with If-Matchremote
CrazyGamesThe data module, one key filudi:save holding { v, d }; localStorage when the module is offremote or local
Playgamabridge.storage, same key and shaperemote
Poki, mocklocalStorage onlylocal