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 mod, in every world.
Permissions
Declared in the manifest, shown to server owners at install time, and hard-enforced on every applicable runtime side. Server denials log denied: requires the '<permission>' permission; client hooks return inert/empty handles and warn once.
| Permission | Gates | Enforcement |
|---|---|---|
world.entities | ctx.entities.*, client entity hooks | hard (both) |
world.entities.read | bounded ctx.world.entities.get/query/findNearby and useWorldEntity/Query projections | hard (both); player projections additionally require players.read |
world.zones | bounded ctx.zones.* and authoritative onZoneEnter/onZoneExit facts | hard (server) |
world.interactions | declarative ctx.interactions.*, client interaction hooks and authoritative completion | hard (both) |
world.activities | bounded ctx.activities.* graph instances, managed persistence and onActivityEvent | hard (server) |
storage | ctx.storage.* | hard (server) |
players.modify | ctx.rewards.grantMoney/grantXp/spendMoney | hard (server) |
players.read | player snapshot APIs and join/leave lifecycle | hard (both); unauthorized worker snapshots are empty |
world.objects.read | ctx.objects.list/get (server), useWorldObjects (client) | hard (both) |
world.objects.write | ctx.objects.setFarm/setProperties on owned presets | hard (server) |
players.inventory | ctx.inventory.count/grant/remove | hard (server) |
players.teleport | ctx.players.teleport | hard (server) |
players.damage | ctx.players.applyDamage (bounded, host-clamped) | hard (server) |
audio | useModAudioBuffer, useModAudioBus, useModSpatialAudio and host audio access | hard (client SDK returns disabled/empty handles) |
ui.hud | HudPanel, semantic input actions, notifications, host HUD surfaces and dialogs | hard (client; undeclared HUD is not mounted) |
camera.control | bounded useModCameraShotRegistration presentation leases | hard (client; cannot override core camera modes or transfer camera ownership) |
physics.colliders | curated static/kinematic/client-local dynamic colliders, server-authoritative dynamic entity bodies/actions, bounded presentation-control leases, and closest-hit raycast via @vibelands/mod-sdk/physics | hard (client capability and server dynamic-body/lease access both reject access without permission; a lease never transfers server authority) |
physics.joints | bounded ctx.physics.createJoint/removeJoint/getJoint/listJoints and onPhysicsJointEvent for mod-owned authoritative bodies | hard (server); does not grant body creation or access to another mod's entities |
Unknown permission ids fail manifest validation in check and at server discovery.
Quotas & budgets
| Resource | Limit | Over-limit behavior |
|---|---|---|
| Replicated entities | 384 per mod per world | spawn() returns null, logged |
Entity properties JSON | ≤ 2 KB per entity | spawn/update rejected |
| Entity component descriptors / values | 32 descriptors per mod, 16 component values per entity; shares the 2 KB entity data budget | unknown, invalid or oversized component data rejected |
| World entity queries | 10/s, burst 20, ≤ 256 results, ≤ 2,048 m radius | empty result after rate exhaustion; inputs are clamped |
| Zones / interactions | 64 zones; 128 interactions, ≤ 20 m range, ≤ 10 s hold | invalid or over-quota registration returns null; invalid intents never reach mod code |
| Activities | 32 definitions, 128 nodes/definition, 64 instances, 16 participants, 16 KB variables/instance | invalid graphs or over-quota operations return false/null |
| Physics joints/actions | 128 joints/mod; 120 body/joint mutations/s, burst 240 | invalid/foreign endpoints return null; exhausted action budget returns false/null; endpoint and mod cleanup destroy joints |
| Entity transform updates | 10 Hz per entity, host-batched | faster updates merge into a pending patch - final state always lands |
| Player damage | ≤ 50 HP per applyDamage call; 4 calls/s per mod (burst 8); dead players rejected | over-limit call returns false, logged |
| 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 mod per world | set() returns false, logged |
| Inbound messages | 20/s per mod per client (burst 40), ≤ 8 KB | silently dropped |
| Server tick budget | 100 ms synchronous hook time per 5 s window | strike; 3 consecutive strikes → disabled for that room |
| Hook errors | 5 per 5 s window | disabled for that room |
| Client frame budget | advisory ~1.5 ms avg | visible in the debug panel; egregious cases get flagged |
| Client bundle | ≤ 1.5 MB gzip | local check and the hosted build fail |
| Assets | ≤ 25 MB per mod | local 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 mod trips the budget or error threshold:
- it's disabled for that room only - other worlds running it are unaffected
- its timers are cleared, its replicated entities and KV state removed
- a loud
DISABLEDline lands in the server log disabledReasonappears inGET /admin/modshealth and the in-game store
Re-enabling via the store/admin API is the owner's explicit "try again" - it restarts the mod fresh.
Client-side containment
- Every surface mounts inside its own React error boundary: a render crash unmounts that mod only and flags it in
window.__VIBELANDS_MODS__.status(). useModFrameswallows and counts callback errors; after 50 the callback is disabled.- Load failures (bad integrity, missing bundle, no surfaces) skip the mod with a visible warning - never a white screen.
Observability
// Browser console
window.__VIBELANDS_MODS__.status() // { [id]: { status, error? } }
window.__VIBELANDS_MODS__.frameMetrics() // { [id]: { avgMs, maxMs, samples } }# Admin API - per-world live health
curl -H "x-admin-key: $KEY" http://localhost:2567/admin/worlds/<id>/mods
# → health: [{ modId, enabled, disabledReason, entityCount, windowExecMs, overBudgetStrikes }]Visual panel: bottom-left of the screen at debugMode ≥ 1 - status, [installed] source marker, avg/max frame ms per mod.