Contents
GuideJavaScript

How To Add Cloud Saves To An Idle Game Without Building A Backend

Build a practical idle-game save loop with local-first saves, safe cloud sync timing, lightweight slotInfo, and conflict-aware recovery.

Idle games look simple until save sync becomes real. Coins change constantly, upgrades unlock over time, players refresh the tab, networks fail, and offline progress has to survive the next launch.

The wrong pattern is to send every number change directly to a backend. The safer pattern is local-first:

  • load local progress before the game loop starts
  • save locally when progress changes
  • sync to the cloud at safe moments
  • keep preview data small
  • handle conflicts explicitly

Persistly's JavaScript SDK is built around that pattern. You can start with one default save using saveData and loadData, then move to named slots later if your game grows.

A Minimal Idle Save

Keep the first version boring. Store facts your game needs to resume, not every derived number on screen.

typescript
type IdleSave = {
  schemaVersion: 1;
  coins: number;
  coinsPerSecond: number;
  upgrades: Record<string, number>;
  lastSavedAt: string;
};

Good save data is small, serializable, and easy to migrate. Derived values such as temporary animation state, floating combat text, current button hover state, or cached UI labels should be rebuilt by the game.

Configure Persistly Once

Configure the SDK during boot with a stage runtime key from the Persistly dashboard.

typescript
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";

await PersistlyGameSaves.configure({
  runtimeKey: "ps_test_replace_me",
});

Use a stage key while developing. Switch to a production runtime key only when the same save flow is tested.

Load Before Gameplay Starts

Load local progress before starting timers, workers, or the main idle loop.

typescript
const defaultSave: IdleSave = {
  schemaVersion: 1,
  coins: 0,
  coinsPerSecond: 1,
  upgrades: {},
  lastSavedAt: new Date().toISOString(),
};

async function loadIdleSave(): Promise<IdleSave> {
  const loaded = await PersistlyGameSaves.shared.loadData();

  if (loaded.status === PersistlyGameSaveStatus.LocalFound && loaded.data) {
    return migrateIdleSave(loaded.data);
  }

  return defaultSave;
}

loadData reads the default autosave slot. That is enough for many small idle, incremental, and casual browser games.

Save Locally On Meaningful Changes

When the player buys an upgrade, claims offline progress, reaches a milestone, or closes a menu, save locally first.

typescript
async function saveIdleProgress(save: IdleSave) {
  const nextSave = {
    ...save,
    lastSavedAt: new Date().toISOString(),
  };

  await PersistlyGameSaves.shared.saveData(nextSave, {
    slotInfo: {
      label: "Autosave",
      coins: Math.floor(nextSave.coins),
      coinsPerSecond: nextSave.coinsPerSecond,
      lastSavedAt: nextSave.lastSavedAt,
    },
  });

  return nextSave;
}

saveData writes locally. It does not need to wait for the network before your game continues.

Use slotInfo for lightweight preview and support data. Put the full playable state in data, not in slotInfo.

Sync At Safe Moments

Cloud sync should happen at moments where a network request makes sense. Do not sync every tick.

Good sync moments:

  • after an upgrade purchase
  • after claiming offline earnings
  • when a run, wave, or milestone completes
  • when the game tab is hidden
  • when the player opens the settings or save menu
  • every few minutes if meaningful progress changed
typescript
async function syncIdleSave() {
  const result = await PersistlyGameSaves.shared.forceSyncData();

  if (result.status === PersistlyGameSaveStatus.Synced) {
    setSyncLabel("Cloud save updated");
    return;
  }

  if (result.status === PersistlyGameSaveStatus.Conflict) {
    showConflictDialog();
    return;
  }

  setSyncLabel("Saved locally. Cloud sync will retry later.");
}

For a production idle game, keep a small sync label or icon. Players do not need constant noise, but they should know whether progress is safely synced before switching devices.

What If Sync Fails?

If the network is down, the player should keep playing. The local save remains available, and the next sync attempt can run from another safe moment.

typescript
window.addEventListener("online", () => {
  void PersistlyGameSaves.shared.syncDue();
});

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    void PersistlyGameSaves.shared.syncDue();
  }
});

The important rule is simple: local progress should not disappear because one request failed.

How Conflicts Happen

Conflicts are normal once a player can use more than one browser or device.

Example:

  1. The player opens the game on a laptop.
  2. The player opens the same save later on a tablet.
  3. Both devices make progress before one sees the other's latest cloud version.
  4. The next sync needs an explicit decision.

For an idle game, the correct conflict UI depends on your design. Some games can choose the newest save. Others should show both versions and ask the player.

typescript
async function resolveWithLocalSave() {
  const result = await PersistlyGameSaves.shared.overwriteCloudData("autosave");

  if (result.status === PersistlyGameSaveStatus.Synced) {
    setSyncLabel("Local save kept");
  }
}

Do not silently overwrite cloud progress unless your game design is comfortable with that policy.

One Save Or Multiple Slots?

Start with saveData, loadData, and forceSyncData when your game has one main save.

Use named slots when your game has:

  • multiple characters
  • separate campaigns
  • manual save slots
  • different challenge runs
  • shared account data plus character saves
typescript
await PersistlyGameSaves.shared.saveSlot("mage-run", saveData, {
  slotInfo: {
    characterName: "Astra",
    level: 12,
    lastSavedAt: saveData.lastSavedAt,
  },
});

The mental model stays the same. saveData is the default autosave path. saveSlot is the named-slot path.

What Not To Store

Idle games often include currencies, upgrades, and timers, so it is tempting to put everything in the client save. Be careful.

Do not store these as trusted facts in a client-writable save:

  • payment state
  • receipts
  • server-authoritative inventory
  • anti-cheat decisions
  • leaderboard scores that must be trusted
  • admin flags
  • secrets, API keys, or auth tokens

Persistly is cloud save infrastructure. It is not an authoritative economy server. If a value decides real money, competitive ranking, or fraud-sensitive rewards, keep that decision on a trusted backend.

Practical Checklist

  • Use loadData before starting the idle loop.
  • Use saveData after meaningful progress changes.
  • Use slotInfo for small preview fields only.
  • Call syncDue or forceSyncData at safe moments.
  • Show a small pending/synced/offline status.
  • Add conflict handling before shipping cross-device saves.
  • Keep save payloads small and versioned.
  • Do not sync every tick.

Next Steps

If you want the smallest possible browser demo, try the one-file jsDelivr guide. If you are building with npm or Vite, use the JavaScript SDK install guide. If you want the architecture behind the pattern, read the offline-first save guide.