Contents
ArchitecturePlatform

Designing Offline-First Save Systems

Learn how to design save systems that keep progress safe during disconnects, retries, and cross-device play.

Offline-first save systems start from a simple promise: player progress should not disappear because a request failed. Cloud sync is still important, but it should not be the only copy of a save. Persistly SDK facades apply this pattern directly: saveData, save_data, SaveDataAsync, and named-slot variants write local slot data first, then sync when the game asks.

Core Principles

  • Local writes are the source of immediate continuity.
  • Cloud writes are backup and cross-device transport.
  • Sync status is visible enough for players to trust.
  • Conflicts are expected, not treated as impossible edge cases.

Offline-first does not mean cloud-optional. It means the game remains usable while the cloud path catches up.

Reference Architecture

txt
Gameplay event
  -> Save builder
  -> Persistly SDK local slot
  -> Persistly SDK pending sync state
  -> Persistly cloud save API
  -> Remote version + slotInfo

Component Responsibilities

ComponentResponsibilityFailure Handling
Save builderConverts runtime data into serializable dataReject invalid or incomplete data
Persistly local slotPersists latest save on deviceSurface fatal local-storage errors
Persistly pending sync stateTracks unsent or failed writesRetry from safe lifecycle moments
Persistly cloud syncSends and loads remote savesReturn structured errors
Conflict resolverChooses or merges divergent versionsAsk player when policy is unsafe

Do Not Send Every Local Write

Instead of calling cloud sync directly from every game system, save through the SDK facade and let the SDK keep the latest local draft. Then call syncDue, forceSyncData, force_sync_data, or ForceSyncDataAsync from controlled moments such as checkpoints, pause, reconnect, or app backgrounding.

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

await PersistlyGameSaves.shared.saveData(gameState);

// Later, from a safe lifecycle moment.
await PersistlyGameSaves.shared.syncDue();

Retry Policy

Retries should be persistent and bounded. An app restart should not erase the latest local save. With Persistly, keep calling the SDK sync method from safe moments and show a pending state when sync is not complete.

Practical Defaults

  • Retry soon after transient failures from lifecycle/network events.
  • Use exponential backoff after repeated failures.
  • Pause retries while offline if the platform exposes network status.
  • Keep latest save per slot if older local drafts are superseded.
  • Show a warning only when unsynced progress matters to the player.
typescript
function nextRetryDelay(attemptCount: number) {
  const base = 1_000;
  const max = 60_000;
  return Math.min(base * 2 ** attemptCount, max);
}

Sync SlotInfo

Every save should carry enough slotInfo to compare versions safely.

Useful Fields

  • slotId: stable identifier for the named save slot, passed as the slot argument in SDK calls.
  • deviceId: identifies where the write came from.
  • localVersion: unique version generated by the client.
  • remoteVersion: version returned by the cloud service.
  • updatedAt: user-facing recency hint, not the only authority.

Common Pitfalls

Treating Timestamps As Perfect

Device clocks can be wrong. Use timestamps for display and heuristics, but prefer server-issued versions or explicit revision numbers for overwrite protection.

Saving Too Much

Large payloads make retries slower and conflicts harder. Persist gameplay facts, then reconstruct derived data.

Hiding Sync Data Completely

Players do not need constant noise, but they should know when progress has not reached the cloud before signing out, switching devices, or uninstalling.

Production Checklist

  • Save through the SDK before attempting network sync.
  • Confirm local SDK state survives app/browser restarts.
  • Dedupe superseded local drafts per slot.
  • Track local and remote versions.
  • Test airplane mode, captive portals, and forced process termination.