NEOLearn

Saving data

Save player progress, shared game state and leaderboards from a script with save, and how it's stored on NEO Platform, on the desktop, in the browser and on BLACKBOX.

Updated October 17, 2026

On this page

A game can keep data between play sessions without a backend of its own: a player's progress and settings, state every player shares, and leaderboards. Scripts reach it through save; Rust code through the neo_save crate. Every call is asynchronous: it returns a promise that resolves on a later simulation tick, so scripts stay deterministic.

Saving and loading a player's data#

save is the current player's own data. save.get(key) resolves to the stored value (or null), save.set(key, value) stores any JSON-like value: numbers, strings, booleans, lists, objects.

progress.js
import { World, on, save } from "neo";

export default class Progress extends World {
  state = { best: 0 };

  @on("round_start")
  async load() {
    this.state.best = (await save.get("best")) ?? 0;
  }

  @on("finished")
  async finished(player, time) {
    if (time > this.state.best) {
      this.state.best = time;
      await save.set("best", time);
    }
  }
}

this.save (self.save in Python) is the same object as the imported save.

Call (JS / Python)Resolves to
save.get(key)the value, or null
save.getEntry(key) / save.get_entry(key){ value, version }, or nullthe version is raised by every write
save.set(key, value)the new version
save.setIf(key, value, version) / save.set_if(...){ ok, version, value }writes only if the stored version is version (0: the key doesn't exist yet); otherwise ok is false and you get the current value and version
save.update(key, patch)the merged objectmerges an object's fields atomically; a field set to null is removed
save.remove(key)whether it existed
save.keys(prefix)a list of keysin key order

Use setIf when two places might write the same key: read with getEntry, compute, write with the version you read, and retry if ok is false.

Shared data and other players#

  • save.shared has the same calls for data every player of your game shares (a world record, a message of the day). Only the session's authority writes it: the host of a peer session, the player of a solo one.
  • save.player(userId) reaches a specific player's data. Anyone in the session can read it; the player and the session's authority can write it (the authority only for players in its session).

Leaderboards#

save.leaderboard(name) is an ordered store of whole numbers by key:

JavaScript
const wins = save.leaderboard("wins");
await wins.increment(1);          // the current player's entry
const top = await wins.top(10);   // [{ key, value, rank }, …], highest first

increment(by, key) adds to an entry; without a key it's the current player's own (keyed by their user id). Players only write their own entry; the session's authority may write any key (a team's, say).

Where it's stored#

Where the game runsStorage
Launched from NEO Platformthe platform's data store for your game, per experience: the same data on every device the player signs in on
The desktop, not launched by the platforma file in the game's save folder
A browser, not launched by the platformthe browser's IndexedDB for that page
BLACKBOXthe console's saves (written atomically)

A game launched from the platform gets a session token with its launch; without one (playing in the editor, a downloaded build), saves stay on the device. Your script doesn't change.

Limits#

  • Values: up to 4 MB each (as JSON). Keys: 1 to 128 characters. Store names (save.leaderboard(name)): letters, digits, _, . and -, up to 50.
  • Each game has a request budget per minute that grows with the number of players in its sessions, and a total storage size. Over the budget, a call fails with resource_exhausted: save on meaningful moments (a level finished, a purchase), not every tick.
  • Previous values are kept for 30 days.

A failed call rejects its promise (Python raises): catch it to show the player something or retry later.

Guests, deleting and exporting#

  • A guest's data is kept when they make an account, whether they upgrade the guest account or sign up while playing as a guest.
  • Deleting an account deletes everything games saved about that player, including their leaderboard entries.
  • Players can download everything games saved about them from Settings > Your data.

From Rust#

Games built in Rust use the neo_save crate's Saves resource: each call returns a ticket, and the result arrives later as a SaveDone message.

Rust
use neo_save::{SaveDone, SaveScope, Saves};

fn load(mut saves: ResMut<Saves>, mut ticket: Local<Option<neo_save::SaveTicket>>) {
    if ticket.is_none() {
        *ticket = Some(saves.get(SaveScope::Me, "best"));
    }
}

fn loaded(mut done: MessageReader<SaveDone>) {
    for d in done.read() {
        info!("save {:?}: {:?}", d.ticket, d.result);
    }
}