The JavaScript SDK supports browser games, JavaScript clients, and wrapper apps.
JavaScript SDK
JavaScript SDK for simple game saves and advanced runtime control.
Use PersistlyGameSaves for normal web-game integrations. Drop to PersistlyClient only when you intentionally need account, slot, or raw runtime contract control.
Simple Game Saves
Start here for browser games, Vite apps, and JavaScript wrappers.
PersistlyGameSaves is the default facade for normal game integrations.
saveData and loadData are the easiest one-save path; they use the default autosave slot under the hood.
Browser integrations use Persistly's built-in localStorage adapter by default; storageHelper is available only when a game needs a custom local storage implementation.
inspectData, acceptCloudData, overwriteCloudData, and keepLocalDataForLater keep one-save conflict recovery free of slot-key parameters.
saveSlot writes named-slot gameplay data immediately; loadSlot reads local data so the game can boot quickly, even offline.
The first forceSyncData, forceSync, syncDueSlots, or syncDue call creates the remote Persistly account and matching slot if needed.
One-save games can stay on saveData/loadData, while named slots such as slot-1, warrior, or mage let games grow into manual saves, campaigns, or slots.
Anonymous browser saves recover on the same browser storage automatically; another device can attach with a short-lived transfer code while the original session still exists.
AccountData is shared across slots and can hold account-wide gameplay data such as bundles, settings, shared inventory, or validated currency balances.
Raw account methods exist for lower-level direct integrations, but the SDK facade is the recommended path.
The facade writes local drafts immediately. Your game calls forceSyncData, forceSync, syncDueSlots, or syncDue from safe lifecycle moments; runtime policy controls due-sync cadence.
Conflict responses preserve local and cloud branches so the game can decide how to recover.
The SDK validates payload-size limits before requests leave the client.
Templates
JavaScript starters
Copy one-save, multi-slot, or account + slots service wrappers before building custom save architecture.
Open templatesBeginner Flow
Simple game saves
Use the shortest path when you only need saveData, loadData, and forceSyncData.
Open guideInstall
Use the published JavaScript package as the default integration path.
npm install @persistlyapp/sdkPlain HTML
For tiny demos, import the SDK from jsDelivr as an ESM module.
Use npm for production applications when possible. Use the CDN path when the game prototype does not have a build step.
<script type="module">
import { PersistlyGameSaves } from "https://cdn.jsdelivr.net/npm/@persistlyapp/sdk@1/+esm";
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
});
await PersistlyGameSaves.shared.saveData({
level: 1,
coins: 50,
});
</script>Package
@persistlyapp/sdk
The public npm package for JavaScript, browser, and JS-based wrapper integrations. Current stable release: 1.0.0.
Open npm packageRepository
JavaScript SDK source
The public GitHub repository for source, examples, releases, and issues.
Open GitHub repoCDN
jsDelivr ESM
A browser module import for plain HTML demos and quick prototypes.
Open CDN moduleGame Flow
Real game scenarios
Boot flow, signed-in restore, accountData, lifecycle sync, logout, archive, and delete guidance.
Open scenariosShared State
AccountData
Store bundles, shared inventory, settings, and validated currency balances with saveAccountData, patchAccountData, and local account-data inspection.
Read accountDataSimple Game Saves
Configure once, then save and load one default game save.
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
});
const loaded = await PersistlyGameSaves.shared.loadData();
if (loaded.status === PersistlyGameSaveStatus.LocalFound && loaded.data) {
console.log("Boot from local data:", loaded.data);
} else {
console.log("Start new game.");
}
// Local write. This does not need a network request.
await PersistlyGameSaves.shared.saveData({
level: 5,
coins: 1200,
checkpoint: "forest-gate",
}, {
slotInfo: { characterName: "Astra", slot: "main" },
});
// First sync creates a Persistly account + slot if needed.
const sync = await PersistlyGameSaves.shared.forceSyncData();
if (sync.status === PersistlyGameSaveStatus.Synced) {
console.log("Synced to Persistly.");
}
if (sync.status === PersistlyGameSaveStatus.Conflict) {
console.log("Show a local-vs-cloud recovery UI.");
}Autosave and Local Storage
Use localStorage for browser games that must survive refreshes and offline periods.
PersistlyGameSaves writes local data first. One-save games can use forceSyncData at safe moments such as manual save, pause, or level complete. Games with multiple saves can use listSlots() and forceSync(slotId).
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
});
await PersistlyGameSaves.shared.saveData(currentGameState);
window.addEventListener("beforeunload", () => {
void PersistlyGameSaves.shared.forceSyncData();
});Conflict Handling
Use exported statuses instead of raw strings.
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.shared.saveData(currentGameData);
const result = await PersistlyGameSaves.shared.forceSyncData();
if (result.status === PersistlyGameSaveStatus.Conflict) {
const save = await PersistlyGameSaves.shared.inspectData();
// Show local-vs-cloud UI in your game.
const choice = await askPlayerHowToRecover({
local: save.data,
cloud: save.lastCloudData,
});
if (choice === "use-cloud") {
await PersistlyGameSaves.shared.acceptCloudData();
}
if (choice === "keep-local") {
await PersistlyGameSaves.shared.overwriteCloudData();
}
if (choice === "decide-later") {
await PersistlyGameSaves.shared.keepLocalDataForLater();
}
}Advanced Runtime Client
Use PersistlyClient when you intentionally need account/session primitives.
Most game code should not start here. This layer exposes the account and slot runtime contract directly for advanced integrations and wrappers.
import {
LocalStorageSaveCache,
PersistlyClient,
PersistlySyncStatus,
} from "@persistlyapp/sdk";
const client = new PersistlyClient({
runtimeKey: process.env.PERSISTLY_RUNTIME_KEY!,
cache: new LocalStorageSaveCache(),
});
const created = await client.createAccount({
playerRef: "player-184",
accountData: { displayName: "Ayla", diamonds: 1200 },
slot: {
slotId: "mage",
slotInfo: {
characterName: "Ayla"
},
data: {
checkpoint: "vault",
coins: 418
}
}
});
// Persist the account session immediately in your trusted account backend or durable local save store.
// Do not log or expose accountSessionToken in player UI, telemetry, or support screenshots.
const accountSession = {
accountId: created.accountId,
accountSessionToken: created.accountSessionToken
};
// Sync the active slot through the account session.
const result = await client.syncAccountSlot({
accountId: accountSession.accountId,
accountSessionToken: accountSession.accountSessionToken,
slotId: "mage",
slotInfo: created.slot.slotInfo,
data: {
checkpoint: "vault",
coins: 612
}
});
if (result.status === PersistlySyncStatus.Conflict) {
const cloudSlot = result.slot;
// App-specific recovery: show a UI, merge data, or retry local edits from cloudSlot.version.
console.error("Conflict. Retry from cloud slot version:", cloudSlot.version);
}Common Mistakes
These are the integration errors most likely to create bad product assumptions.
Do not use playerRef as a public runtime lookup key from shipped clients.
Do not start with raw create/load/sync unless you intentionally need the advanced runtime contract.
Do not compare raw status strings. Use PersistlyGameSaveStatus and PersistlySyncStatus constants.
Do not treat accountSessionToken as an account password or recovery system. The default facade persists it as part of the local game-save identity.
Do not treat transfer codes as passwords, invites, playerRef lookup, support lookup, or long-lived recovery keys.
Do not treat runtime keys as privileged admin secrets.
Do not treat client-written accountData as a payment ledger. Validate purchases in a trusted payment/backend flow first.
Do not invent slot_id semantics at the platform layer; put save-selection labels in slotInfo instead.
Do not ignore canonical sync responses. Replace your local canonical snapshot after every create, load, or sync.
Do not sync every frame or use Persistly for real-time PvP data.