Contents
TutorialJavaScript

Transfer A Persistly Save Between Browser And Godot

Use short-lived Persistly transfer codes to move an anonymous account between browser games and Godot without building sign-in first.

Persistly transfer codes let a player move an anonymous account from one device to another while the original device still has the account session.

Use this for no-login games that need a simple "continue on another device" flow before you add your own sign-in system. The player opens the old device, creates a short-lived code, then enters that code on the new device.

Transfer codes are not usernames, passwords, support lookup keys, or permanent recovery codes. If every device loses the local account session, a public game client cannot safely recover that anonymous account.

When To Use Transfer Codes

Use transfer codes when:

  • both devices run a Persistly SDK
  • the old device can still load the player's save
  • the new device has no local Persistly account yet
  • the player can manually enter or paste a short code
  • you want transfer before building a full sign-in system

Use your own trusted backend instead when the game already has accounts. In that case, store accountId and accountSessionToken server-side for the authenticated user, then attach the Persistly account after your own login succeeds.

Browser To Browser

This is the simplest flow for a JavaScript game.

On the old browser, configure the SDK, save normally, and sync so the latest data is in the cloud:

typescript
import { PersistlyGameSaves } from "@persistlyapp/sdk";

await PersistlyGameSaves.configure({
  runtimeKey: "ps_test_replace_me",
});

await PersistlyGameSaves.shared.saveData({
  level: 12,
  coins: 3400,
  checkpoint: "ice-gate",
});

await PersistlyGameSaves.shared.forceSyncData();

Then create a transfer code:

typescript
const transfer = await PersistlyGameSaves.shared.createTransferCode({
  deviceLabel: "Old browser",
});

showTransferCodeToPlayer(transfer.transferCode, transfer.expiresAt);

Show the code on screen. Do not write it into logs, telemetry, support screenshots, analytics events, or long-lived storage.

On the new browser, configure the SDK first. Then consume the code before the player starts a new local save:

typescript
await PersistlyGameSaves.configure({
  runtimeKey: "ps_test_replace_me",
});

await PersistlyGameSaves.shared.attachWithTransferCode("P7K2D-M9Q4R", {
  deviceLabel: "New browser",
});

const loaded = await PersistlyGameSaves.shared.loadData();
startGameFromState(loaded.data);

attachWithTransferCode stores the returned accountId and accountSessionToken in the new browser's local SDK storage. Future saveData, loadData, and forceSyncData calls use the same account.

Browser To Godot

The same transfer code can attach a Godot build to an account created by a browser game, as long as both builds use the same Persistly project environment and compatible save data shape.

On the browser side:

typescript
const transfer = await PersistlyGameSaves.shared.createTransferCode({
  deviceLabel: "Browser",
});

showTransferCodeToPlayer(transfer.transferCode, transfer.expiresAt);

On the Godot side, configure the addon with the same environment key, then attach with the code:

gdscript
var persistly := PersistlyGameSaves.new()

var configured := persistly.configure({
	"runtimeKey": "ps_test_replace_me"
})

if configured.get("ok", false):
	var attached := persistly.attach_with_transfer_code("P7K2D-M9Q4R", {
		"deviceLabel": "Godot build"
	})

	if attached.get("ok", false):
		var loaded := persistly.load_data()
		start_game_from_state(loaded.get("data", {}))

For production builds, use the ps_live_ key from the same Persistly project environment on both clients.

Godot To Browser

The flow also works in the other direction.

On the Godot device that already has the account:

gdscript
var code := persistly.create_transfer_code({
	"deviceLabel": "Godot desktop"
})

if code.get("ok", false):
	show_transfer_code_to_player(code["transferCode"])

On the browser:

typescript
await PersistlyGameSaves.shared.attachWithTransferCode("P7K2D-M9Q4R", {
  deviceLabel: "Browser",
});

const loaded = await PersistlyGameSaves.shared.loadData();
startGameFromState(loaded.data);

Keep The Save Shape Portable

Cross-SDK transfer moves the Persistly account and slots. It does not automatically translate game-specific save schemas.

If JavaScript and Godot builds share the same save, keep the data object JSON-compatible and stable:

json
{
  "schemaVersion": 1,
  "level": 12,
  "coins": 3400,
  "checkpoint": "ice-gate",
  "inventory": [
    { "itemId": "twig_sword", "level": 3 }
  ]
}

Avoid storing engine-only objects, scene nodes, class instances, functions, binary blobs, or platform-specific paths in the save data. Use plain objects, arrays, strings, numbers, booleans, and null.

If the Godot build and browser build need different runtime details, keep those details in local-only game storage instead of Persistly save data.

Handle Existing Local Progress

Attach should happen before local progress exists on the new device.

If the new device already has local Persistly state, stop and ask the player what to do. Do not silently overwrite local progress. The normal options are:

  • keep playing the local save
  • clear local Persistly state and attach the transferred account
  • create a new transfer code from the other device

Only call clearLocalAccount or clear_local_account after the player explicitly chooses to replace local SDK state.

Errors To Expect

Common transfer-code errors:

ErrorMeaningPlayer action
transfer_code_invalidCode is malformed, revoked, wrong environment, or unknownCheck the code and environment
transfer_code_expiredCode expiredCreate a fresh code on the old device
transfer_code_consumedCode was already usedCreate a fresh code
transfer_code_rate_limitedToo many attempts were made against a real codeCreate a fresh code and wait briefly
transfer_code_disabledThe API deployment has transfer codes disabledCheck backend configuration

Use player-friendly copy such as "That code expired. Create a new code on your other device." Keep the raw error code in developer logs only if your logging system already redacts sensitive values.

Security Rules

Transfer codes are intentionally narrow:

  • They expire quickly.
  • They can be used once.
  • They are created only from an existing account session.
  • They attach only to the same runtime environment.
  • They should never be used as passwords or account names.
  • They should never be stored in support tickets, analytics, or public logs.

The runtime key is not enough to discover an account. Persistly does not provide public account lookup by playerRef, externalAccountRef, email, username, or provider subject.

Release Checklist

Before shipping this flow:

  • Test browser to browser transfer with a stage key.
  • Test browser to Godot transfer with a stage key if both clients share the same save.
  • Test expired and already-used codes.
  • Test attaching into a device that already has local progress.
  • Switch both clients to production runtime keys only for production builds.
  • Keep a visible "create new code" path when a code fails.

Transfer codes are a bridge for anonymous saves. When your game later adds real sign-in, use your trusted backend to store the Persistly account session for the authenticated player.