Architecture guide · Cross-platform persistence
A TypeScript game save system that survives new releases and interrupted writes.
A save feature is not one serialization call. Separate the state you persist, the schema version you migrate, the slots players manage, the storage backend you write, and the server authority your client must never impersonate.
Treat save compatibility and storage durability as different problems.
State selection
Persist durable gameplay facts, not React nodes, scene objects, sockets, or caches.
Schema evolution
Stamp a version and migrate every supported historical payload into today's shape.
Storage durability
Use a backend appropriate to Web, desktop, mobile, mini games, or a remote service.
Authority
Decide which state the client may own and which economy or multiplayer facts require a server.
A migration fixes a valid old payload whose shape changed. Backup recovery finds a physically intact generation after corruption or an interrupted write. Neither one replaces authentication, conflict resolution, or authoritative backend validation.
Make every schema change an explicit, ordered migration.
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import {
defineMigrations,
persistOptions,
} from '@overworld-engine/core'
const migrate = defineMigrations({
1: (state) => ({
...state,
gold: state.coins ?? 0,
}),
2: (state) => ({
...state,
gold: Number(state.gold),
}),
})
export const useWallet = create<WalletState>()(
persist(
initializer,
persistOptions({
name: 'wallet',
version: 2,
migrate,
}),
),
)Migration keys are target versions and run in ascending order above the stored version. Keep them pure, preserve unknown data deliberately, and test direct upgrades from every version you still support—not only from the immediately previous release.
Copy a consistent live-save namespace into player-facing slots.
import {
createSaveSlots,
fromWebStorage,
} from '@overworld-engine/core'
const slots = createSaveSlots({
storage: fromWebStorage(localStorage),
prefix: 'overworld',
})
slots.saveTo('manual-1')
slots.saveTo('checkpoint')
const saves = slots.listSlots()
const restored = slots.loadFrom('manual-1')
if (restored) {
window.location.reload()
}The slot manager snapshots all live keys under the shared prefix while excluding its own slot namespace. Restoring rewrites storage; already-hydrated Zustand stores must reload or call their persist rehydration methods before the restored state becomes visible in memory.
Commit desktop files through a verified temporary generation.
import { createTauriSaveFileBackend }
from '@overworld-engine/adapters-savefile'
import {
commitSlot,
recoverSlot,
} from '@overworld-engine/core'
const backend = createTauriSaveFileBackend()
await commitSlot(
backend,
'saves/slot-1',
encodedPayload,
{ backupCount: 2 },
)
const outcome = await recoverSlot(backend, 'saves/slot-1', {
backupCount: 2,
isValid: validateGameSave,
})The commit sequence is temporary write, filesystem sync, read-back verification, oldest-first backup rotation, then atomic rename. Recovery scans current, backup 1, backup 2, and later generations until both the physical envelope and your optional business validator pass.
The adapter treats payloads as opaque bytes. Your game still owns fields such as schema version, build compatibility, content revision, random roots, and any domain-level checksum. Browser storage has no equivalent to a real filesystem sync, so do not claim identical durability across platforms.
Keep one save model and swap the platform boundary.
- Web: localStorage or another enumerable browser storage for local progress and settings.
- Tauri desktop: an atomic file backend with real fsync and rotating backups.
- Capacitor, WeChat, Telegram, and Steam: adapters that preserve the same persistence contracts.
- Node and tests: memory or application-owned storage with injected clocks.
A REST storage adapter can synchronize non-authoritative snapshots such as settings, unlocked local content, or single-player progress. Flush pending writes at lifecycle boundaries, authenticate every request, and define conflict behavior explicitly. Last-write-wins is a policy, not an inevitable property of cloud saves.
Currency, paid inventory, competitive progression, and shared-world state should not become authoritative merely because a client uploaded JSON. Store or reconstruct those facts on a trusted backend and validate client operations there.
Test the failure matrix before players create irreplaceable saves.
- Load every supported historical version and compare the migrated semantic state.
- Reject malformed JSON, invalid headers, incompatible content, and impossible values.
- Interrupt commits at each operation and confirm recovery returns an intact generation.
- Exercise quota exhaustion, permission denial, offline writes, retry, and account switching.
- Test two devices editing the same cloud snapshot and surface the chosen conflict policy.
- Record which backup was used so support can diagnose silent corruption and recovery.
Overworld supplies persistence conventions, sequential migrations, named snapshots, REST-compatible storage, atomic file orchestration, and platform adapters. Your game defines the save schema, retention policy, cloud service, authorization model, and domain validation.
Share the game rules, not platform APIs
See how the same domain systems run across Web, desktop, mobile, mini apps, and Node.
Read the cross-platform architecture guide →