Contents
ConceptPlatform

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.

typescript
type SaveVersion = {
  slotId: string;
  deviceId: string;
  localVersion: string;
  remoteVersion?: string;
  updatedAt: string;
};

type Conflict = {
  slotId: string;
  local: SaveVersion;
  remote: SaveVersion;
};

Resolution Strategies

StrategyBest ForRisk
Last-write-winsLow-value settings or cosmetic changesCan erase better progress silently
Player choiceCampaign saves and visible progressionAdds UX friction
Field mergeIndependent fields such as settingsRequires careful domain rules
Server authorityEconomy, multiplayer, competitive dataNeeds 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
typescript
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.

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

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

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