Contents
Handling Save Conflicts Without Losing Progress
Compare practical strategies for resolving divergent local and cloud saves in games.
Save conflicts happen when two versions of the same save change independently. A player may use two devices, play offline, or restore from an older local copy. The goal is not to prevent every conflict; the goal is to resolve them without silently destroying valuable progress.
Conflict Signals
You usually have a conflict when a local write targets a remote version that is no longer current.
type SaveVersion = {
slotId: string;
deviceId: string;
localVersion: string;
remoteVersion?: string;
updatedAt: string;
};
type Conflict = {
slotId: string;
local: SaveVersion;
remote: SaveVersion;
};Resolution Strategies
| Strategy | Best For | Risk |
|---|---|---|
| Last-write-wins | Low-value settings or cosmetic changes | Can erase better progress silently |
| Player choice | Campaign saves and visible progression | Adds UX friction |
| Field merge | Independent fields such as settings | Requires careful domain rules |
| Server authority | Economy, multiplayer, competitive data | Needs backend-owned truth |
Do not use last-write-wins for high-value progression unless losing the older branch is acceptable and clearly understood.
Player Choice Pattern
For campaign progress, let the player choose between versions. Show concrete facts instead of raw IDs.
Useful Comparison Fields
- Last played time
- Device name or platform
- Level or checkpoint
- Playtime
- Currency and inventory summary
- Warning when one copy is not fully synced
function describeSave(save: PlayerSaveSummary) {
return {
title: `Level ${save.level} at ${save.checkpointName}`,
subtitle: `${save.playtimeMinutes} minutes played`,
detail: `Updated ${formatRelativeTime(save.updatedAt)} on ${save.deviceLabel}`,
};
}Field Merge Pattern
Some data can be merged safely because fields do not compete.
function mergeSettings(local: SettingsSave, remote: SettingsSave): SettingsSave {
return {
schemaVersion: 1,
updatedAt: new Date().toISOString(),
audio: local.audio.updatedAt > remote.audio.updatedAt ? local.audio : remote.audio,
controls:
local.controls.updatedAt > remote.controls.updatedAt
? local.controls
: remote.controls,
accessibility:
local.accessibility.updatedAt > remote.accessibility.updatedAt
? local.accessibility
: remote.accessibility,
};
}When Merge Is Unsafe
Avoid automatic merge when fields depend on each other. Inventory, quest progression, crafting, and premium currency often need transactional rules.
Server Version Pattern
Remote versions help prevent accidental overwrites.
For one-save games that use saveData and loadData, keep conflict handling on the same default-data facade:
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
async function syncOneSaveWithConflictRecovery() {
const result = await PersistlyGameSaves.shared.forceSyncData();
if (result.status !== PersistlyGameSaveStatus.Conflict) {
return result;
}
const save = await PersistlyGameSaves.shared.loadData();
const choice = await askPlayerHowToRecover({
local: save.data,
cloud: save.lastCloudData,
});
if (choice === "use-cloud") {
return PersistlyGameSaves.shared.acceptCloudData();
}
if (choice === "keep-local") {
return PersistlyGameSaves.shared.overwriteCloudData();
}
return PersistlyGameSaves.shared.keepLocalDataForLater();
}For games with manual saves, campaigns, or multiple slots, use the slot-specific version:
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
async function syncWithConflictRecovery(slotId: string) {
const result = await PersistlyGameSaves.shared.forceSync(slotId);
if (result.status !== PersistlyGameSaveStatus.Conflict) {
return result;
}
const slot = await PersistlyGameSaves.shared.loadSlot(slotId);
const choice = await askPlayerHowToRecover({
local: slot.data,
cloud: slot.lastCloudData,
});
if (choice === "use-cloud") {
return PersistlyGameSaves.shared.acceptCloudVersion(slotId);
}
if (choice === "keep-local") {
return PersistlyGameSaves.shared.overwriteCloudVersion(slotId);
}
return PersistlyGameSaves.shared.keepLocalForLater(slotId);
}UX Guidelines
Keep Conflict Screens Rare
Resolve safe conflicts automatically. Ask players only when there is a real chance of losing meaningful progress.
Show Plain Language
Use "This device" and "Cloud save" labels. Include level, checkpoint, and time. Avoid exposing revision hashes unless the screen is for support or debugging.
Keep Backups Temporarily
When a player chooses one branch, keep the losing branch for a short recovery window if your storage policy allows it.
Production Checklist
- Use remote versions or compare-and-set semantics where available.
- Avoid timestamp-only conflict decisions for important saves.
- Define merge rules per data domain.
- Ask players when progress loss is possible.
- Log conflict outcomes for support and QA.