Contents
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
Gameplay event
-> Save builder
-> Persistly SDK local slot
-> Persistly SDK pending sync state
-> Persistly cloud save API
-> Remote version + slotInfoComponent Responsibilities
| Component | Responsibility | Failure Handling |
|---|---|---|
| Save builder | Converts runtime data into serializable data | Reject invalid or incomplete data |
| Persistly local slot | Persists latest save on device | Surface fatal local-storage errors |
| Persistly pending sync state | Tracks unsent or failed writes | Retry from safe lifecycle moments |
| Persistly cloud sync | Sends and loads remote saves | Return structured errors |
| Conflict resolver | Chooses or merges divergent versions | Ask 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.
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.
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.