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.
| Data | Use | Written by |
|---|---|---|
| Progress the rules own (level, coins, unlocks) | Persistent player variables and the save effect | The room, from rules |
| The player's own blob (settings, a draft, a note) | Helix.dataStore | The client |
| World-wide totals and rankings | Counters and leaderboards | The 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:
"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
playerJoinrule already sees the restored values. - Fire
saveon 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 valuesPreview 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.
Build your first multiplayer world
Start from the multiplayer scaffold, add one server-validated action, and publish. HELIX multiplayer is declarative, so there is no server code to write.
Sell something for LIX
Register a product on your world, ask the player to buy it, and read back what they own. The shell charges LIX; your world never handles money.