Overview
Every Instance has three key-value stores, all scoped to that instance. In single-node mode they live in memory; with Redis they are stored in Redis hashes and consistent across all nodes.
| Method / Property | Description | Returns |
|---|
instance.state | Server-only runtime state. Never leaves the server. | ScopedState |
instance.publicState | Synced live to every client in the instance - a snapshot on join, then every change. | ScopedState |
instance.userState(userId) | Private per-user state, synced only to that user's own clients. Other members never receive it. | ScopedState |
Instance state is for runtime data: active while the instance exists, discarded when it is destroyed. It is not a database replacement.
Synced state
All synced state is server-writable only - clients hold live read replicas via client.stateOf(path), client.userStateOf(path), and client.onStateChange(...).
// Server
await match.publicState.set('round', 3); // everyone sees this
await match.userState('alice').set('hand', cards); // only Alice sees this
await match.state.set('spawn-seed', seed); // nobody but the server
// Client
client.stateOf('ranked/match-42'); // public state replica
client.userStateOf('ranked/match-42'); // your private state
client.onStateChange(change => {
// change.scope === 'user' marks your private state
});
API
All three stores share the same ScopedState API:
| Method / Property | Description | Returns |
|---|
instance.state.get(key) | Get a value. Returns undefined if not set. | Promise<unknown> |
instance.state.set(key, value) | Set a value (JSON-serialized). | Promise<void> |
instance.state.delete(key) | Remove a key. | Promise<void> |
instance.state.keys() | List all keys in this namespace. | Promise<string[]> |
instance.state.clear() | Remove all keys in this namespace. | Promise<void> |
Examples
// Boss health
await dungeon.state.set('boss-health', 5000);
dungeon.on('attack-boss', async (session, payload) => {
const { damage } = payload as { damage: number };
const health = (await dungeon.state.get('boss-health') as number) ?? 0;
const newHealth = Math.max(0, health - damage);
await dungeon.state.set('boss-health', newHealth);
dungeon.broadcast('boss-health-updated', { health: newHealth });
if (newHealth === 0) {
dungeon.broadcast('boss-defeated', { killer: session.userId });
}
});
// Match timer
match.onJoin(async () => {
if (match.sessions.length === 2) {
await match.state.set('started-at', Date.now());
match.broadcast('match-started', { timeLimitMs: 5 * 60 * 1000 });
}
});
// Kill counter
dungeon.on('monster-killed', async (session) => {
const kills = ((await dungeon.state.get('kills') as number) ?? 0) + 1;
await dungeon.state.set('kills', kills);
dungeon.broadcast('kill-count', { kills });
});
Cleanup
domain.destroyInstance(id) automatically callsinstance.state.clear(). You can also clear manually.
// Automatic
await world.destroyInstance('dungeon-1');
// Manual reset
await dungeon.state.clear();
// Delete individual key
await dungeon.state.delete('boss-health');
Auto-destroy instances
Temporary instances (matches, dungeon runs) can clean themselves up once everyone leaves, while permanent instances never do:
world.createInstance('city'); // permanent
world.createInstance('match-42', { autoDestroy: 60_000 }); // gone 60s after emptying
autoDestroy: true uses a 30-second grace period. The timer arms when the last member leaves (a never-visited instance is not reaped), cancels if anyone joins during the grace period, and checks cluster-wide emptiness through presence before destroying. instance.temporary tells the two kinds apart.
Use cases
- Match timers and countdowns
- Boss / objective health and progress
- Runtime counters (kills, score)
- Active objectives or flags
- Game phase / round state
- Per-instance configuration
⚠Instance state is not persistent. With Redis, it survives node restarts but is cleared when the instance is destroyed. Use your own database for data that must outlast instances.