Skip to content

Permissions & Quotas

The containment contract: what you must declare, what you get, and what happens when limits are hit. None of it is optional - quotas are always on, for every plugin, in every world.

Permissions

Declared in the manifest, shown to server owners at install time, enforced server-side where the host mediates the capability: undeclared use logs denied: requires the '<permission>' permission and no-ops.

PermissionGatesEnforcement
world.entitiesctx.entities.*hard (server)
storagectx.storage.*hard (server)
players.modifyctx.rewards.grantMoney/grantXphard (server)
players.readplayer list APIsinformational in apiVersion 2 (reads are snapshot-only)
world.objects.readctx.objects.list/get (server), useWorldObjects (client)hard (both)
world.objects.writectx.objects.setFarm/setProperties on owned presetshard (server)
players.inventoryctx.inventory.count/grant/removehard (server)
audioclient audio bus + usePluginAudioBufferhard (client SDK returns a disabled stub)
ui.hudHudPanel surfaceinformational in apiVersion 2
physics.colliderscurated static/kinematic/client-local dynamic colliders, server-authoritative dynamic entity boxes, and closest-hit raycast via @vibelands/plugin-sdk/physicshard (client capability and server dynamic-body spawn both reject access without permission)

Unknown permission ids fail manifest validation in check and at server discovery.

Quotas & budgets

ResourceLimitOver-limit behavior
Replicated entities384 per plugin per worldspawn() returns null, logged
Entity properties JSON≤ 2 KB per entityspawn/update rejected
Entity transform updates10 Hz per entity, host-batchedfaster updates merge into a pending patch - final state always lands
Replicated KV blob≤ 8 KB, ≤ 2 writes/s (burst 4)over-rate writes defer, last write wins
Persistent storage≤ 32 KB per value, ≤ 2 MB per plugin per worldset() returns false, logged
Inbound messages20/s per plugin per client (burst 40), ≤ 8 KBsilently dropped
Server tick budget100 ms synchronous hook time per 5 s windowstrike; 3 consecutive strikes → disabled for that room
Hook errors5 per 5 s windowdisabled for that room
Client frame budgetadvisory ~1.5 ms avgvisible in the debug panel; egregious cases get flagged
Client bundle≤ 1.5 MB gziplocal check and the hosted build fail
Assets≤ 25 MB per pluginlocal check and the hosted build fail

Awaited I/O (ctx.storage, DB latency) never counts against the tick budget - only CPU you actually burn.

The circuit breaker

When a plugin trips the budget or error threshold:

  1. it's disabled for that room only - other worlds running it are unaffected
  2. its timers are cleared, its replicated entities and KV state removed
  3. a loud DISABLED line lands in the server log
  4. disabledReason appears in GET /admin/plugins health and the in-game store

Re-enabling via the store/admin API is the owner's explicit "try again" - it restarts the plugin fresh.

Client-side containment

  • Every surface mounts inside its own React error boundary: a render crash unmounts that plugin only and flags it in window.__VIBELANDS_PLUGINS__.status().
  • usePluginFrame swallows and counts callback errors; after 50 the callback is disabled.
  • Load failures (bad integrity, missing bundle, no surfaces) skip the plugin with a visible warning - never a white screen.

Observability

js
// Browser console
window.__VIBELANDS_PLUGINS__.status()        // { [id]: { status, error? } }
window.__VIBELANDS_PLUGINS__.frameMetrics()  // { [id]: { avgMs, maxMs, samples } }
bash
# Admin API - per-world live health
curl -H "x-admin-key: $KEY" http://localhost:2567/admin/worlds/<id>/plugins
# → health: [{ pluginId, enabled, disabledReason, entityCount, windowExecMs, overBudgetStrikes }]

Visual panel: bottom-left of the screen at debugMode ≥ 1 - status, [installed] source marker, avg/max frame ms per plugin.

VibeLands Creator - Plugin SDK apiVersion 2