Anonymous browser idle games: load local first, save locally often, sync at safe checkpoints.
JavaScript Game Scenarios
Use Persistly through real game lifecycle flows.
These scenarios show where Persistly fits in browser games, signed-in games, cross-browser restore, shared accountData, and safe cleanup flows.
Fit
Persistly works best when the game is local-first and syncs intentionally.
The SDK should not feel like raw API plumbing. Think in terms of boot, save locally, sync safely, resolve conflict, and continue.
Signed-in games: store accountId and accountSessionToken in your trusted backend after first sync.
Another browser/device: attach the saved Persistly account session before listing or loading slots.
Multiple slots: use named slots and keep slot select screen data in slotInfo.
Shared account state: use accountData for bundles, settings, shared inventory, and validated currency balances.
Offline play: keep local state dirty and sync later; conflicts are normal when another device changed cloud state.
Boot
Load local state first so the game starts fast.
Do not block the main menu on a cloud request unless your game specifically needs a fresh cloud check before allowing play.
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
});
const slot = await PersistlyGameSaves.shared.loadData();
if (slot.status === PersistlyGameSaveStatus.LocalFound && slot.data) {
startGameFromData(slot.data);
} else {
startNewGame();
}
// Do not block boot on cloud unless your game needs that.
void PersistlyGameSaves.shared.syncDue();Cross-Browser Restore
Signed-in recovery needs your backend to store the Persistly account session.
An anonymous localStorage save cannot recover on a new browser by itself. Your account backend must return the saved accountId and accountSessionToken.
// Browser A: player is signed in to your game account.
await PersistlyGameSaves.shared.forceSyncData();
const session = await PersistlyGameSaves.shared.getAccountSession({
includeToken: true,
});
await myBackend.savePersistlyLink({
userId: currentUser.id,
accountId: session.accountId,
accountSessionToken: session.accountSessionToken,
});
// Browser B: same player signs in later.
const link = await myBackend.getPersistlyLinkForCurrentUser();
await PersistlyGameSaves.shared.attachAccount({
accountId: link.accountId,
accountSessionToken: link.accountSessionToken,
});
const slots = await PersistlyGameSaves.shared.listSlots();
await PersistlyGameSaves.shared.refreshSlot("autosave");
const slot = await PersistlyGameSaves.shared.loadData();Account Data
Use accountData for shared state across every slot.
Good examples: bundles, unlocked slots, shared inventory, settings, and premium currency balances after trusted purchase validation. Use saveAccountData, patchAccountData, and account-data inspection helpers instead of storing shared data in a slot.
Shared Account State
Patch account-wide state, then sync the account.
Document patch behavior in your game code. Persistly treats accountData separately from slot data.
await PersistlyGameSaves.shared.patchAccountData({
diamonds: 200,
bundles: {
starter: true,
founder: true,
},
sharedInventory: [1, 25, 35],
});
await PersistlyGameSaves.shared.forceSyncAccount();Lifecycle
Sync at safe browser lifecycle moments.
Local save should happen before risky lifecycle moments. Async cloud sync during tab close is not guaranteed, so also sync on checkpoints and reconnect.
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
void PersistlyGameSaves.shared.forceSyncData();
}
});
window.addEventListener("online", () => {
void PersistlyGameSaves.shared.syncDue();
});
async function onCheckpointReached(data: GameData) {
await PersistlyGameSaves.shared.saveData(data);
await PersistlyGameSaves.shared.forceSyncData();
}Result Shapes
Build UI from statuses instead of guessing.
Use exported status constants in code. These shapes show the data your UI normally cares about: local saved, synced, and conflict.
// saveData/saveSlot: local write finished. Cloud sync may still be pending.
{
"status": "local_saved",
"slotId": "autosave"
}
// forceSyncData/forceSync: cloud accepted current local data.
{
"status": "synced",
"slotId": "autosave",
"version": 12,
"updatedAt": "2026-05-20T12:00:00.000Z"
}
// forceSyncData/forceSync: cloud has a different cloud branch.
{
"status": "conflict",
"slotId": "autosave",
"localData": { "level": 6 },
"cloudData": { "level": 7 },
"cloudVersion": 13
}Lifecycle Cleanup
Keep logout, local cache clear, archive, and permanent delete separate.
One wrong delete integration can erase player progress. Treat local cleanup and remote erasure as different product actions.
clearLocalAccount(): local logout/switch player. Cloud account and slots stay intact.
clearLocalSlot(slotId): remove local cache for one slot only. Cloud data stays intact.
archiveSlot(slotId): retire a synced slot but keep remote history/loadability.
deleteSlot(slotId): permanent slot erasure.
deleteAccount(): permanent account erasure, including synced slots under that account.