Skip to content

State, saving, and cleanup ​

First decide who needs a value and how long it must live. That choice determines which SDK API to use.

Choose where data lives ​

NeedUseWho can read itSurvives restart?
Local animation or open panelReact state or a refThis clientNo
Owner-configured world settingsManifest configSchemaMod client and serverSaved by the host as world configuration
A placed object's settingseditorItems propertiesWorld object renderer; permitted object readsYes, when persisted by the editor/object API
Temporary moving objectsctx.entitiesMod clientsNo; recreate on start
Shared score or activity phasectx.stateEvery client of this modNo; restore from storage if needed
Private saved progressctx.storageThis mod's server in this worldYes, after a successful write
Player inventory or rewardsctx.inventory / ctx.rewardsHost-managed account surfacesHost-managed persistence
A request or one-time notificationctx.messages / client message hooksIts recipientsNo replay for late joiners

Replicated means sent to connected players. Persistent means saved for a later session. A replicated value is not automatically persistent.

Start, run, stop ​

  1. onWorldStart(ctx) loads saved data and initializes current state.
  2. Player, message, interaction, and tick hooks run during the session.
  3. onDispose(ctx) handles normal shutdown work.
  4. Host-owned timers and runtime entities are cleaned up. Registered resource disposers run after onDispose.

Save important changes when they occur. A process crash may prevent onDispose from running. Keep state per context with a WeakMap; the same mod module can serve more than one world.

Check save results ​

ctx.storage.set() returns Promise<boolean>. Await it and inspect the result:

ts
async function saveProgress(ctx, progress) {
  const saved = await ctx.storage.set('progress', progress);
  if (!saved) {
    ctx.log('Progress was not saved');
    return false;
  }
  ctx.state.set({ completed: progress.completed });
  return true;
}

This helper illustrates write-result handling; call it from a server hook after validating the action. Storage and replicated state are separate operations. Reward and inventory writes are separate too: several successful calls do not form one database transaction. Do not promise an exactly-once reward simply because a “claimed” flag was saved.

IDs and time ​

Use the full entity ID returned by ctx.entities.spawn() for updates and removal. It may return null. If the client sends a full ID back, validate that it belongs to an entity your mod owns; avoid splitting on : and assuming all IDs have only two parts.

Use ctx.now() for server timestamps and ctx.schedule for timers. The test harness controls this clock, so tests can advance time without waiting. sessionId identifies a connection; do not assume it stays the same on reconnect.

Repeated requests ​

Declare mutating messages in messageSchemas, using delivery: "at-most-once" when appropriate. Reuse the returned operation ID only for a retry of the same intent. The replay window is bounded and does not survive every restart.

Check gameplay state before each change. If correctness depends on a durable business operation, design its saved operation record and recovery behavior; transport deduplication alone is insufficient.

Release resources ​

Use ctx.schedule.interval() and ctx.schedule.timeout() for server timers; both return a cancellation function. Use ctx.resources.defer() for custom server subscriptions and useModResourceScope() for custom client resources. React effects should return cleanup functions for listeners and subscriptions.

Built-in asset hooks manage their caches. For large repeated objects, use batch renderers and avoid React state updates on every animation frame.

Verify the boundary you depend on ​

Use the test harness for validation and clock-driven behavior. Use a real hosted preview with two clients for replication. Verify persistence across a real runtime restart; a component remount alone does not prove it.

VibeLands Creator · Runtime API v2