HELIX Docs
Guides

Persist player data

Keep player progress between sessions with persistent player variables, or store a player's own data with the Data Store.

HELIX gives a world two durable places to keep things. Pick by asking who is allowed to write it.

DataUseWritten by
Progress the rules own (level, coins, unlocks)Persistent player variables and the save effectThe room, from rules
The player's own blob (settings, a draft, a note)Helix.dataStoreThe client
World-wide totals and rankingsCounters and leaderboardsThe room, from rules

Progress the server owns

Mark a scalar player variable "persistent": true and write it with the save effect at the moment that matters. Extend the example from Build your first multiplayer world:

public/helix.json
"multiplayer": {
  "authoritative": true,
  "state": {
    "playerVars": {
      "coins": { "type": "number", "default": 0, "min": 0, "integer": true, "persistent": true },
      "bonusClaimed": { "type": "boolean", "default": false, "persistent": true }
    }
  },
  "actions": { "claimBonus": { "args": {} } },
  "rules": [
    {
      "when": { "on": "action", "name": "claimBonus" },
      "if": { "op": "==", "a": { "var": "self.bonusClaimed" }, "b": false },
      "then": [
        { "do": "add", "target": "self.coins", "by": 50 },
        { "do": "set", "target": "self.bonusClaimed", "to": true },
        { "do": "save", "player": "self" }
      ]
    }
  ]
}
  • Saved values are loaded before join rules run, so a playerJoin rule already sees the restored values.
  • Fire save on the event that changed something worth keeping (a level up, a checkpoint, a purchase), never on a tick. Saves are debounced and the room also flushes on leave and disconnect.
  • Only scalar variables (number, string, boolean, vec3) can persist, at most 16 per world and 8 KiB per player.
  • A stored value is re-validated against your current manifest on every load. Renaming a variable resets it, so treat persistent names as a schema.

The player's own data

Helix.dataStore is a durable key-value store scoped to your world. It is the right place for settings and anything the player authored. Key it by player yourself (for example player:<userId>). Reads return null for an unset key. Writes can carry the version you last read, so two tabs cannot silently overwrite each other, and 0 asserts the key does not exist yet:

type Settings = { musicVolume: number };
const key = `player:${user.id}`; // user comes from Helix.init() or Helix.auth.getUser()

const entry = await Helix.dataStore.getEntry<Settings>(key); // { value, version } | null
const next = { ...(entry?.value ?? { musicVolume: 0.8 }), musicVolume: 0.5 };

await Helix.dataStore.set(key, next, { expectedVersion: entry?.version ?? 0 });

const { keys, nextCursor } = await Helix.dataStore.list('player:'); // keys only, never values

Preview mode does not save

When a world is opened outside the HELIX shell (a plain vite preview), dataStore.set() does nothing and says so in the console. A clean preview run is never evidence that a save works; run helix dev to exercise it.

Treat anything the client writes as untrusted. Coins, items and scores that other players or the economy depend on belong in rules (above) or in the economy.

Next

Sell an item for LIX

Add a purchase to your world.

On this page