Godot SDK

Godot SDK for simple game saves and advanced runtime control.

Use PersistlyGameSaves for normal Godot integrations. Drop to PersistlyClient only when you intentionally need account, slot, or raw runtime contract control.

Simple Game Saves

Start with the Godot facade, not raw account/session plumbing.

PersistlyGameSaves is the default Godot facade for normal game integrations.

save_data and load_data use the default autosave slot for one-save games; save_slot and load_slot remain available for named slots.

The runtime client still exists for advanced account and slot integrations.

Use PersistlySlotStatus constants instead of raw status strings in game code.

Conflict responses preserve the cloud slot branch so Godot games can replace local canonical data before reconciling edits.

Advanced Runtime Client

Use this layer only when your integration needs direct account and slot control.

create_account(payload) creates one account, one first slot, and one account session token.

load_account(account_id, account_session_token) loads the account through its session.

sync_account_slot(...) accepts lightweight success responses and keeps cloud slot data separate on conflicts.

Godot templates

Copy one-save, multi-slot, or account + slots service wrappers before custom architecture.

Install

Use the public Godot addon source, not a private checkout path.

TERMINAL
Download the addon from the official GitHub repository and copy addons/persistly into your Godot project.

Setup

Configure once, then save and load named slots.

GDSCRIPT
const PersistlyGameSaves = preload("res://addons/persistly/persistly_game_saves.gd")

var persistly := PersistlyGameSaves.new()
persistly.configure({
  "runtime_key": "ps_test_replace_me",
})

var saved := persistly.save_data({
  "level": 5,
  "coins": 1200,
  "checkpoint": "forest-gate",
}, {
  "characterName": "Astra",
  "slot": "main",
})

if saved.get("status") == PersistlyGameSaves.PersistlySlotStatus.LOCAL_SAVED:
  print("Saved locally. Call force_sync_data or sync_due from a safe lifecycle moment.")

var loaded := persistly.load_data()
var sync := persistly.force_sync_data()

Player Auth

Use Auth Bridge helpers before normal save calls.

Configure the account mode, sign in with a Firebase, Supabase, or Auth0 token, then continue using save_data and load_data.

GDSCRIPT
const PersistlyGameSaves = preload("res://addons/persistly/persistly_game_saves.gd")

var persistly := PersistlyGameSaves.new()
persistly.configure({
  "runtime_key": "ps_test_replace_me",
  "account_mode": "authRequired",
})

var id_token := await get_firebase_id_token()
await persistly.sign_in_with_firebase_token(id_token)

persistly.save_data({
  "level": 5,
  "coins": 1200,
})

Connect Later

Connect anonymous progress to a provider account later.

Use connect_with_firebase_token, connect_with_supabase_token, connect_with_auth0_token, or connect_provider after your game receives the provider token.

GDSCRIPT
persistly.configure({
  "runtime_key": "ps_test_replace_me",
  "account_mode": "anonymousFirst",
})

persistly.save_data({ "level": 7, "coins": 420 })
await persistly.force_sync_data({ "bypassCooldown": true })

var token := await get_provider_token()
var result := await persistly.connect_with_firebase_token(token)

if result.get("status") == "account_auth_conflict":
  show_auth_conflict_choices_ui(result.get("error", {}))

Firebase Provider Helper

Use the generic provider helper when that fits your service wrapper.

GDSCRIPT
persistly.configure({
  "runtime_key": "ps_test_replace_me",
  "account_mode": "authRequired",
})

var token := await get_firebase_id_token()
var result := await persistly.sign_in_with_provider({
  "provider": "firebase",
  "token": token,
  "deviceLabel": "godot",
})

if result.get("status") == "account_auth_conflict":
  show_account_conflict_ui(result.get("error", {}))

Supabase Provider Helper

Use the generic provider helper when that fits your service wrapper.

GDSCRIPT
persistly.configure({
  "runtime_key": "ps_test_replace_me",
  "account_mode": "authRequired",
})

