Contents
Basic Save Sync With The JavaScript SDK
Build a small browser-game save loop with PersistlyGameSaves, local data, cloud sync, and recoverable errors.
Browser games need save logic that feels instant even when network conditions are not ideal. PersistlyGameSaves writes local data first, then syncs to Persistly when your game reaches a safe network moment.
This guide uses the public @persistlyapp/sdk package and the high-level facade. It starts with the one-save saveData and loadData path. Use saveSlot(slotId) and loadSlot(slotId) when the game has manual saves, campaigns, or multiple slots. Use PersistlyClient only when you intentionally need the lower-level runtime contract.
What This Pattern Covers
- Local-first writes for responsive UI
- Cloud sync after each meaningful checkpoint
- SlotInfo preview data for menus and future conflict handling
- Small, serializable payloads that are easy to inspect
Production note: do not block core gameplay on a network round trip. Save locally first, then call
forceSyncData,forceSync, orsyncDuefrom safe lifecycle moments with visible status when needed.
Example Save Shape
Keep save data boring. Prefer JSON-compatible primitives and clear version fields.
type SaveData = {
schemaVersion: 1;
player: {
level: number;
xp: number;
coins: number;
};
inventory: Array<{
id: string;
quantity: number;
}>;
updatedAt: string;
};Recommended SlotInfo
| Field | Purpose |
|---|---|
schemaVersion | Lets future code migrate older saves safely. |
updatedAt | Helps players and support tools understand recency. |
slotId | Identifies this logical game save in your own UI or support tools. |
syncState | Drives UI labels such as saved, syncing, or offline. |
Basic Sync Flow
Configure once at boot, load local data first, then save and sync at meaningful moments.
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
});
function buildSaveData(gameState: GameState): SaveData {
return {
schemaVersion: 1,
player: {
level: gameState.player.level,
xp: gameState.player.xp,
coins: gameState.wallet.coins,
},
inventory: gameState.inventory.items.map((item) => ({
id: item.id,
quantity: item.quantity,
})),
updatedAt: new Date().toISOString(),
};
}
async function bootGame() {
const loaded = await PersistlyGameSaves.shared.loadData();
if (loaded.status === PersistlyGameSaveStatus.LocalFound && loaded.data) {
startGameFromState(loaded.data as SaveData);
} else {
startNewGame();
}
}
async function saveCheckpoint(gameState: GameState) {
const saveData = buildSaveData(gameState);
// Local write first. This does not need a network request.
await PersistlyGameSaves.shared.saveData(saveData, {
slotInfo: {
checkpoint: saveData.player.level,
updatedAt: saveData.updatedAt,
},
});
setSyncStatus("syncing");
const sync = await PersistlyGameSaves.shared.forceSyncData();
if (sync.status === PersistlyGameSaveStatus.Synced) {
setSyncStatus("saved");
} else if (sync.status === PersistlyGameSaveStatus.Conflict) {
showConflictUi("autosave");
} else {
setSyncStatus("saved-locally");
}
}When To Save
Saving on every frame is noisy and expensive. Saving only on quit risks data loss. Most games need a few meaningful triggers.
Good Save Triggers
- Level completed
- Inventory changed
- Currency balance changed
- Settings changed
- Player returns to menu
- Timed autosave every few minutes
Avoid These Triggers
- Every render tick
- Every physics update
- Every cursor movement
- Any event with large transient data
Loading Existing Saves
Load local data first so the game starts quickly. If your game needs to check cloud data on boot, ask Persistly to refresh the slot after local boot.
async function loadGameData() {
const local = await PersistlyGameSaves.shared.loadData();
if (local.status === PersistlyGameSaveStatus.LocalFound && local.data) {
applySave(local.data as SaveData);
}
// Optional: refresh from Persistly when network is available.
await PersistlyGameSaves.shared.refreshSlot("autosave");
const refreshed = await PersistlyGameSaves.shared.loadData();
if (refreshed.data) {
applySave(refreshed.data as SaveData);
}
}loadData and saveData use the default autosave slot. If your game needs more than one save, keep the same structure and pass stable slot keys to loadSlot, saveSlot, and forceSync.
Production Checklist
- Validate loaded JSON before applying it to game data.
- Keep save payloads small and avoid logs, screenshots, or replay buffers.
- Treat remote save data as untrusted input.
- Add a visible offline or sync-pending indicator.
- Record enough slotInfo preview data to debug player support requests.
Next Step
Once basic save sync works, add conflict handling with acceptCloudData, overwriteCloudData, or keepLocalDataForLater. Use the slot-specific helpers only after your game adds manual saves or multiple slots.