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.3.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.");
}Player Auth
Use Auth Bridge helpers instead of handling account sessions in game code.
Configure the account mode, sign in with a Firebase, Supabase, or Auth0 token, then keep using saveData, loadData, saveSlot, and loadSlot normally.
import { PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
accountMode: "authRequired",
});
const idToken = await getFirebaseIdTokenFromFirebaseAuth();
await PersistlyGameSaves.shared.signInWithFirebaseToken(idToken);
await PersistlyGameSaves.shared.saveData({
level: 5,
coins: 1200,
});Connect Later
Connect anonymous progress to a provider account later.
Use connectWithFirebaseToken, connectWithSupabaseToken, connectWithAuth0Token, or connectProvider after your game receives the provider token.
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
accountMode: "anonymousFirst",
});
// The player can save and sync before signing in.
await PersistlyGameSaves.shared.saveData({ level: 7, coins: 420 });
await PersistlyGameSaves.shared.forceSyncData();
// Later, after your Firebase UI signs the player in:
const token = await getFirebaseIdTokenFromFirebaseAuth();
try {
await PersistlyGameSaves.shared.connectWithFirebaseToken(token);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "account_auth_conflict") {
showAuthConflictChoicesUI();
return;
}
throw error;
}Auth
Firebase Auth Bridge
Firebase ID tokens become Persistly account sessions while save and load helpers stay unchanged.
Open Firebase guideAuth
Supabase Auth Bridge
Supabase access tokens become Persistly account sessions while save and load helpers stay unchanged.
Open Supabase guideAuth
Account modes
Choose anonymousFirst or authRequired and handle sign-out and account_auth_conflict behavior.
Open account modesFirebase Provider Helper
Use signInWithProvider when you prefer the generic provider shape.
Phase 1A accepts the firebase provider key. The token still comes from Firebase Auth.
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
accountMode: "authRequired",
});
const token = await getFirebaseIdTokenFromFirebaseAuth();
try {
await PersistlyGameSaves.shared.signInWithProvider({
provider: "firebase",
token,
deviceLabel: "web",
});
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "account_auth_conflict") {
showAccountConflictUI(error.details);
return;
}
throw error;
}Supabase Provider Helper
Use the Supabase helper or the generic provider shape.
Phase 1B accepts the supabase provider key. The token still comes from Supabase Auth.
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
accountMode: "authRequired",
});
const token = await getSupabaseAccessToken();
try {
await PersistlyGameSaves.shared.signInWithProvider({
provider: "supabase",
token,
deviceLabel: "web",
});
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "account_auth_conflict") {
showAccountConflictUI(error.details);
return;
}
throw error;
}Auth0 Provider Helper
Use the Auth0 helper or the generic provider shape.
Auth0 tokens become Persistly account sessions while normal save and load helpers stay unchanged.
await PersistlyGameSaves.configure({
runtimeKey: "ps_test_replace_me",
accountMode: "authRequired",
});
const token = await getAuth0Token();
try {
await PersistlyGameSaves.shared.signInWithProvider({
provider: "auth0",
token,
deviceLabel: "web",
});
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "account_auth_conflict") {
showAccountConflictUI(error.details);
return;
}
throw error;
}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.