var token := await get_supabase_access_token()
var result := await persistly.sign_in_with_provider({
  "provider": "supabase",
  "token": token,
  "deviceLabel": "godot",
})

if result.get("status") == "account_auth_conflict":
  show_account_conflict_ui(result.get("error", {}))

Auth0 Provider Helper

Use the generic provider helper when that fits your service wrapper.

GDSCRIPT
persistly.configure({
  "runtime_key": "ps_test_replace_me",
  "account_mode": "authRequired",
})

var token := await get_auth0_token()
var result := await persistly.sign_in_with_provider({
  "provider": "auth0",
  "token": token,
  "deviceLabel": "godot",
})

if result.get("status") == "account_auth_conflict":
  show_account_conflict_ui(result.get("error", {}))

Autosave and Local Storage

Write local state first, then force sync at safe lifecycle points.

GDSCRIPT
persistly.save_data(current_state)

func _notification(what):
  if what == NOTIFICATION_WM_CLOSE_REQUEST:
    persistly.force_sync_data()

Conflict Handling

Use exported status constants in facade code and explicit statuses in advanced sync results.

GDSCRIPT
const SYNC_STATUS_ACCEPTED := "accepted"
const SYNC_STATUS_CONFLICT := "conflict"
const CONFLICT_REASON_BASE_VERSION_MISMATCH := "base_version_mismatch"

var result := client.sync_account_slot(account_id, account_session_token, slot_id, {
  "baseVersion": local_save["version"],
  "slotInfo": local_save["slotInfo"],
  "data": next_data
})

if result.get("status") == SYNC_STATUS_ACCEPTED:
  local_save = result["slot"]

if result.get("status") == SYNC_STATUS_CONFLICT:
  var cloud_slot := result["slot"]

  var details := result.get("details", {})
  if details.get("reason") == CONFLICT_REASON_BASE_VERSION_MISMATCH:
    # Keep cloud_slot as the baseline. If the player keeps local edits,
    # retry with baseVersion = cloud_slot["version"] and the local draft data.
    pass

Advanced Runtime Client

Create an account and sync a slot directly when you need the lower-level contract.

GDSCRIPT
var client := PersistlyClient.new(
  "https://api.persistly.app",
  "ps_test_..."
)

var created := client.create_account({
  "playerRef": "player-184",
  "accountData": { "diamonds": 1200 },
  "slot": {
    "slotId": "mage",
    "slotInfo": {
      "characterName": "Ayla",
      "slotLabel": "Mage"
    },
    "data": {
      "coins": 418,
      "checkpoint": "vault"
    }
  }
})

var account_id := created["accountId"]
var account_session_token := created["accountSessionToken"]
var slot_id := "mage"
var slot := created["slot"]

var result := client.sync_account_slot(account_id, account_session_token, slot_id, {
  "baseVersion": slot["version"],
  "slotInfo": slot["slotInfo"],
  "data": {
    "coins": 612,
    "checkpoint": "reactor"
  }
})

Behavior Notes

What the current Godot SDK enforces.

HTTP and HTTPS runtime requests go through HTTPClient with an explicit bearer runtime key and timeout.

Payload size checks run locally before transport: 16 KB for slotInfo and 256 KB for slot data.

A small in-memory cache stores cloud slot branchs by slotId and can provide baseVersion when the caller omits it during account slot sync.

Playable Demo

Use the public Last Beacon project to verify a real engine integration.

The official Godot repository includes Last Beacon, an endless idle sample project that creates an account, stores the session locally, and syncs a slot from a custom game UI.

Use the sample to confirm gameplay flow, accountId handling, accountSessionToken persistence, and conflict behavior before you wrap Persistly in your own project abstractions.

Validation

Validate against the public sample and your stage environment.

Run the official sample with a ps_test_ key and verify create, load, sync, and a forced conflict before using ps_live_ in production.

Keep your game UI custom. Persistly should stay behind the scenes as the save-sync layer.