Contents
SDK exampleJavaScript

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, or syncDue from safe lifecycle moments with visible status when needed.

Example Save Shape

Keep save data boring. Prefer JSON-compatible primitives and clear version fields.

typescript
type SaveData = {
  schemaVersion: 1;
  player: {
    level: number;
    xp: number;
    coins: number;
  };
  inventory: Array<{
    id: string;
    quantity: number;
  }>;
  updatedAt: string;
};
FieldPurpose
schemaVersionLets future code migrate older saves safely.
updatedAtHelps players and support tools understand recency.
slotIdIdentifies this logical game save in your own UI or support tools.
syncStateDrives 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.

typescript
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.

typescript
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.