Manifest Reference
vibelands-mod.json at the mod root. Validated by vibelands check, by the server at discovery (invalid manifests are logged and skipped), and by the SDK's validateModManifest.
Full example
{
"id": "ambient-butterflies",
"name": "Ambient Butterflies",
"version": "1.2.0",
"apiVersion": 2,
"runtime": "public",
"tier": "A",
"description": "Butterflies that drift over flowers during the day.",
"author": { "name": "Dev", "url": "https://example.dev" },
"license": "MIT",
"entries": {
"client": "client/index.jsx"
},
"assets": "assets/",
"permissions": ["audio"],
"configSchema": {
"type": "object",
"properties": {
"density": { "type": "number", "default": 1, "minimum": 0, "maximum": 3 }
}
},
"editorItems": [
{ "id": "butterfly-marker", "label": "Butterfly Marker", "category": "Nature" }
],
"tags": ["ambient", "decorative"]
}Fields
| Field | Required | Rules |
|---|---|---|
id | ✓ | kebab-case ^[a-z][a-z0-9-]{1,63}$, globally unique. Becomes the namespace for entities (<id>:<suffix>), storage, messages, and asset URLs |
name | ✓ | display name |
version | ✓ | semver; treat as immutable once distributed |
apiVersion | ✓ | must be 2 (the host refuses mismatches at load) |
runtime | - | "public" (default, marketplace/review-gated hosted mods) or "internal" (workspace/first-party mods) |
tier | ✓ | "A" (client-only decorative) or "B" (server-authoritative). Tier B requires entries.server |
entries.client | public | path to the React/R3F client source entry (built to dist/client.js for hosted mods, Vite-bundled for workspace mods). Public mods require this field |
entries.server | tier B | path to the server source entry (built to dist/server.js, CJS, Node 20, isolated worker target for hosted public mods) |
assets | - | informative; the hosted builder ships the assets/ folder when present (≤ 25 MB) |
permissions | - | array of known capability ids - unknown ids fail validation and every applicable API v2 capability fails closed at runtime |
configSchema | - | JSON-schema-lite for per-world config (below) |
messageSchemas | - | bounded client→server/server→client payload contracts (below); a declared direction is a strict allowlist |
components | - | versioned object schemas for mod-owned runtime entity component data (below) |
storageExport | - | explicit schema/version allowlist for mod storage that may travel in a World Package v2 |
editorItems | - | placeable items contributed to the editor Add panel (below) |
rendering | - | client render pipelines the mod supports and its role on the others (below); defaults to classic-only decorative |
description, author, license, tags, minHostVersion | - | metadata for humans and Marketplace listings |
rendering
VibeLands renders a world through one client pipeline: classic (WebGL, GLSL ShaderMaterial/onBeforeCompile) or node (WebGPU renderer, TSL node materials). Declare which ones your client entry supports:
"rendering": { "pipelines": ["classic", "node"], "role": "decorative" }pipelinesdefaults to["classic"]. Mods without a client entry work on every pipeline.role: "decorative"(default): on other pipelines the client skips the mod, and its server part keeps running.role: "essential": the world is only offered on pipelines this mod supports.- To support both, branch on
useRenderPipeline().vibelands checkreportsVB_RENDER_PIPELINE_CLASSIC_APIorVB_RENDER_PIPELINE_NODE_APIwhen a file uses a pipeline-specific API without branching.
configSchema
A deliberately small subset of JSON Schema - enough for validated config and an auto-generated admin form:
{
"type": "object",
"properties": {
"density": { "type": "number", "default": 1, "minimum": 0, "maximum": 3 },
"rounds": { "type": "integer", "default": 3, "minimum": 1, "maximum": 10 },
"color": { "type": "string", "default": "#7ad7ff" },
"nightOnly":{ "type": "boolean", "default": true },
"mode": { "type": "string", "enum": ["calm", "wild"], "default": "calm" }
}
}Resolution rules (applied by the host before your code sees the config):
- wrong-typed values → replaced by
default - numbers → clamped to
minimum/maximum;integer→ rounded - values outside
enum→ replaced bydefault - unknown keys → dropped
The resolved object is what useModConfig() (client) and ctx.config (server) return - both sides always see the same validated values.
messageSchemas
Tier B mods should declare every network intent and event in the manifest:
"messageSchemas": {
"clientToServer": {
"openChest": {
"summary": "Request opening one nearby chest.",
"delivery": "at-most-once",
"payload": {
"type": "object",
"properties": {
"chestId": { "type": "string", "minLength": 1, "maxLength": 128 }
},
"required": ["chestId"],
"additionalProperties": false
}
}
},
"serverToClient": {
"chestResult": {
"payload": {
"type": "object",
"properties": {
"ok": { "type": "boolean" },
"rewardCents": { "type": "integer", "minimum": 0, "maximum": 1000000 }
},
"required": ["ok", "rewardCents"],
"additionalProperties": false
}
}
}
}Supported payload types are object, array, string, number, integer, boolean, and null, with bounded properties, required, items, enum, length/count, and numeric limits. $ref, regex, unions, executable validators, and arbitrary schema extensions are intentionally unsupported. Object payloads default to additionalProperties: false.
Once either direction object is present, that direction is a strict allowlist: undeclared names and invalid payloads are blocked in the client SDK and live host. The headless harness throws on the same violation. An absent direction remains open only for API v2 compatibility with older mods.
Client→server declarations may set delivery: "at-most-once" for mutations such as purchases, rewards, claims, and mission transitions. The client SDK then generates an operation id automatically. If application code retries the same intent, it must pass the original id again; a new id means a new intent. The host accepts an id once per authenticated player, mod, and message type within the bounded replay window (currently 5 minutes, at most 2,048 retained ids per mod instance). This suppresses duplicate execution but is not a durable result cache across process restarts.
components
Components are schema-versioned data descriptors for this mod's own runtime entities. They do not alter core player, vehicle, or Colyseus schemas:
"components": [
{
"id": "cargo-state",
"version": 1,
"summary": "Replicated cargo securement state.",
"schema": {
"type": "object",
"properties": {
"secured": { "type": "boolean" },
"massKg": { "type": "number", "minimum": 0, "maximum": 5000 }
},
"required": ["secured"],
"additionalProperties": false
}
}
]IDs are kebab-case and automatically qualified as mod:<modId>:<componentId> outside the owning entity API. A mod may declare up to 32 descriptors and attach up to 16 values to one entity. Values are validated on spawn/update and share the entity's 2 KB cold-data budget with properties. Increment version when persisted data needs a migration; the v3 draft generator carries descriptor schemas and versions into contracts/state.schema.json.
storageExport
Mod storage is private and excluded from world exports by default. Opt in only portable keys, with a schema for every value:
"storageExport": {
"schemaVersion": 1,
"entries": [
{
"key": "world-progress",
"summary": "Portable world-level activity progress.",
"schema": {
"type": "object",
"properties": { "completed": { "type": "integer", "minimum": 0 } },
"required": ["completed"],
"additionalProperties": false
}
}
]
}Only listed, schema-valid keys are included. Imports restore them only when the destination resolves the exact mod version, manifest integrity, schema version, and schema integrity. Private keys never enter the package.
Manifest v3 draft
API v3 is currently a tooling draft, not a loadable runtime ABI. Creator can project an existing v2 manifest into the draft capability and contract model without changing the production file:
vibelands migrate . --to 3 --dry-run --jsonSee the Contract Registry for generated files and the command/query/event model.
Generate the shared TypeScript maps and keep them synchronized:
npm run generate
# equivalent: vibelands generate .The default output is shared/mod-messages.generated.ts. vibelands check fails with a stable diagnostic when that file is missing or stale.
editorItems
Items the mod contributes to the in-game editor's Add library (up to 64 per mod):
"editorItems": [
{
"id": "decor-chest",
"label": "Decorative Chest",
"category": "Treasure",
"description": "A purely decorative chest.",
"properties": { "title": "Chest", "color": "#7a4a21" },
"shadows": true,
"propertySchema": {
"type": "object",
"properties": {
"title": { "type": "string", "title": "Title", "maxLength": 64 }
}
},
"scale": 1
}
]| Field | Required | Rules |
|---|---|---|
id | ✓ | kebab-case, unique within the mod; the placed object's preset becomes mod:<modId>:<id> |
label | ✓ | item card display name |
category | - | any string; known editor categories merge in, new ones get their own filter chip. Defaults to Mods |
preset | - | claim a bare legacy preset id (e.g. tree) instead of the mod: namespace - existing DB objects with that preset render/behave through this item. First claimer wins; conflicts are logged and skipped |
collider | - | blocking collider for placed objects (server physics + client): { "type": "ball", "radius", "offsetY" }, { "type": "cuboid", "halfExtents", "offsetY" } or { "type": "compound", "pieces": [{ "halfExtents", "offset" }] } (max 12 pieces; optional XYZ Euler rotation per piece) |
physics | - | host-authoritative placed-object physics (Tier B + physics.colliders + collider required). Supports bodyType, damping, friction/restitution and combine rules, density, CCD, bounded carryable/throwable, and local-+Y powered bounce: { minimumSpeed, maximumSpeed }. Carry, throw, and bounce require bodyType: "dynamic"; a fixed knockdown item may also declare carryable/throwable, which apply once it is knocked loose. A dynamic item may declare trailer (see below) |
resource | - | makes the item harvestable: { "kind", "requiredWeaponId", "itemId", "maxHealth", "reward", "respawnMs" }. kind: "tree" gets the host fall-and-stump presentation; itemId must exist in the inventory catalog. Optional drops (≤ 6 × { "itemId", "quantity"?, "chance" }, chance in (0, 1]) are rolled on depletion; optional skill/level/xp are forwarded to canHarvestResource/onResourceHarvested |
shadows | - | set false for invisible or transparent editor volumes that should not cast or receive host shadows. Defaults to true |
properties | - | default properties merged into every placement (placer values win); ≤ 2 KB |
propertySchema | - | host-rendered per-object fields persisted in properties; supports string, number, integer, boolean, enum, limits, descriptions, and multiline strings |
scale | - | uniform default scale, positive number |
Reference: the first-party forest mod claims the legacy tree/pine/palm presets with colliders and axe-harvestable wood resources.
physics.trailer
A dynamic editor item with physics.trailer is a towable trailer. Drivers couple it to a car's tow ball with G; the host owns the ball joint, the wheels (it draws them), suspension, tyres, the parking stand, the cargo bed and hitch failure. Coordinates are item-local at scale 1: +Z along the drawbar towards the car, +Y up, origin on the ground under the axle.
| Field | Required | Rules |
|---|---|---|
coupler | ✓ | {x, y, z} socket locking onto the tow ball (balls ride about 0.44 m above ground) |
axles | ✓ | 1–3 { z, halfTrack }; each axle carries two wheels |
mountY | ✓ | strut top-mount height |
wheelRadius | ✓ | 0.1–0.8 m |
wheelWidth | - | 0.02–1 m, default 0.18 |
suspension | - | { restLength, maxLength, stiffness (N/m, ≤ 400000), damping (N·s/m, ≤ 60000), maxForce } per wheel |
tire | - | { corneringStiffness (N/rad), grip (0.05–2), rollingResistance (0–0.3) } per wheel |
jack | - | {x, y, z} parking-stand foot; carries the nose only while uncoupled |
cargoBed | - | { min, max } box; dynamic props inside it ride with the trailer |
hitch | - | { attachDistance (≤ 2 m), breakForce (500–400000 N), maxYawDeg, maxRollDeg } |
parkingBrake | - | boolean, default true |
trailer requires bodyType: "dynamic" and cannot be combined with carryable or knockdown. Reference: the first-party trailer-kit mod; authoring rules in the repository's docs/TRAILERS.md.
Physical reference: the first-party trampoline mod declares a dynamic, carryable/throwable editor item with a powered bounce surface. The host applies the same energy-bounded rule to players, rigid bodies and vehicles through the physics facade; mod code does not access native bodies.
Editor items are also part of the in-game AI command vocabulary: players with edit rights can say spawn 10 beacons (matching the item's label or id) and the host spawns the item with its default properties and scale, exactly like an editor placement - but only in worlds where the mod is enabled. Core preset and vehicle names always win over a mod item with the same name.
items
Inventory items the mod contributes to the host item catalog (up to 128 per mod). They flow through the host inventory - stacking, persistence, phone UI, General Store - exactly like core items:
"items": [
{ "id": "wheat_seed", "name": "Wheat Seed", "category": "Seed",
"maxStack": 99, "shop": { "buyCents": 180 } },
{ "id": "wheat", "name": "Wheat", "category": "Crop",
"maxStack": 999, "shop": { "sellCents": 120 } }
]| Field | Required | Rules |
|---|---|---|
id | ✓ | lowercase id; may claim a legacy core item id (e.g. wheat) so persisted player inventories keep working. First claimer wins |
name | ✓ | display name |
category | - | inventory grouping label; defaults to Item |
maxStack | - | 1–9999, defaults to 99 |
effects | - | applied by the host when the player uses the item: health/hunger/thirst/energy deltas |
shop | - | { "buyCents", "sellCents" } - buyable and/or sellable at the General Store |
tool | - | { "weaponId", "harvestMultiplier", "color"? } - harvest upgrade: while carried, hits with that weapon (pickaxe, axe) deal harvestMultiplier× (1–10) damage to resources; the best carried tool wins and its #rrggbb color tints the held tool head for every player |
icon | - | inventory icon: an emoji ("🍯") or an icon-kit glyph tinted by color: ore, lump, pile, gem-rough, gem, orb, ingot, coil, nails, planks, brick, pane, cloth, vial, potion, jar, ring, necklace, amulet, horseshoe, pickaxe, axe. Up to 32 characters; omitted → category icon |
color | - | #rrggbb accent for kit glyphs and the slot tint (tools fall back to tool.color) |
rarity | - | common, uncommon, rare, epic or legendary frame; omitted → derived from the shop value |
Reference: the first-party farming mod (seeds + crops).
recipes
Crafting recipes shown in the inventory's Crafting tab (up to 128 per mod):
"recipes": [
{ "id": "smelt-iron", "label": "Iron Bar", "category": "Smelting",
"station": "furnace", "skill": "smithing", "level": 10, "xp": 36,
"ingredients": [{ "itemId": "iron_ore", "quantity": 2 }, { "itemId": "charcoal", "quantity": 1 }],
"output": { "type": "items", "items": [{ "itemId": "iron_bar", "quantity": 1 }] } }
]| Field | Required | Rules |
|---|---|---|
id | ✓ | kebab-case, unique in the mod; addressed as <modId>:<id> |
label | ✓ | display name |
category | - | Crafting-tab filter; defaults to Crafting |
ingredients | ✓ | 1–8 { itemId, quantity }; may name core or other mods' items (hidden when unknown) |
output | ✓ | { "type": "items", "items": [...] } (≤ 8, batchable up to 25×) or { "type": "worldObject", "preset", "label"?, "distance"? } |
station | - | one of this mod's editor item ids; the crafter must stand within 4 m of one |
skill / level / xp | - | progression metadata shown in the UI and passed to canCraftRecipe / onRecipeCrafted; the owning mod enforces levels |
Reference: the first-party resources mod (62 recipes across smelting, smithing, tools, crafting, jewellery, cooking, herblore and building).
Items render through defineModClient({ objectRenderers: { 'decor-chest': Component } }). Placements are regular persisted world objects - select/move/duplicate/delete work with zero mod code, and the server validates that the owning mod is enabled and declares the item. Optional server hooks: onEditorObjectPlaced(ctx, event) / onEditorObjectRemoved(ctx, event).
The built artifact manifest
The hosted sandbox builder writes a copy of the manifest into the signed artifact with an added integrity block (sha384 per entry). The server recomputes hashes from hosted artifact files at load time - the artifact values are for registries and humans, not trust.