Skip to content
Browse Docs and Help
how-to

Persist progress and migrate save data

Use host storage and handle unavailable or incompatible saves.

On this page

Read and validate

import llvls from '@llvls/sdk';

await llvls.init();
const value = await llvls.save.get('best');
const best = typeof value === 'number' && Number.isFinite(value) ? value : 0;
void best;

The generic parameter of save.get<T>() is not runtime validation. Check loaded values before trusting their shape. The fragment omits error UI; production code must handle failed reads.

Write deliberately

import llvls from '@llvls/sdk';

async function keepBest(best: number): Promise<boolean> {
  try {
    await llvls.save.set('best', best);
    return true;
  } catch (error: unknown) {
    const code = error instanceof Error && 'code' in error ? error.code : 'unknown';
    console.warn('Save did not finish', code);
    return false;
  }
}
void keepBest;

Call after initialisation. Report success only after the promise resolves. Save at meaningful checkpoints instead of on every animation frame. Do not silently turn a failed signed-in save into a claim that progress is synced.

Storage modes

Signed-in saves belong to the account and game. Signed-out saves are held by the host on the device. On first sign-in, device data moves only if the account has no save for that game; otherwise the account save wins. Preview saves are temporary and are not account progress.

Migration

Compare storedSaveSchema and currentSaveSchema from init. Handle no save, known older formats and unsupported newer data explicitly. Validate and transform data before writing it. A schema bump alone performs no migration.

Rolling back a release can make newer saves incompatible. Test your migration with sample data from each supported older format and with malformed values. Use copies of your own test data, never real players’ private saves.

Use save.keys() and save.remove() for deliberate cleanup. Never use player names as save keys or access browser storage directly.