Interactive Universal Items
Production implementation contract for portable items with authoritative behavior, typed state, character abilities, and default-on Vault publication.
Interactive Universal Items
An Interactive Universal Item is a portable, ownable object whose definition includes its assets, behavior, interaction surface, and state schema. A shower, pinball machine, weapon, vehicle, or appliance is published once, works in many worlds, and keeps the same public functions and events while each owned copy and world placement keeps its own state.
Implementation handoff — target contract
The product/storage seams below are live, and the prototype proves the interaction model. The canonical package, production item runtime, SDK surface, and Web/Unreal adapters described after the status table are the contract to implement. They are not shipped APIs yet.
For the deployed upload/finalize/read behavior, use the
live wearable and Unreal contract. To run the
same dual-format probe against real cooked .pak, .utoc, and .ucas package outputs, send the
Unreal wearable test to the integration engineer.
Current live surface versus target
| Surface | Current live or proven | Production work still required |
|---|---|---|
| Product and ownership | universal_items is the product; every user_inventory_items row is an owned instance with JSON state. | Validate owned state against the item definition and add revisioned writes. |
| Placement | Home placements already store an inventory-instance link, transform, placement state, and metadata. | Apply the same placement overlay contract to every World/Instance runtime. |
| Vault | Vault assets have durable IDs, immutable numeric versions, compatibility, typed metadata, and usage tracking. A Universal Item can reference one through vault_asset_id. | Publish the item definition and dependency-complete package through this path for every reusable item. |
| Default publication | users.vault_auto_publish_generated_assets is true by default and generation respects the preference. | Make Universal Item finalization use the same default-on policy and accept an explicit per-publish opt-out. |
| Character system | The humanoid runtime has abilities, channel arbitration, named sockets, clip registration, and layered animation. | Ship a first-party ItemInteractionAbility and permit safe runtime clip/layer contribution. |
| Interactive behavior | The prototype proves typed vars, functions/events, sockets, declarative authority, replication filtering, capabilities, takeover input, cues, sandboxing, and call budgets. | Move the contract into a versioned shared schema and run it in the production Instance authority. |
| Runtime parity | The prototype renders on the Web path. | Web and Unreal must consume one manifest and one authoritative state protocol; only presentation adapters differ. |
The production boundary is deliberate: do not expose Helix.items publicly until the conformance
plan at the end passes on both runtimes.
Non-negotiable model
Keep these layers separate:
- Definition — immutable manifest, state schema/defaults, functions, events, sockets, logic, compatibility, and pinned Vault dependencies.
- Owned instance — ownership plus portable persistent state, keyed by inventory instance ID.
- Placement — world-local transform, approved overrides, bindings, and placement state.
- Session — authoritative live state replicated to players in the current Instance.
- Private authority state — secrets and anti-cheat data that are never sent to clients.
Definitions reference durable Vault asset IDs and pinned versions. They never persist CDN URLs. Installation creates fresh owned/placement IDs and state from defaults; it never copies another owner's state.
Canonical package
The package extension is .hlxi. Version 1 remains a deterministic container, but production
packages contain a small manifest/logic graph and pinned Vault references rather than duplicating
large media blobs.
item.hlxi
├── item.manifest.json
├── logic/authority.hlx.json # engine-neutral, executed by Instance authority
├── logic/presentation.web.mjs # optional, untrusted Web presentation
├── logic/presentation.unreal.json # optional, declarative Unreal presentation mapping
├── migrations/1-to-2.hlx.json # deterministic state migration, when needed
└── checksums.json # SHA-256 for every packaged fileThe canonical identity has three independent versions:
schemaVersion— item-manifest contract version, beginning at1.0.definition.version— semantic version of the item behavior and public surface.- Vault dependency
version— immutable integer version of each referenced Vault asset.
The published bundle and every dependency are content-addressed. A world may follow the latest definition for authoring, but a published World Build pins exact item and asset versions.
Manifest v1
This is the normative shape. JSON Schema should be generated from these types and published at
/schemas/interactive-item-manifest/1.0.
type JsonSchema = Record<string, unknown>;
type StatePartition = 'owned' | 'placement' | 'session' | 'private';
type Replication = 'none' | 'owner' | 'instance';
type ItemAuthority = 'platform' | 'instance';
type ItemAuth = 'anyone' | 'owner' | 'world-admin' | 'linked-item' | 'authority';
type VaultRef = {
assetId: string; // vault_assets.id UUID; never a URL
version: number; // immutable vault_asset_versions.version
kind:
| 'prop' | 'character' | 'animation' | 'audio' | 'texture' | 'material'
| 'environment' | 'terrain' | 'vfx' | 'decal' | 'sky'
| 'gaussian_splat' | 'scene' | 'unreal_package';
role: string; // model.primary, animation.reload, audio.fire, ...
required: boolean;
};
type ItemStateField = {
schema: JsonSchema;
default: unknown;
authority: ItemAuthority;
persistence: 'portable' | 'placement' | 'transient' | 'private';
replication: Replication;
writableBy: 'authority' | 'owner' | 'world-admin';
};
type ItemFunction = {
input: JsonSchema;
output?: JsonSchema;
auth: ItemAuth;
mutates: StatePartition[];
idempotency: 'required' | 'optional' | 'forbidden';
rateLimit?: { calls: number; perSeconds: number };
doc: string;
};
type ItemEvent = {
payload: JsonSchema;
delivery: 'reliable' | 'ephemeral';
audience: Replication;
};
type InteractiveItemManifestV1 = {
schemaVersion: '1.0';
definition: {
id: `helix:item:${string}`;
version: `${number}.${number}.${number}`;
title: string;
description: string;
kind: 'home_item' | 'prop' | 'wearable' | 'weapon' | 'vehicle' | 'appliance';
implements: string[]; // e.g. interactive_item, weapon.ranged
};
compatibility: {
engines: Array<'web' | 'unreal'>;
systems: string[]; // e.g. humanoid-character@^0.2.8
skeleton?: 'helix-humanoid@1';
};
assets: Record<string, VaultRef>;
physical: {
sizeMeters: [number, number, number];
pivot: 'footprint-center';
collision: { mode: 'bounds' | 'vault-ref' | 'none'; asset?: string };
};
sockets: Record<string, {
type: 'stand' | 'seat' | 'camera' | 'hand' | 'surface' | 'slot' | 'effect';
position: [number, number, number];
rotation?: [number, number, number];
node?: string;
}>;
state: Record<StatePartition, Record<string, ItemStateField>>;
functions: Record<string, ItemFunction>;
events: Record<string, ItemEvent>;
cues: Record<string, {
itemAnimation?: string;
actorAnimation?: string;
audio?: string;
vfx?: string;
at?: string;
loop?: boolean;
}>;
character?: {
ability: 'item-interaction';
channels: Record<string, { priority: number; mode: 'exclusive' | 'shared' }>;
input: Partial<Record<'move' | 'look' | 'primary' | 'secondary' | 'tertiary' | 'cancel', string>>;
};
capabilities: string[];
logic: {
authoritative: { format: 'helix-declarative-v1'; entry: string };
presentation?: Partial<Record<'web' | 'unreal', { format: string; entry: string }>>;
};
authoring: {
portableOverridePaths: string[];
worldBindings: Array<{ event: string; target: string; function: string }>;
};
publication: {
vault: 'default' | 'never'; // default obeys the creator preference; never is explicit opt-out
primaryAsset: string; // key in assets
};
migrations?: Array<{ from: string; to: string; entry: string }>;
};An interactive item is not a new Vault media kind. Its primary render resource retains its real
kind—usually prop—and is searchable with implements: ["interactive_item", ...]. The item package
is attached as the target definition.item_manifest related artifact. Implementing that role
requires widening the current material-only related-artifact gate. This preserves the distinction
between a reusable resource and an ownable product while making interactive behavior discoverable.
Vault dependency contracts
Item dependencies use the settled typed Vault formats:
| Kind | Canonical descriptor or artifact |
|---|---|
material | .helix-material.json — application/vnd.helix.material+json; maps use source.albedo, source.normal, source.roughness, and optional runtime.ktx2.albedo, runtime.ktx2.normal, runtime.ktx2.roughness roles |
scene | .helix-scene.json — application/vnd.helix.scene+json; v1 nodes reference a Vault asset UUID plus position/rotation/scale, with optional runtime bundles |
sky | .helix-sky.json — application/vnd.helix.sky+json; pins HDR/EXR source and generated lighting/backdrop renditions |
terrain | .helix-terrain.json — application/vnd.helix.terrain+json |
vfx | .helix-vfx.json — application/vnd.helix.vfx+json; adapters map the portable graph to Web effects or Niagara |
gaussian_splat | .spz — model/vnd.gaussian-splat.spz; gaussianSplat.scope is object, fragment, or environment |
The current related-artifact input is
Array<{ role, artifact: { filename, buffer, contentType? } }> and the resolved read shape is
{ role, url, mimeType, sizeBytes, checksumSha256 }. Immutable version metadata stores
{ role, storageKey, storageTarget, mimeType, sizeBytes, checksumSha256 }. Materials currently identify as
pbr-material-preview, report preview-beta PBR/tileability capability, and declare either
portable-source-only or portable-source-plus-ktx2.
Every final-mesh item thumbnail also carries metadata.canonicalThumbnail: source
validated-final-mesh, renderer version final-mesh-textured-v1, transparent background, black UI
composite, camera preset, and source SHA-256. Provider/reference images remain provenance-only and
must never become the canonical item thumbnail.
State, revision, and idempotency
| Partition | Stored on | Authority | Persistence | Replication | Publish behavior |
|---|---|---|---|---|---|
owned | inventory instance | platform/Instance command service | Travels with the owned copy | Owner or Instance, field-by-field | Values reset to manifest defaults |
placement | world placement | Instance authority | Stays with that placement | Instance, field-by-field | Values removed; allowlisted authoring overrides may remain |
session | live Instance | Instance authority | Never persisted | Instance or owner | Always removed |
private | authority store | platform/Instance authority | As explicitly required | Never | Always removed, including defaults marked secret |
Every state partition has an integer revision. Every mutation carries expected revisions and returns
the revisions it committed. A conflict returns RevisionConflict; clients refetch and retry from the
new snapshot instead of overwriting concurrent changes.
Every function call carries a caller-generated idempotencyKey. The authority stores the terminal
result for functions marked required; the same caller/key/function/instance tuple returns the
recorded result without running twice. This is mandatory for durability, inventory, currency,
rewards, score finalization, ammunition consumption, and ownership effects.
type ItemCallRequest<T> = {
instanceId: string;
function: string;
args: T;
idempotencyKey: string;
expectedRevisions?: Partial<Record<StatePartition, number>>;
};
type ItemCallResult<T> = {
callId: string;
value: T;
revisions: Record<StatePartition, number>;
emitted: Array<{ sequence: number; event: string; payload: unknown }>;
};Hydration order is fixed: definition defaults → owned state → placement state/overrides → session
defaults → onSpawn. No client renders a default state and then replaces it with persistent state.
Lifecycle
The authoritative runtime owns this sequence:
validate → install → hydrate → place → spawn → activate
↕ calls / input / events / ticks
deactivate → despawn → unplace
transfer: flush owned state → revoke old authority → attach new owner
upgrade: snapshot → deterministic migration → validate → commit atomicallyOnly onSpawn, onCall, onInput, onTick, and onDespawn execute item logic. install,
place, transfer, publication, and migration are platform commands. Logic cannot forge ownership
or bypass them.
SDK and runtime surface
The public Web API is asynchronous and authority-backed:
interface HelixItems {
inspect(instanceId: string): Promise<ItemSnapshot>;
getActions(instanceId: string): Promise<ItemAction[]>;
call<I, O>(request: ItemCallRequest<I>): Promise<ItemCallResult<O>>;
subscribe(instanceId: string, listener: (change: ItemChange) => void): () => void;
bind(placementId: string, binding: WorldBinding): Promise<void>;
unbind(placementId: string, bindingId: string): Promise<void>;
}The authoritative module sees a narrow, capability-checked context:
interface ItemRuntimeContext {
readonly self: { definitionId: string; inventoryInstanceId: string; placementId: string };
readonly actor: { userId: string; characterId: string } | null;
readonly state: {
get<T>(partition: StatePartition, key: string): T;
patch(partition: StatePartition, values: Record<string, unknown>): void;
};
emit(event: string, payload: unknown): void;
cue(name: string): void;
callItem(targetInstanceId: string, fn: string, args: unknown): Promise<unknown>;
character: {
occupy(socket: string, options?: { animation?: string; channels?: string[] }): void;
release(): void;
play(animation: string, options?: { loop?: boolean; layer?: string }): void;
attach(asset: string, socket: string): void;
};
world: {
invoke(bindingTarget: string, fn: string, payload: unknown): void;
raycast?(origin: number[], direction: number[], options: { maxDistance: number }): { targetId: string } | null;
applyDamage?(targetId: string, amount: number, cause: string): void;
consumeInventory?(inventoryInstanceId: string, quantity: number): Promise<void>;
};
now(): number; // authority clock
after(seconds: number, fn: string, args?: unknown): void;
}
interface InteractiveItemModule {
onSpawn(ctx: ItemRuntimeContext): void | Promise<void>;
onCall(ctx: ItemRuntimeContext, fn: string, args: unknown): unknown | Promise<unknown>;
onInput?(ctx: ItemRuntimeContext, frame: ItemInputFrame): void;
onTick?(ctx: ItemRuntimeContext, dt: number): void;
onDespawn?(ctx: ItemRuntimeContext): void | Promise<void>;
}The Unreal plugin exposes the same operations as latent Blueprint/C++ calls and receives the same snapshot/event envelopes. It does not run a separate item protocol.
Character integration
Items never bind keys or manipulate the character controller directly. They request abstract actions
and character channels through the first-party ItemInteractionAbility.
movementandcamera-controlare exclusive and priority-arbitrated.upper-body, animation layers, hands, sockets, and input actions are shared.character.occupy("seat")moves the character to an item socket; this is intentionally distinct from attaching a prop to a character bone.- Actor clips target
helix-humanoid@1, are validated/grounded during publication, and are registered under the item definition/version namespace. - A weapon may own aim, upper-body, hands, and fire/reload actions while locomotion continues.
- A shower or pinball takeover may temporarily own movement and camera until release.
Late placement must be supported: adding an item after the ability manager starts must still register its clips and contribute its animation layer safely.
Authority and security
One command choke point validates every call:
- Resolve immutable item definition and instance ownership.
- Validate function name and JSON-Schema arguments.
- Check
auth, world capability grants, and content/runtime compatibility. - Check expected revisions and idempotency.
- Enforce depth 8, 200 calls per item per second, declared function limits, memory, and execution-time budgets.
- Execute engine-neutral authority logic in a sandbox with no network, filesystem, wall clock, randomness, or host references.
- Validate each requested effect against capabilities.
- Atomically commit state, idempotency result, and reliable events.
- Replicate only fields and events the recipient may read.
Presentation scripts can play cues and simulate non-valuable motion. They cannot write authoritative state, award score, apply damage, consume inventory, grant items, or emit reliable events. Action labels and enabled states are UI hints; the call gate always re-checks authority.
Default-on Vault publication
Universal Item finalization publishes to Vault unless either:
- the creator preference
vaultAutoPublishGeneratedAssetsis false; or - this publish explicitly sets
publication.vaulttonever.
default means “obey the preference,” not “force public.” The server records whether publication was
automatic or explicitly disabled. Current generated-job responses expose vaultAssetId and
vaultAutoPublish: "published" | "disabled"; the per-item publication.vault override remains part
of this target contract, not a live field.
The deterministic publish transform is:
function toVaultDefinition(source: AuthoringItem): PublishedItemDefinition {
return {
definition: canonicalize(source.definition),
compatibility: source.compatibility,
assets: pinVaultVersions(source.assets),
physical: source.physical,
sockets: source.sockets,
stateSchema: source.stateSchema,
stateDefaults: resetToPublishableDefaults(source.stateSchema),
functions: source.functions,
events: source.events,
cues: source.cues,
character: source.character,
capabilities: source.capabilities,
logic: verifiedLogic(source.logic),
authoringDefaults: pick(source.placementOverrides, source.authoring.portableOverridePaths),
migrations: source.migrations,
};
}It preserves:
- definition metadata, schema, clean defaults, logic, functions/events, sockets, capabilities;
- pinned Vault dependencies and runtime renditions;
- animations, VFX, audio, materials, collision, and compatibility;
- only explicitly allowlisted portable authoring overrides.
It removes or resets:
- owned state values, durability, scores, contents, ammunition, preferences, and usage counters;
- placement IDs, transforms, world IDs, local links, binding IDs, and non-allowlisted overrides;
- occupants, cooldowns, playback positions, active balls/projectiles, timers, and session handles;
- owner/user/inventory-instance IDs and acquisition/commerce history;
- private state, anti-cheat seeds, secrets, credentials, runtime handles, and idempotency records.
Installing the result creates new IDs and revision 0 for every partition. A changed reusable
definition becomes a new immutable version or derivative; it never mutates existing pinned worlds.
Runtime adapters
The Instance authority executes one manifest and one logic graph for every runtime. Adapters only translate presentation:
| Contract | Web adapter | Unreal adapter |
|---|---|---|
| Geometry | Resolve Web GLB rendition | Resolve cooked Unreal rendition |
| Animation | Register helix-humanoid@1 GLB clips | Retarget/use matching UE skeleton clips |
| VFX | Map portable graph to Web renderer | Map the same graph to Niagara |
| Audio | Web Audio spatial source | Unreal Audio Component |
| State/events | Shared snapshots, revisions, and event sequence | Same protocol |
| Authority | Instance service | Same Instance service |
An engine-specific presentation feature may degrade or be absent. The declared functions, authority rules, state, and reliable events may not diverge.
Example: shower booth
The package owns the booth behavior. A world may bind finished to cleanliness, a quest, or lighting
without changing the shower.
{
"schemaVersion": "1.0",
"definition": {
"id": "helix:item:shower-booth",
"version": "1.0.0",
"title": "Shower Booth",
"description": "Portable interactive shower with door, water, temperature and actor poses.",
"kind": "appliance",
"implements": ["interactive_item", "appliance.shower"]
},
"compatibility": {
"engines": ["web", "unreal"],
"systems": ["humanoid-character@^0.2.8"],
"skeleton": "helix-humanoid@1"
},
"assets": {
"model": { "assetId": "10000000-0000-4000-8000-000000000001", "version": 3, "kind": "prop", "role": "model.primary", "required": true },
"wash": { "assetId": "10000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.wash", "required": true },
"rinse": { "assetId": "10000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.rinse", "required": true },
"water": { "assetId": "10000000-0000-4000-8000-000000000004", "version": 2, "kind": "audio", "role": "audio.water_loop", "required": true },
"waterVfx": { "assetId": "10000000-0000-4000-8000-000000000005", "version": 1, "kind": "vfx", "role": "vfx.water", "required": true },
"doorAudio": { "assetId": "10000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.door", "required": true }
},
"physical": {
"sizeMeters": [1.1, 2.18, 1.1],
"pivot": "footprint-center",
"collision": { "mode": "bounds" }
},
"sockets": {
"stall": { "type": "stand", "position": [0, 0, 0] },
"approach": { "type": "stand", "position": [0, 0, 1.1], "rotation": [0, 3.14159, 0] },
"camera": { "type": "camera", "position": [0, 1.65, 0.2] },
"showerhead": { "type": "effect", "position": [0, 1.9, -0.1] },
"door": { "type": "hand", "position": [-0.43, 1.1, 0.49] }
},
"state": {
"owned": {
"preferredTemp": { "schema": { "type": "number", "minimum": 18, "maximum": 45 }, "default": 38, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "owner" },
"minutesUsed": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "portable", "replication": "owner", "writableBy": "authority" }
},
"placement": {
"spray": { "schema": { "enum": ["rain", "jet", "mist"] }, "default": "rain", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" }
},
"session": {
"doorOpen": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"running": { "schema": { "type": "boolean" }, "default": false, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"occupant": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }
},
"private": {}
},
"functions": {
"open": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Open the door when unoccupied." },
"close": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "optional", "doc": "Close the door." },
"enter": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Occupy the booth." },
"start": { "input": { "type": "object", "properties": { "temperature": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "anyone", "mutates": ["owned", "session"], "idempotency": "required", "doc": "Start water and the wash pose." },
"stop": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session"], "idempotency": "required", "doc": "Stop water and release the actor." },
"setTemperature": { "input": { "type": "object", "required": ["value"], "properties": { "value": { "type": "number", "minimum": 18, "maximum": 45 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned"], "idempotency": "optional", "doc": "Save the owner's preferred temperature." }
},
"events": {
"doorChanged": { "payload": { "type": "object", "required": ["open"], "properties": { "open": { "type": "boolean" } } }, "delivery": "reliable", "audience": "instance" },
"started": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
"finished": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" }
},
"cues": {
"doorOpen": { "itemAnimation": "door.open", "audio": "doorAudio", "at": "door" },
"doorClose": { "itemAnimation": "door.close", "audio": "doorAudio", "at": "door" },
"waterOn": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": true },
"waterOff": { "audio": "water", "vfx": "waterVfx", "at": "showerhead", "loop": false },
"washing": { "actorAnimation": "wash", "loop": true }
},
"character": {
"ability": "item-interaction",
"channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } },
"input": { "cancel": "stop" }
},
"capabilities": ["actor.occupy", "actor.animate", "audio.play", "vfx.play"],
"logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
"authoring": {
"portableOverridePaths": ["placement.spray"],
"worldBindings": [{ "event": "finished", "target": "world", "function": "cleanliness.applyShower" }]
},
"publication": { "vault": "default", "primaryAsset": "model" }
}export const shower: InteractiveItemModule = {
onSpawn(ctx) {
if (ctx.state.get('session', 'running')) ctx.state.patch('session', { running: false, occupant: null });
},
async onCall(ctx, fn, args: any) {
const occupant = ctx.state.get<string | null>('session', 'occupant');
if (fn === 'open' && occupant === null) {
ctx.state.patch('session', { doorOpen: true });
ctx.cue('doorOpen');
ctx.emit('doorChanged', { open: true });
} else if (fn === 'close') {
ctx.state.patch('session', { doorOpen: false });
ctx.cue('doorClose');
ctx.emit('doorChanged', { open: false });
} else if (fn === 'enter' && occupant === null && ctx.actor) {
ctx.state.patch('session', { occupant: ctx.actor.userId, doorOpen: false });
ctx.character.occupy('stall', { channels: ['movement', 'camera-control'] });
} else if (fn === 'start' && ctx.actor?.userId === occupant) {
ctx.state.patch('session', { running: true });
ctx.state.patch('owned', { preferredTemp: args.temperature });
ctx.character.play('wash', { loop: true });
ctx.cue('waterOn');
ctx.emit('started', { actorId: occupant });
ctx.after(9, 'stop');
} else if (fn === 'stop' && (ctx.actor === null || ctx.actor?.userId === occupant)) {
const used = ctx.state.get<number>('owned', 'minutesUsed');
ctx.state.patch('owned', { minutesUsed: used + 1 });
ctx.state.patch('session', { running: false, occupant: null, doorOpen: true });
ctx.cue('waterOff');
ctx.character.release();
ctx.emit('finished', { actorId: occupant });
} else if (fn === 'setTemperature') {
ctx.state.patch('owned', { preferredTemp: args.value });
}
}
};Publishing preserves the clean preferredTemp: 38 default and allowlisted spray authoring value.
It removes the current owner's preference and usage count, placement transform, occupant, running
water, door/session state, world bindings added outside the manifest, and every runtime handle.
Example: networked pinball machine
Physics may be predicted by the playing client, but the Instance authority awards score. Spectators receive replicated ball transforms; the machine's own high-score table is portable owned state.
{
"schemaVersion": "1.0",
"definition": {
"id": "helix:item:pinball-neon",
"version": "1.0.0",
"title": "Neon Pinball",
"description": "Networked pinball machine with per-copy high scores.",
"kind": "appliance",
"implements": ["interactive_item", "arcade.pinball"]
},
"compatibility": {
"engines": ["web", "unreal"],
"systems": ["humanoid-character@^0.2.8"],
"skeleton": "helix-humanoid@1"
},
"assets": {
"model": { "assetId": "20000000-0000-4000-8000-000000000001", "version": 5, "kind": "prop", "role": "model.primary", "required": true },
"playPose": { "assetId": "20000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.play", "required": true },
"arcadeAudio": { "assetId": "20000000-0000-4000-8000-000000000003", "version": 2, "kind": "audio", "role": "audio.cues", "required": true },
"scoreVfx": { "assetId": "20000000-0000-4000-8000-000000000004", "version": 1, "kind": "vfx", "role": "vfx.score", "required": true }
},
"physical": { "sizeMeters": [0.8, 1.9, 1.7], "pivot": "footprint-center", "collision": { "mode": "bounds" } },
"sockets": {
"player": { "type": "stand", "position": [0, 0, 1.2], "rotation": [0, 3.14159, 0] },
"camera": { "type": "camera", "position": [0, 1.75, 2.2], "rotation": [0.22, 3.14159, 0] },
"table": { "type": "effect", "position": [0, 1.0, 0] }
},
"state": {
"owned": {
"highScores": { "schema": { "type": "array", "maxItems": 10, "items": { "type": "object", "required": ["userId", "score"], "properties": { "userId": { "type": "string" }, "score": { "type": "integer", "minimum": 0 } } } }, "default": [], "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "authority" }
},
"placement": {
"difficulty": { "schema": { "enum": ["easy", "standard", "hard"] }, "default": "standard", "authority": "instance", "persistence": "placement", "replication": "instance", "writableBy": "world-admin" }
},
"session": {
"playerId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"score": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"balls": { "schema": { "type": "integer", "minimum": 0, "maximum": 3 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"ballPose": { "schema": { "type": ["object", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" }
},
"private": {
"gameEpoch": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" }
}
},
"functions": {
"play": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Claim the machine and begin a three-ball game." },
"input": { "input": { "type": "object", "required": ["action", "phase"], "properties": { "action": { "enum": ["leftFlipper", "rightFlipper", "launch", "nudge"] }, "phase": { "enum": ["pressed", "released"] } }, "additionalProperties": false }, "auth": "anyone", "mutates": [], "idempotency": "forbidden", "rateLimit": { "calls": 120, "perSeconds": 1 }, "doc": "Submit transient controls from the current player." },
"reportBall": { "input": { "type": "object", "required": ["pose"], "properties": { "pose": { "type": "object" } } }, "auth": "anyone", "mutates": ["session"], "idempotency": "forbidden", "rateLimit": { "calls": 30, "perSeconds": 1 }, "doc": "Relay a predicted pose; never awards score." },
"scoreTarget": { "input": { "type": "object", "required": ["targetId", "gameEpoch"], "properties": { "targetId": { "type": "string" }, "gameEpoch": { "type": "integer", "minimum": 1 } } }, "auth": "authority", "mutates": ["session"], "idempotency": "required", "doc": "Authority-verified target hit." },
"finishBall": { "input": { "type": "object", "additionalProperties": false }, "auth": "authority", "mutates": ["owned", "session", "private"], "idempotency": "required", "doc": "Advance or end the game and commit a record." },
"leave": { "input": { "type": "object", "additionalProperties": false }, "auth": "anyone", "mutates": ["session", "private"], "idempotency": "required", "doc": "Release the machine." }
},
"events": {
"started": { "payload": { "type": "object", "required": ["playerId"], "properties": { "playerId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
"scored": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
"gameOver": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
"newRecord": { "payload": { "type": "object", "required": ["score"], "properties": { "score": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" }
},
"cues": {
"playing": { "actorAnimation": "playPose", "loop": true },
"score": { "audio": "arcadeAudio", "vfx": "scoreVfx", "at": "table" }
},
"character": {
"ability": "item-interaction",
"channels": { "movement": { "priority": 20, "mode": "exclusive" }, "camera-control": { "priority": 20, "mode": "exclusive" } },
"input": { "look": "aim", "primary": "leftFlipper", "secondary": "rightFlipper", "tertiary": "launch", "cancel": "leave" }
},
"capabilities": ["actor.occupy", "actor.animate", "physics.predict", "audio.play", "vfx.play"],
"logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
"authoring": {
"portableOverridePaths": ["placement.difficulty"],
"worldBindings": [{ "event": "newRecord", "target": "world", "function": "arcade.onRecord" }]
},
"publication": { "vault": "default", "primaryAsset": "model" }
}export const pinball: InteractiveItemModule = {
onSpawn(ctx) {
ctx.state.patch('session', { playerId: null, score: 0, balls: 0, ballPose: null });
ctx.state.patch('private', { gameEpoch: 0 });
},
async onCall(ctx, fn, args: any) {
const playerId = ctx.state.get<string | null>('session', 'playerId');
if (fn === 'play' && playerId === null && ctx.actor) {
const gameEpoch = ctx.state.get<number>('private', 'gameEpoch') + 1;
ctx.state.patch('session', { playerId: ctx.actor.userId, score: 0, balls: 3 });
ctx.state.patch('private', { gameEpoch });
ctx.character.occupy('player', { animation: 'playPose', channels: ['movement', 'camera-control'] });
ctx.emit('started', { playerId: ctx.actor.userId });
} else if (fn === 'input' && ctx.actor?.userId === playerId) {
ctx.world.invoke('physics', 'pinball.input', args);
} else if (fn === 'reportBall' && ctx.actor?.userId === playerId) {
ctx.state.patch('session', { ballPose: args.pose });
} else if (fn === 'scoreTarget') {
if (args.gameEpoch !== ctx.state.get<number>('private', 'gameEpoch')) return;
const points: Record<string, number> = { bumper: 10, ramp: 25, jackpot: 100 };
const score = ctx.state.get<number>('session', 'score') + (points[args.targetId] ?? 0);
ctx.state.patch('session', { score });
ctx.cue('score');
ctx.emit('scored', { score });
} else if (fn === 'finishBall') {
const balls = ctx.state.get<number>('session', 'balls');
if (balls > 1) {
ctx.state.patch('session', { balls: balls - 1, ballPose: null });
return;
}
const score = ctx.state.get<number>('session', 'score');
const highScores = ctx.state.get<Array<{ userId: string; score: number }>>('owned', 'highScores');
const next = [...highScores, { userId: playerId!, score }]
.sort((a, b) => b.score - a.score).slice(0, 10);
const record = next[0]?.userId === playerId && next[0]?.score === score;
ctx.state.patch('owned', { highScores: next });
ctx.state.patch('session', { playerId: null, balls: 0, ballPose: null });
ctx.emit('gameOver', { score });
if (record) ctx.emit('newRecord', { score });
ctx.character.release();
} else if (fn === 'leave' && ctx.actor?.userId === playerId) {
ctx.state.patch('session', { playerId: null, ballPose: null });
ctx.character.release();
}
}
};Publishing keeps an empty highScores default and the allowlisted difficulty. It removes the actual
leaderboard from the source copy, player, score, balls, ball pose, nonce, camera takeover, transform,
and world-added bindings. A platform-wide leaderboard is a separate service, not owned-item state.
Example: universal weapon
The weapon composes with locomotion through upper-body/hands channels. Damage and ammunition are authoritative world/platform operations; recoil, muzzle flash, shell motion, and hit markers are presentation.
{
"schemaVersion": "1.0",
"definition": {
"id": "helix:item:carbine-mk1",
"version": "1.0.0",
"title": "Carbine MK1",
"description": "Portable ranged weapon with shared equip, aim, fire and reload behavior.",
"kind": "weapon",
"implements": ["interactive_item", "weapon.ranged"]
},
"compatibility": {
"engines": ["web", "unreal"],
"systems": ["humanoid-character@^0.2.8"],
"skeleton": "helix-humanoid@1"
},
"assets": {
"model": { "assetId": "30000000-0000-4000-8000-000000000001", "version": 4, "kind": "prop", "role": "model.primary", "required": true },
"aim": { "assetId": "30000000-0000-4000-8000-000000000002", "version": 1, "kind": "animation", "role": "animation.aim", "required": true },
"fire": { "assetId": "30000000-0000-4000-8000-000000000003", "version": 1, "kind": "animation", "role": "animation.fire", "required": true },
"reload": { "assetId": "30000000-0000-4000-8000-000000000004", "version": 2, "kind": "animation", "role": "animation.reload", "required": true },
"shot": { "assetId": "30000000-0000-4000-8000-000000000005", "version": 1, "kind": "audio", "role": "audio.fire", "required": true },
"reloadAudio": { "assetId": "30000000-0000-4000-8000-000000000006", "version": 1, "kind": "audio", "role": "audio.reload", "required": true },
"muzzleFlash": { "assetId": "30000000-0000-4000-8000-000000000007", "version": 1, "kind": "vfx", "role": "vfx.muzzle", "required": true }
},
"physical": { "sizeMeters": [0.18, 0.34, 0.86], "pivot": "footprint-center", "collision": { "mode": "bounds" } },
"sockets": {
"grip": { "type": "hand", "position": [0, 0, 0], "node": "Grip_R" },
"support": { "type": "hand", "position": [0, 0, 0], "node": "Grip_L" },
"muzzle": { "type": "effect", "position": [0, 0.05, -0.83], "node": "Muzzle" }
},
"state": {
"owned": {
"durability": { "schema": { "type": "number", "minimum": 0, "maximum": 100 }, "default": 100, "authority": "platform", "persistence": "portable", "replication": "owner", "writableBy": "authority" },
"finish": { "schema": { "type": "string", "maxLength": 64 }, "default": "factory", "authority": "instance", "persistence": "portable", "replication": "instance", "writableBy": "owner" }
},
"placement": {},
"session": {
"wielderId": { "schema": { "type": ["string", "null"] }, "default": null, "authority": "instance", "persistence": "transient", "replication": "instance", "writableBy": "authority" },
"magazine": { "schema": { "type": "integer", "minimum": 0, "maximum": 30 }, "default": 30, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" },
"reserve": { "schema": { "type": "integer", "minimum": 0, "maximum": 180 }, "default": 90, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" },
"cooldownUntil": { "schema": { "type": "number", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "transient", "replication": "owner", "writableBy": "authority" }
},
"private": {
"shotSequence": { "schema": { "type": "integer", "minimum": 0 }, "default": 0, "authority": "instance", "persistence": "private", "replication": "none", "writableBy": "authority" }
}
},
"functions": {
"equip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Attach and activate weapon channels." },
"unequip": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Release channels and attachment." },
"aim": { "input": { "type": "object", "required": ["active"], "properties": { "active": { "type": "boolean" } }, "additionalProperties": false }, "auth": "owner", "mutates": [], "idempotency": "forbidden", "doc": "Set aim presentation state." },
"fire": { "input": { "type": "object", "required": ["origin", "direction"], "properties": { "origin": { "type": "array", "minItems": 3, "maxItems": 3 }, "direction": { "type": "array", "minItems": 3, "maxItems": 3 } }, "additionalProperties": false }, "auth": "owner", "mutates": ["owned", "session", "private"], "idempotency": "required", "rateLimit": { "calls": 10, "perSeconds": 1 }, "doc": "Validate a shot, consume ammunition, and apply an authoritative hit." },
"reload": { "input": { "type": "object", "additionalProperties": false }, "auth": "owner", "mutates": ["session"], "idempotency": "required", "doc": "Move reserve ammunition into the magazine." }
},
"events": {
"equipped": { "payload": { "type": "object", "required": ["actorId"], "properties": { "actorId": { "type": "string" } } }, "delivery": "reliable", "audience": "instance" },
"fired": { "payload": { "type": "object", "required": ["actorId", "sequence"], "properties": { "actorId": { "type": "string" }, "sequence": { "type": "integer" } } }, "delivery": "reliable", "audience": "instance" },
"hit": { "payload": { "type": "object", "required": ["targetId", "damage"], "properties": { "targetId": { "type": "string" }, "damage": { "type": "number" } } }, "delivery": "reliable", "audience": "instance" },
"depleted": { "payload": { "type": "object", "additionalProperties": false }, "delivery": "reliable", "audience": "owner" }
},
"cues": {
"aim": { "actorAnimation": "aim", "loop": true },
"fire": { "actorAnimation": "fire", "audio": "shot", "vfx": "muzzleFlash", "at": "muzzle" },
"reload": { "actorAnimation": "reload", "audio": "reloadAudio" }
},
"character": {
"ability": "item-interaction",
"channels": {
"upper-body": { "priority": 30, "mode": "shared" },
"hands": { "priority": 30, "mode": "shared" },
"camera-control": { "priority": 10, "mode": "shared" }
},
"input": { "look": "aim", "primary": "fire", "secondary": "aim", "tertiary": "reload" }
},
"capabilities": ["actor.animate", "actor.attach", "combat.raycast", "combat.damage", "audio.play", "vfx.play"],
"logic": { "authoritative": { "format": "helix-declarative-v1", "entry": "logic/authority.hlx.json" } },
"authoring": {
"portableOverridePaths": ["owned.finish"],
"worldBindings": [{ "event": "hit", "target": "world", "function": "combat.onWeaponHit" }]
},
"publication": { "vault": "default", "primaryAsset": "model" }
}export const weapon: InteractiveItemModule = {
onSpawn(ctx) {
ctx.state.patch('session', { wielderId: null, magazine: 30, reserve: 90, cooldownUntil: 0 });
ctx.state.patch('private', { shotSequence: 0 });
},
async onCall(ctx, fn, args: any) {
const wielderId = ctx.state.get<string | null>('session', 'wielderId');
if (fn === 'equip' && ctx.actor) {
ctx.state.patch('session', { wielderId: ctx.actor.userId });
ctx.character.attach('model', 'hand_r.grip');
ctx.emit('equipped', { actorId: ctx.actor.userId });
} else if (fn === 'fire' && ctx.actor?.userId === wielderId) {
const now = ctx.now();
const magazine = ctx.state.get<number>('session', 'magazine');
if (magazine <= 0) return ctx.emit('depleted', {});
if (now < ctx.state.get<number>('session', 'cooldownUntil')) return;
const sequence = ctx.state.get<number>('private', 'shotSequence') + 1;
const durability = ctx.state.get<number>('owned', 'durability');
ctx.state.patch('session', { magazine: magazine - 1, cooldownUntil: now + 100 });
ctx.state.patch('owned', { durability: Math.max(0, durability - 0.02) });
ctx.state.patch('private', { shotSequence: sequence });
const hit = ctx.world.raycast?.(args.origin, args.direction, { maxDistance: 250 });
if (hit) {
ctx.world.applyDamage?.(hit.targetId, 20, ctx.self.definitionId);
ctx.emit('hit', { targetId: hit.targetId, damage: 20 });
}
ctx.cue('fire');
ctx.emit('fired', { actorId: wielderId, sequence });
} else if (fn === 'aim' && ctx.actor?.userId === wielderId) {
if (args.active) ctx.character.play('aim', { loop: true, layer: 'upper-body' });
} else if (fn === 'reload' && ctx.actor?.userId === wielderId) {
const magazine = ctx.state.get<number>('session', 'magazine');
const reserve = ctx.state.get<number>('session', 'reserve');
const moved = Math.min(30 - magazine, reserve);
ctx.state.patch('session', { magazine: magazine + moved, reserve: reserve - moved });
ctx.cue('reload');
} else if (fn === 'unequip' && ctx.actor?.userId === wielderId) {
ctx.state.patch('session', { wielderId: null });
ctx.character.release();
}
}
};Publishing keeps durability/finish schema and clean defaults, but removes the owner's current durability/finish unless the finish is deliberately passed as an allowlisted derivative override. It always removes ammunition, cooldown, wielder, shot sequence, hit data, runtime attachments, and world combat bindings added outside the package.
Migration and compatibility
- Definitions and Vault versions are immutable. Editing creates a new version.
- Patch versions may change presentation without changing state/function/event schemas.
- Minor versions may add optional fields, functions, or events.
- Major versions require deterministic migrations and cannot silently upgrade pinned World Builds.
- Migrations receive only the old validated state document and declared defaults. They have no I/O, clock, randomness, user lookup, or runtime context.
- Migration is atomic: validate source → transform → validate target → commit all partitions and revisions, or keep the old version untouched.
- An unsupported runtime rendition fails installation before the world starts; it never falls back silently to an incompatible asset.
Ordered implementation and conformance plan
- Publish
[email protected]types, JSON Schema, validator, canonicalizer, package reader/writer, and golden fixtures for these three examples. - Add the item definition version and pinned Vault related-artifact records without duplicating media blobs.
- Add owned/placement/session/private revision envelopes and idempotent item call storage.
- Implement the authoritative command service, capability gate, replication filter, reliable event sequence, call-depth/rate/memory/time budgets, and deterministic timers.
- Port the proven declarative interpreter; reject authority scripts for cross-engine definitions.
- Ship
ItemInteractionAbility, late clip/layer registration, sockets, abstract input, and channel arbitration in the character runtime. - Implement Web and Unreal adapters against the same recorded protocol fixtures.
- Make Universal Item finalization run the exact Vault publish transform by default, with preference and per-publish opt-out tests.
- Add SDK/CLI/MCP operations for inspect, place, configure, bind, call, validate, publish, install, upgrade, and usage tracking; MCP remains an instruction layer over the CLI.
- Run a two-client, two-runtime conformance suite: publish each example, install it into another creator's world, exercise functions/events/state/replication, reconnect, transfer/replace, migrate, republish, and prove source state did not leak.
The acceptance test is simple: one creator publishes an interactive object, another creator or agent places it in a different world, and its standard behavior survives while its typed events and functions safely participate in that world's custom logic.
Source map
Current claims in this handoff are grounded in:
- Backend owned state:
helix-backend-api/src/database/entities/user-inventory-item.entity.ts - Backend placement state:
helix-backend-api/src/database/entities/home-item-placement.entity.ts - Product-to-Vault reference:
helix-backend-api/src/database/entities/universal-item.entity.ts - Default-on preference:
helix-backend-api/src/database/entities/user.entity.ts - Vault resources/versions/contracts:
helix-backend-api/src/vaultandsrc/database/entities/vault-asset*.ts - Character abilities/channels/sockets:
helix-web-engine-new/docs/character-architecture.md - Proven item manifest/runtime:
helixgame.com/prototypes/interactive-items/src/runtime - Proven examples and character bridge findings:
helixgame.com/prototypes/interactive-items/itemsandSPEC-CHANGES.md
Inventory & Items
Universal items that work across worlds and runtimes, and the four-tier execution model that decides what the client may do and what the server must own.
Live wearable and Unreal contract
The deployed Universal Item upload, finalize, status, download, and Unreal rendition contract, audited against helix3.