Contents
GuideGodot

Building A Cloud Save System In Godot

Plan a Godot save system with Persistly local-first slots, cloud sync boundaries, and safe scene restoration.

Godot makes local file saves straightforward, but Persistly already handles the local-first save boundary for the game. Keep scene restoration separate from sync transport so your game can save immediately, work offline, and recover cleanly.

This guide uses the public Godot addon facade shape: configure, save_data, load_data, save_slot, load_slot, and force_sync.

Save Data Contract

Save stable game facts, not node instances.

gdscript
func build_save_data(player: Node) -> Dictionary:
    return {
        "schemaVersion": 1,
        "updatedAt": Time.get_datetime_string_from_system(true),
        "scene": get_tree().current_scene.scene_file_path,
        "player": {
            "health": player.health,
            "coins": player.coins,
            "checkpointId": player.checkpoint_id
        },
        "inventory": player.inventory.to_save_array()
    }

Save Data Boundaries

StoreBest ForAvoid
Persistly local slotFast boot, offline play, pending writesSecrets or account authority
Persistly cloud syncCross-device continuity, backupFrame-by-frame transient data
Game backendEconomy authority, multiplayer dataClient-trusted progression claims

Persistly Save First

Call the Persistly addon when the game reaches a meaningful save point. save_data and save_slot write to the addon local cache first, then force_sync_data, force_sync, sync_due_slots, or sync_due can push pending data to the cloud when the network is available.

gdscript
const SAVE_SLOT := "default"
const PersistlyGameSaves = preload("res://addons/persistly/persistly_game_saves.gd")

var persistly := PersistlyGameSaves.new()

func configure_persistly() -> void:
    persistly.configure({
        "runtime_key": "ps_test_replace_me"
    })

func save_checkpoint(save_data: Dictionary) -> void:
    var saved := persistly.save_slot(SAVE_SLOT, save_data, {
        "slotInfo": {
            "scene": save_data.get("scene", ""),
            "checkpointId": save_data.get("player", {}).get("checkpointId", ""),
            "updatedAt": save_data.get("updatedAt", "")
        }
    })

    if saved.get("status") == PersistlyGameSaves.PersistlySlotStatus.LOCAL_SAVED:
        sync_status_changed.emit("saved_local")

Treat client saves as player-owned cache. Validate shape and version before applying loaded data, especially if progression affects purchases, unlocks, or multiplayer access.

Cloud Sync Adapter

Keep Persistly calls behind an adapter. Scene code should ask for save operations, not construct HTTP requests directly. The adapter can save locally immediately and sync later without a second game-owned save file.

gdscript
const SAVE_SLOT := "default"

func save_now(save_data: Dictionary) -> void:
    save_checkpoint(save_data)

func sync_save(save_data: Dictionary) -> void:
    save_checkpoint(save_data)
    sync_status_changed.emit("syncing")

    var result := persistly.force_sync(SAVE_SLOT)

    if result.get("status") == PersistlyGameSaves.PersistlySlotStatus.SYNCED:
        sync_status_changed.emit("saved")
    else:
        sync_status_changed.emit("saved_offline")

Restoring Scenes Safely

Do not apply save data until the target scene has loaded and required nodes exist.

Restore Flow

  1. Parse save data.
  2. Validate schemaVersion.
  3. Load saved scene or fallback scene.
  4. Wait for scene-ready signal.
  5. Apply player and inventory data.
  6. Recompute derived values such as quest markers.
gdscript
func apply_save(save_data: Dictionary) -> void:
    if save_data.get("schemaVersion") != 1:
        push_warning("Unsupported save schema")
        return

    var scene_path = save_data.get("scene", "res://scenes/start.tscn")
    await get_tree().change_scene_to_file(scene_path)
    await get_tree().process_frame

    var player = get_tree().current_scene.get_node("Player")
    player.health = save_data.player.health
    player.coins = save_data.player.coins
    player.restore_checkpoint(save_data.player.checkpointId)

If you want to load the local Persistly slot first, keep the same validation boundary:

gdscript
func load_from_persistly() -> void:
    var loaded := persistly.load_slot(SAVE_SLOT)
    var data = loaded.get("data", {})

    if typeof(data) == TYPE_DICTIONARY and not data.is_empty():
        apply_save(data)

Production Checklist

  • Save through Persistly at meaningful checkpoints, not every frame.
  • Let the Persistly addon keep pending writes locally when cloud sync fails.
  • Retry cloud sync with backoff or on lifecycle/network events.
  • Validate dictionary keys before applying data.
  • Store checkpoint IDs instead of fragile coordinates.
  • Test with network disabled before and after scene transitions.