SDK reference
Every element you import from @vvd/sdk — what it is, what's inside it, and how to structure one.
The dictionary. One entry per element, alphabetized within groups: what it is, why it exists, every property it takes, one minimal example. For the narrative version of any of these — the path through, rather than the lookup — each entry's See also line points at the guide or build-along that introduced it.
Everything on this page is imported from @vvd/sdk unless the example shows a subpath
(the one exception: defineDocumentCodec prefers the server-safe
@vvd/sdk/document/codec entry).
Defining a creation
The identity functions. Each takes one options object, returns it typed, and is the single authoring entry point for its kind of thing: an app provides a host, a tool consumes one, a block is the embeddable face of a tool, and a codec is the one module where Yjs may appear.
AssetManifest
The declared asset surface of a tool or app — the assets property on
defineTool / defineApp. A plain record mapping each
version-stable slot key ("weather.rain") to an AssetSpec:
| Property | Type | What it does |
|---|---|---|
path | string | Where the file lives inside the bundle's assets/ dir: "audio/rain.ogg". Required. |
kind | "audio" | "image" | "font" | "video" | "data" | What the slot holds. Required. |
meta | Record<string, unknown> | Computed at build (size, sha256, image w/h, audio durationMs) plus declared semantics the machine can't infer (loop, gainDb). |
replaceable | boolean | May an end-user override this slot? Default true (content); set false for UI chrome. |
accept | string[] | Accept filter for a replacement, e.g. ["audio/*"]. |
aliases | string[] | Prior keys that resolve to this slot — rename-stability across versions. |
Code references a slot by key, never by path, and resolves it with
useAsset — which is what lets a user replace the file without your code
changing.
See also: useAsset · useAssetUrl · the appAssets row of
HostServices.
buildSeed
Builds a document seed — a Yjs update binary — by running your build callback against
a fresh in-memory doc. It exists so sample content can be authored without a server: the
result is what a codec's seed property holds and what a fixture
hands a mock host, applied by the no-socket local runtime (previews, Storybook, tests)
before the document goes ready. Never applied to a live doc.
buildSeed(build: (doc: Y.Doc) => void): Uint8Array
The callback receives a raw Y.Doc — write through the same roots your codec reads. That
makes a seed-building module a codec-adjacent module: keep it next to the codec, not in a
view.
import { buildSeed } from "@vvd/sdk"
export const sampleTraySeed = buildSeed((doc) => {
doc.getArray<number>("rolls").push([12, 7, 19])
})See also: defineDocumentCodec (the seed row) —
defineStateCodec derives its seed from your field defaults, so you only build one by hand
for a hand-rolled codec or a fixture.
defineApp
Declares an app — a lens that provides a useHost() and owns a space (a tab, a route,
a project kind). Reach for it when you're building the thing that hosts tools, not the
thing that edits one document; unlike defineTool it validates its derived manifest at the
moment your module is imported, so a bad route or version throws before anything mounts.
| Property | Type | What it does |
|---|---|---|
id | string | Stable id — registry key, per-world enablement key, app-switcher key. Required. |
name | string | Human label — the world-shell tab and app-switcher entry. Required. |
templateName | string | What the app calls itself in a template list (a layout picker), where every row is already this kind of app. Absent = name. |
route | string | The URL segment it mounts at: /worlds/<world-slug>/<route>. One lowercase segment; two apps can't share one. Required. |
subRoutes | boolean | The app owns the URL subtree below its route; the host then provides nav.route so the app reads/drives its position without touching the router. Default false — sub-paths 404. |
Host | ComponentType<AppHostProps> | The app's data-bound useHost() provider — it renders <HostProvider services={…}> around children. Required. |
Surface | ComponentType<AppSurfaceProps> | The document surface the app wraps tools in (frosted panel, site frame). Receives the tool's variant hint ("panel" or "plain") plus children. Required. |
tools | ToolRegistry | The tools this app composes — dispatch by document type happens against this registry. Required. |
readOnly | boolean | Static edit stance (a shell-level hint). Runtime truth stays identity.canEdit(coords). Default false. |
tours | TutorialTour[] | Guided walkthroughs the host plays — same declarative contract as a tool's tours. |
version | string | The app's own semver, for listing and updates. Default "0.0.0". |
engines | Partial<AppEngines> | Minimum contract versions it needs. Default { host: 1, app: 1 } — leave it alone until you have a reason. |
capabilities | HostCapabilityName[] | The install-consent surface. Derive it with deriveCapabilities(tools, extra) so it can't drift from what the code calls. Default []. |
author | AppAuthor | Attribution for the listing: name plus optional url. |
icon | string | Icon id (resolved by the host's icon engine) for the switcher / listing. |
description | string | One-line human description for the switcher / listing. |
category | string | Launcher/storefront grouping slug — e.g. "site" for apps whose job is "be a website for your world". |
stateClass | string | The collab-state identity this app's shared state document is keyed by. Apps that share one state door declare the same class (every wiki template declares "wiki"). Absent = the app's own id. |
assets | AssetManifest | The developer-owned, version-locked asset pack this app ships (audio, images, fonts), resolved via useAsset. |
publish | AppPublishConfig | The publish seam: either full hooks (bake + renderBaked) or a data preset ({ preset: "world-site" | "project-docs" } — see below). Absent = the app isn't offered publish. |
customization | AppCustomizationParam[] | The standard Customize schema (serializable; rides the manifest). Each param is { type: "colors" } (named preset palettes, optionally user-customizable) or { type: "font" } (a list of font options), with a stable id, a label, and a default. The host renders the Customize pill; the app reads values via useAppCustomization(). |
access | AppAccessPolicy | Reserved marker for the future access sub-layer. Leave it out. |
The two publish presets are platform-executed projections — pure data on the manifest,
so an app becomes publishable with no publish code of its own. { preset: "world-site" }
publishes the wiki-shaped projection of the world's shippable content (index, refs,
routes, types, media, documents), served through the site-family surface — "publish my
world". { preset: "project-docs" } publishes only the instance's own project documents
as a reading surface (the quill-style story reader) — nothing of the world's canon ships.
Full hooks are the code path for apps whose published surface neither preset fits.
import { ToolRegistry, defineApp, deriveCapabilities } from "@vvd/sdk"
import { LobbyHost } from "./host"
import { LobbySurface } from "./surface"
import lobbyTool from "./tool"
const tools = new ToolRegistry().register(lobbyTool)
export default defineApp({
id: "lobby",
name: "Lobby",
route: "lobby",
version: "1.0.0",
capabilities: deriveCapabilities(tools, ["world"]),
Host: LobbyHost,
Surface: LobbySurface,
tools,
})See also: defineTool · useHost — long form:
Apps reference → defineApp, in full and the
Apps build-along.
defineBlock
Declares a block — a renderer insertable into another surface (a card body, a page),
carrying its own bag of data inside the host document. Reach for it when your tool ships
UI for someone else's document, or when you're building a document-less, world-reading
widget (scope: "world") mountable anywhere via <BlockSlot>.
| Property | Type | What it does |
|---|---|---|
type | string | The block's registration key — what a section stores and <BlockSlot type> resolves. Required. |
name | string | Human label for the "insert a block" menu. |
scope | "document" | "world" | What the block reads. "document" (default): its data arrives as block.data from the host document. "world": it ignores block.data, reads through useHost(), and can mount anywhere via <BlockSlot> with no instance to supply. |
needs | HostCapabilityName[] | Capabilities this block consumes. BlockHost fail-fasts on a missing one with a named boundary in the block's place, not an undefined deref. |
render | ComponentType<BlockRenderProps<TData>> | The renderer. Required. |
addable | boolean | May a blank one be inserted from a menu? Default true. Set false for referenced-document embeds, which are created by dropping a document, not from a menu. |
Its render receives BlockRenderProps<TData>:
| Prop | Type | What it does |
|---|---|---|
block | BlockInstance<TData> | id, type, and the typed data bag from the host document's section. |
canEdit | boolean | Mirror of the host's edit gate for the owning document. |
onUpdate | (updates: Partial<TData>) => void | Patch block.data through the owning document's codec — a block never opens its own connection. |
handle | DocumentHandle | null | The owning document's handle, for blocks that bind <CollaborativeText>. |
surface | "inline" | "panel" | Where the block is mounted. "panel" = alone in a surface the host handed it whole ("Open in panel") — same data and rights, bigger box. Default "inline". |
import { type BlockRenderProps, defineBlock } from "@vvd/sdk"
interface RollData {
label: string
result: number
}
function RollBlock({ block, canEdit, onUpdate }: BlockRenderProps<RollData>) {
return (
<aside>
<strong>{block.data.label}</strong>: {block.data.result}
{canEdit ? (
<button onClick={() => onUpdate?.({ result: 1 + Math.floor(Math.random() * 20) })}>
Reroll
</button>
) : null}
</aside>
)
}
export const rollBlock = defineBlock<RollData>({
type: "dice-roll",
name: "Die roll",
render: RollBlock,
})See also: defineTool — long form:
Tools reference → Blocks and
World blocks.
defineDocumentCodec
Declares a hand-rolled document codec — the per-document-type adapter that maps a raw
Yjs doc to a typed snapshot plus typed actions, and the only module where Yjs may appear.
Reach for it when defineStateCodec can't express your data (a canvas, a tree, custom CRDT
structure); everything a state codec produces for free you now declare by hand.
| Property | Type | What it does |
|---|---|---|
read | (doc) => TData | Read a typed snapshot. No Yjs leaks past the return value. Required. |
observe | (doc, onChange) => () => void | Watch the parts that affect the snapshot; return the unsubscribe. Required. |
actions | (doc) => TActions | Typed mutators bound to the document. |
isEmpty | (doc) => boolean | Is the document still empty of user content? Lets the platform discard an untouched starter on close. Ignore your own on-open scaffolding. |
stats | (doc) => Record<string, number> | How much of what the document is made of it holds ({ pins: 4 }) — drives getting-started depth tasks. Count what a user would count. |
undoScope | (doc) => UndoScopeRoot[] | The Y roots your actions mutate — UndoScopeRoot is Y.Map | Y.Array | Y.XmlFragment, the shared-root shapes an undo manager can track. Omitted = the card-editor default roots; a codec whose data lives elsewhere must declare its roots or its edits are invisible to Cmd+Z. |
seed | Uint8Array | Placeholder state (build with buildSeed) applied only to a fresh, empty doc in local no-socket mode — previews, Storybook. Never applied to a live doc. |
toolId | string | The id of the definition that interprets this document (dispatch metadata). |
schemaVersion | string | Current data-schema semver this codec reads/writes. Default "1.0.0". |
minSchema | string | Oldest version it can still read + migrate. A document below it gates as upgradeRequired instead of silently breaking. |
migrate | (doc, fromVersion) => void | Within-major migration up to schemaVersion. Must be idempotent and CRDT-convergent — it can run on multiple clients concurrently. |
agentActions | Record<string, AgentAction> | Named, described, schema-validated mutations the platform exposes over REST/MCP. Each: describe, input (a Standard Schema, e.g. zod), apply(doc, input), optional title. |
agentSummary | string | One line describing what a document of this type is, for agent discovery. |
stateShape | StateShapeSpec | The introspectable field shape — Record<fieldName, { kind: "value" | "list" | "map" | "prose"; default?; schema? }>; present means "auto-generate my agent CRUD". Set by defineStateCodec; hand-rolled codecs omit it and declare agentActions explicitly. |
collectMediaRefs | (data) => readonly string[] | Which world_media ids this document references — what publishing promotes to the public bucket. |
history | CodecHistory<TData> | Revision-history participation — all-optional overrides: summarize(before, after) phrases timeline captions in your own nouns, diff(before, after) renders a richer diff than the generic projection walk, plus restore/prose hooks. Omit it entirely and generic revision history still works; auto-derived where a stateShape exists. |
import { defineDocumentCodec } from "@vvd/sdk/document/codec"
interface Tray {
rolls: number[]
}
interface TrayActions {
roll(sides: number): void
}
export const trayCodec = defineDocumentCodec<Tray, TrayActions>({
read: (doc) => ({ rolls: doc.getArray<number>("rolls").toArray() }),
observe: (doc, onChange) => {
const rolls = doc.getArray<number>("rolls")
rolls.observe(onChange)
return () => rolls.unobserve(onChange)
},
actions: (doc) => ({
roll: (sides) =>
doc.transact(() =>
doc.getArray<number>("rolls").push([1 + Math.floor(Math.random() * sides)]),
),
}),
undoScope: (doc) => [doc.getArray("rolls")],
agentSummary: "A dice tray: the log of rolls made at this table.",
})Import the pure entry in anything server-facing
agentActions run inside an API route. Import defineDocumentCodec from
@vvd/sdk/document/codec, never the @vvd/sdk barrel, and keep the codec module free of
React and the DOM — otherwise vvd save can't derive your agent surface.
See also: defineStateCodec · useDocument — long
form: Tools reference → Making it agentable.
defineStateCodec
Declares a typed field shape and returns a complete, collaborative DocumentCodec — read,
observe, typed actions, seed, undo scope, history captions, and an auto-derived agent
surface, with zero Yjs written by you. Reach for it first, always; eject to
defineDocumentCodec only when a declared shape can't express your data.
defineStateCodec(shape, options?) — shape is a Record<string, StateField> built from
field.*; options is:
| Property | Type | What it does |
|---|---|---|
toolId | string | Passed through to the codec (dispatch metadata). |
agentSummary | string | One line describing what a document of this type is — required for every registered type's agent discovery. |
schemaVersion | string | Data-schema semver. Adding fields never needs a bump — a missing field reads as its default. Default "1.0.0". |
minSchema | string | Oldest readable version (the support window). |
migrate | (doc, fromVersion) => void | Non-additive evolution (renames, reshapes) — the standard codec migrate. |
The returned codec's actions (what useDocument(...).actions gives you):
| Action | Signature | What it does |
|---|---|---|
set | set(name, value) | Set a value field (last-write-wins). |
update | update(name, fn) | Read-modify-write a value field in one transaction. |
list | list(name) | Positional ops on a list field: push, insert(index, …items), remove(index, count?), replace(index, item), move(from, to). |
map | map(name) | Per-key ops on a map field: set(key, value), delete(key). |
transact | transact(fn) | Batch several actions into one undo step / one broadcast. |
import { defineStateCodec, field } from "@vvd/sdk"
export const sheetCodec = defineStateCodec(
{
title: field.value("Untitled sheet"),
rows: field.list<{ label: string; done: boolean }>(),
votes: field.map<number>(),
notes: field.prose(),
},
{ agentSummary: "A shared sheet: a title, rows of items, votes, and notes." },
)See also: field.* · useDocument ·
defineDocumentCodec — introduced in
Build a shared sheet; storage semantics in the
Storing data guide.
defineTool
Declares a tool — a document editor for one or more document types, consuming its host
through useHost(). This is the root of every tool project: the host resolves your tool by
the document's type and mounts your one render in whichever view fits.
| Property | Type | What it does |
|---|---|---|
id | string | Stable forever — analytics, tours, secrets and the Workshop key off it. Required. |
name | string | What a human sees: the Workshop listing, the "new document" menu. Required. |
documentTypes | string[] | The document types this tool renders/edits — its only registration. Required. |
render | ComponentType<ToolRenderProps<TContent>> | The one render, mounted in all three views. Required. |
capabilities | ToolCapabilities | The tool's relationship to world data: readsWorld, writesWorld, supportsProjectScope, supportsSharing (all optional booleans). Distinct from needs, which names host services. |
needs | HostCapabilityName[] | Host capabilities the tool (and its blocks) consume by name — the consent surface, and what makes useHostCapability fail loud. Never list an inherited capability here. |
Skeleton | ComponentType | Connecting-state skeleton, rendered by <DocumentGate> while the document is connecting. Falls back to the SDK default. |
surface | "panel" | "plain" | Surface hint: "panel" (default, the host's frosted document surface) or "plain" (bare — a canvas that owns its own surface). |
embed | "preview" | "interactive" | What an embed of this tool's documents is: a read-only, pointer-inert picture (default) or the live tool with the viewer's real edit rights. |
panelTitle | boolean | Whether the host draws its rename-in-place document title above the tool. Default true; set false when your own UI already shows the title. |
version | string | The tool's own semver — pins which immutable asset bundle its app-asset refs resolve against. Default "0.0.0". |
assets | AssetManifest | The asset pack this tool ships (audio, images, fonts), overridable per-document, resolved via useAsset. |
system | boolean | Marks an internal editor surface (not a catalog tool) — analytics skips it entirely. Absent = a real catalog tool. |
tours | TutorialTour[] | Declarative guided walkthroughs the host plays — pure data, no components or handlers. |
index | ToolIndexSpec | What's queryable about this tool's documents — serializable jsonpath selectors extracted into projection rows on every save. |
codec | DocumentCodec | The codec that interprets this tool's documents — the one definition the agent surface and index derive from. One knob, two homes: in code the property is codec (the imported DocumentCodec object); in vvd.json the same knob is spelled codecModule — a module path ("@/codec") that vvd save imports to derive the manifest's agent/index blocks. |
import {
DocumentGate,
type ToolRenderProps,
defineTool,
useDocument,
} from "@vvd/sdk"
import { sheetCodec } from "./codec"
function SheetView({ document, context }: ToolRenderProps) {
const { data, status, actions, retry } = useDocument(
{ worldId: document.worldId, documentId: document.id, scope: context.scope },
sheetCodec,
)
return (
<DocumentGate status={status} onRetry={retry}>
<input
value={data?.title ?? ""}
readOnly={!context.canEdit}
onChange={(e) => actions?.set("title", e.target.value)}
/>
</DocumentGate>
)
}
export default defineTool({
id: "sheet",
name: "Sheet",
documentTypes: ["sheet"],
render: SheetView,
})See also: ToolRenderProps · useDocument ·
defineBlock — long form:
Tools reference → defineTool, in full;
first met in Build a shared sheet.
field.*
The four field constructors — the whole authoring surface of a
defineStateCodec shape, each carrying its own CRDT merge behavior.
They exist so you choose merge semantics per field once, declaratively, instead of writing
Yjs: pick by what should happen when two people edit at the same time.
| Constructor | Signature | Merge behavior | Notes |
|---|---|---|---|
field.value | field.value(defaultValue, schema?) | Last-write-wins scalar. Concurrent writes to the same field converge on one winner; writes to different fields both win. | For titles, modes, config. Literal defaults widen ("Untitled" → string); narrow deliberately with field.value<"a" | "b">("a"). |
field.list | field.list(defaultItems?, schema?) | Positional CRDT. Concurrent inserts from two peers both land, in a stable converged order. | The default applies to the seed only (fresh local/preview docs), never to a live doc. schema describes one list item. |
field.map | field.map(schema?) | Per-key last-write-wins. Concurrent writes to different keys both win; the same key converges on one winner. | schema describes a map value. No default — a missing key is simply absent. |
field.prose | field.prose() | Character-level merge (collaborative rich text). | Reads back as an opaque field token, not a string — hand it, with the document handle, to <CollaborativeText>. Invisible to the index and to auto-derived agent ops. |
The optional schema on the first three is a runtime schema (zod works — it implements
Standard Schema) that makes the field's auto-generated agent operations and import target
precise. Adding a field never needs a migration: a missing field reads as its default.
import { defineStateCodec, field } from "@vvd/sdk"
export const questCodec = defineStateCodec({
title: field.value("Untitled quest"), // LWW scalar
objectives: field.list<{ text: string; done: boolean }>(), // both inserts land
rewards: field.map<number>(), // per-key LWW
briefing: field.prose(), // per-character merge
})See also: defineStateCodec · CollaborativeText
— merge semantics in depth in the Storing data guide.
Reading & writing documents
The document runtime, as a tool sees it: open a document as typed data, gate on its connection status, and render collaborative prose — with the whole connection/sync/Yjs stack hidden behind the codec.
CollaborativeText
A collaborative rich-text surface for one field.prose() field of a document — TipTap, Yjs
and the shared doc are all hidden inside. Reach for it whenever people type paragraphs two
of them might edit at once; a field.value string would be last-write-wins and eat one
person's edit.
| Prop | Type | What it does |
|---|---|---|
handle | DocumentHandle | null | The opaque handle from useDocument — connects the editor to the open document. Pass the handle, not data. Required. |
field | string | The prose field's token, read straight off your typed snapshot (data.notes). Required. |
editable | boolean | Yours to set — the component does not read host permissions. Default true. |
placeholder | string | Shown while the field is empty. |
seed | unknown | The field's last-known content (e.g. from a JSON snapshot) — used once, only when the field was never collaborative and the document has synced. |
className | string | Styling passthrough. |
extensions | Extensions | Extra TipTap extensions (mentions, slash commands). Must be a stable reference — a fresh array each render rebuilds the editor and drops the caret. Default []. |
import {
CollaborativeText,
DocumentGate,
type DocumentCoords,
useDocument,
} from "@vvd/sdk"
import { sheetCodec } from "./codec"
export function Notes({ coords, canEdit }: { coords: DocumentCoords; canEdit: boolean }) {
const { data, handle, status, retry } = useDocument(coords, sheetCodec)
return (
<DocumentGate status={status} onRetry={retry}>
{data ? (
<CollaborativeText
handle={handle}
field={data.notes}
editable={canEdit}
placeholder="Start typing…"
/>
) : null}
</DocumentGate>
)
}See also: field.* · useDocument — long form:
Tools reference → Collaborative text.
DocumentGate
The shared status gate every tool wraps its body with: error states render the SDK's retry
card, connecting renders a skeleton, ready renders your children. It exists so
status/error/skeleton handling is inherited with zero per-tool connection UI — the tool owns
useDocument, so the tool surfaces status and retry here.
| Prop | Type | What it does |
|---|---|---|
status | DocumentStatus | From useDocument. auth_error / error / disconnected → the shared <DocumentError> card; connecting → the skeleton; ready → children. Required. |
onRetry | () => void | Wire to useDocument().retry — the error card's Retry button. |
skeleton | ComponentType | Connecting-state component. Lowercase here; capital Skeleton on the tool definition. Falls back to DefaultDocumentSkeleton. |
children | ReactNode | Rendered when ready. Required. |
import { DocumentGate, type DocumentCoords, useDocument } from "@vvd/sdk"
import { sheetCodec } from "./codec"
import { SheetSkeleton } from "./skeleton"
export function SheetBody({ coords }: { coords: DocumentCoords }) {
const { data, status, retry } = useDocument(coords, sheetCodec)
return (
<DocumentGate status={status} onRetry={retry} skeleton={SheetSkeleton}>
<h1>{data?.title}</h1>
</DocumentGate>
)
}See also: useDocument · defineTool (Skeleton) — the
related exports DocumentError and DefaultDocumentSkeleton are the pieces it composes.
encodeDocumentSnapshot
Encodes the current state of an open document as a portable update binary — the one
supported way a tool captures a document for publishing. It exists so publishing never
requires the tool to touch Yjs: take the bytes from the handle, hand them to
host.publish.publish(...).
| Parameter | Type | What it does |
|---|---|---|
handle | DocumentHandle | null | The open document. A not-yet-open handle returns an empty update. |
Returns Uint8Array — the snapshot the published surface re-seeds an in-memory doc with;
the same codec reads it back identically.
import {
type DocumentHandle,
type VvdDocument,
encodeDocumentSnapshot,
useHost,
} from "@vvd/sdk"
export function PublishButton({
document,
handle,
}: {
document: VvdDocument
handle: DocumentHandle | null
}) {
const host = useHost()
if (!host.publish || !handle) return null
return (
<button
onClick={async () => {
const { url } = await host.publish!.publish({
documentId: document.id,
documentType: document.type,
title: document.title,
snapshot: encodeDocumentSnapshot(handle),
})
await navigator.clipboard.writeText(url)
}}
>
Publish a snapshot
</button>
)
}See also: useDocument (handle) · the publish row of
HostServices.
ToolRenderProps
The props a tool's render receives — exactly two: the document's identity and the mount's
context. It exists as the whole input contract of a tool view: everything else (live data,
capabilities) is reached through hooks, so the same render runs under every host.
| Prop | Type | What it does |
|---|---|---|
document | VvdDocument<TContent> | Identity only — turn it into live data with useDocument. |
context | ToolContext | What the platform can't otherwise tell you about this mount. |
document (VvdDocument):
| Property | Type | What it does |
|---|---|---|
id | string | The document id. |
worldId | string | The world it lives in. |
type | string | The document_type discriminator — what resolved your tool. |
title | string | The document's name (host-owned; rename via nav.renameDocument). |
scope | Scope | Where the data lives: { type: "world", worldId } or { type: "project", worldId, projectId }. |
visibility | "shown" | "hidden" | Whether it is shown or hidden. |
content | TContent | Optional tool-specific payload on hosts that inject one — live data still comes from useDocument. |
context (ToolContext):
| Property | Type | What it does |
|---|---|---|
scope | Scope | The world (and optionally project) this mount is scoped to. |
canEdit | boolean | May this viewer write? A render decision — enforcement lives server-side regardless of what you draw. |
view | "embed" | "editor" | "fullscreen" | Which of the three views is mounted. ToolHost always injects a concrete value (default "editor"). |
focus | { blockId?: string } | The host mounted the tool focused on one block ("Open in panel"). A tool that doesn't understand it ignores it and renders its whole document. |
handle | DocumentHandle | null | Populated after useDocument for blocks that bind <CollaborativeText>. |
import { type ToolRenderProps } from "@vvd/sdk"
export function SheetView({ document, context }: ToolRenderProps) {
if (context.view === "embed") {
return <CompactSheet documentId={document.id} />
}
return <FullSheet documentId={document.id} canEdit={context.canEdit} />
}See also: defineTool · useDocument — long form:
Tools reference → defineTool, in full.
useDocument
Opens a document and returns it as typed, reactive data with typed mutators — the whole connection/sync/Yjs stack hidden behind the runtime and your codec. This is the one hook every tool's render calls; it re-renders only when the snapshot actually changes, never on unrelated edits.
useDocument(coords, codec):
| Parameter | Type | What it does |
|---|---|---|
coords | DocumentCoords | Which document: worldId, documentId, optional scope, optional era (pin to a specific era; omitted = the reader's ambient lens). |
codec | DocumentCodec<TData, TActions> | How to interpret it — from defineStateCodec or defineDocumentCodec. Treat as mount-constant. |
Returns UseDocumentResult<TData, TActions>:
| Property | Type | What it does |
|---|---|---|
data | TData | null | Typed, reactive snapshot — null until the document is open. Referentially stable between unrelated re-renders. |
status | DocumentStatus | "connecting" | "ready" | "disconnected" | "auth_error" | "error" — feed it to <DocumentGate>. |
actions | TActions | null | Typed mutators — null until open. Writes sync automatically. |
handle | DocumentHandle | null | Opaque handle for SDK collaborative components (<CollaborativeText>, presence hooks, encodeDocumentSnapshot). Never read its internals. |
retry | () => void | Destroy + rebuild the connection on the same document — for the error states. No-op until open. |
canEditOverride | boolean | null | Live server permission override (null = none seen). The host folds it into identity.canEdit. |
schemaVersion | string | null | The document's data-schema version after lazy within-major migration. |
upgradeRequired | boolean | true when the document is a different major than the codec (or below minSchema): render read-only and offer an explicit upgrade — data still reads. |
import { DocumentGate, type ToolRenderProps, useDocument } from "@vvd/sdk"
import { sheetCodec } from "./codec"
export function SheetView({ document, context }: ToolRenderProps) {
const { data, status, actions, retry } = useDocument(
{ worldId: document.worldId, documentId: document.id, scope: context.scope },
sheetCodec,
)
return (
<DocumentGate status={status} onRetry={retry}>
<button
disabled={!context.canEdit}
onClick={() => actions?.list("rows").push({ label: "New row", done: false })}
>
Add row ({data?.rows.length ?? 0})
</button>
</DocumentGate>
)
}See also: defineStateCodec · DocumentGate ·
useDocumentPresence — the loop is walked through in
Build a shared sheet; data-plane rules in the
Retrieving data guide.
Presence
Who else is here. Presence derives from Yjs awareness — a separate protocol from document data — so reading it never touches your codec and never writes the doc.
PresencePeer
One remote (or local) collaborator on an open document, deduped by user. It exists as the one shape every presence hook returns, so a presence strip renders the same peers whether it sits inside the tool or beside it.
| Property | Type | What it does |
|---|---|---|
userId | string | Stable user id — the dedupe key (one user in two tabs counts once). |
name | string | Display name, host-stamped. |
color | string | The user's stable presence/cursor color. |
avatarMediaId | string | null | Their profile picture's media id — resolve via the media capability; fall back to color + initial. |
isSelf | boolean | True for the local user's own awareness state. |
import { type PresencePeer } from "@vvd/sdk"
export function Avatar({ peer }: { peer: PresencePeer }) {
return (
<span
title={peer.isSelf ? `${peer.name} (you)` : peer.name}
style={{ borderColor: peer.color }}
>
{peer.name.slice(0, 1).toUpperCase()}
</span>
)
}See also: useDocumentPresence ·
useDocumentPresenceById — the
Presence guide.
useCollabPresence
Ephemeral per-peer state (cursor, selection, "what I'm looking at") over a shared state room's awareness — set your own small JSON, read everyone's. Reach for it when peers need to see each other doing, not just being there: it's lost on disconnect by design, and identity fields are host-stamped, never author-writable.
useCollabPresence<T>(handle) takes the DocumentHandle of the shared state (usually from
useCollabState) and returns:
| Property | Type | What it does |
|---|---|---|
peers | CollabPresencePeer<T>[] | Every peer: userId, name, color, avatarMediaId, isSelf, plus state: T | null — their last ephemeral payload. |
setLocal | (state: T | null) => void | Publish your own ephemeral state (null clears it). |
import { field, useCollabPresence, useCollabState } from "@vvd/sdk"
export function Cursors() {
const { handle } = useCollabState({ title: field.value("") })
const { peers, setLocal } = useCollabPresence<{ x: number; y: number }>(handle)
return (
<div onPointerMove={(e) => setLocal({ x: e.clientX, y: e.clientY })}>
{peers
.filter((p) => !p.isSelf && p.state)
.map((p) => (
<span
key={p.userId}
style={{ left: p.state!.x, top: p.state!.y, color: p.color }}
>
{p.name}
</span>
))}
</div>
)
}See also: useCollabState · PresencePeer — the
Presence guide.
useCollabState
Typed, live, convergent shared state for this mount — a tool's document, or an app
surface's durable per-instance state doc — declared as a field.* shape, with
peers included. Reach for it in an app (which has no document of its own) or any surface
that wants shared state without managing coordinates: the host's collab capability says
where the state lives.
useCollabState(shape, options?) takes the same arguments as
defineStateCodec and returns everything
useDocument returns, plus:
| Property | Type | What it does |
|---|---|---|
peers | PresencePeer[] | Who's in this shared state right now (deduped, self marked). |
coords | DocumentCoords | Where the state lives — for advanced composition (presence, undo). |
import { field, useCollabState } from "@vvd/sdk"
export function LobbyBanner() {
const { data, actions, peers, status } = useCollabState({
motto: field.value("Welcome, travelers"),
})
if (status !== "ready" || !data) return null
return (
<header>
<input value={data.motto} onChange={(e) => actions?.set("motto", e.target.value)} />
<span>{peers.length} here now</span>
</header>
)
}See also: useDocument · useCollabPresence — app
state in Apps reference → State, routes and tabs.
useDocumentAwareness
The live Yjs Awareness for the document a handle wraps — the raw channel every presence
hook on this page derives from. Reach for it when the typed hooks aren't enough: to hand
awareness through to an SDK collaborative component that draws cursors, or to broadcast a
small ephemeral payload of your own with setLocalStateField.
| Parameter | Type | What it does |
|---|---|---|
handle | DocumentHandle | null | From useDocument, or useCollabState's handle. |
Returns Awareness | null (the y-protocols/awareness type) — null until the document
opens.
Whatever you broadcast over it is ephemeral by contract: it lives only while you're
connected, is never written to the document, and identity fields stay host-stamped. For the
common typed case — publish one small state, read every peer's —
useCollabPresence is the friendlier door; the raw object is for when
you need the protocol itself (the Blocks starter kit
broadcasts each peer's hand position this way).
import { field, useCollabState, useDocumentAwareness } from "@vvd/sdk"
export function Board() {
const { handle } = useCollabState({ title: field.value("") })
const awareness = useDocumentAwareness(handle)
return (
<div
onPointerMove={(e) => awareness?.setLocalStateField("cursor", { x: e.clientX, y: e.clientY })}
onPointerLeave={() => awareness?.setLocalStateField("cursor", null)}
/>
)
}See also: useCollabPresence ·
useDocumentPresence — the
Presence guide.
useDocumentPresence
The deduped peer list on the document a handle wraps — re-renders only on an awareness
change. Reach for it inside the tool that holds the document open: a presence strip, a
"who's editing" row.
| Parameter | Type | What it does |
|---|---|---|
handle | DocumentHandle | null | From useDocument. null (not yet open) returns []. |
Returns PresencePeer[] — deduped by userId, the local user marked isSelf.
import { type DocumentHandle, useDocumentPresence } from "@vvd/sdk"
export function PresenceStrip({ handle }: { handle: DocumentHandle | null }) {
const peers = useDocumentPresence(handle)
const others = peers.filter((p) => !p.isSelf)
if (others.length === 0) return null
return (
<div>
{others.map((p) => (
<span key={p.userId} style={{ background: p.color }}>
{p.name}
</span>
))}
</div>
)
}See also: PresencePeer ·
useDocumentPresenceById — the
Presence guide.
useDocumentPresenceById
Presence for chrome that sits next to a mounted tool — a panel header's avatar row — which
knows a documentId but holds no DocumentHandle (the handle lives inside the tool). It
observes the doc the tool itself has open — no second connection — and returns [] until
the tool has opened it.
| Parameter | Type | What it does |
|---|---|---|
documentId | string | null | The document whose open connection to observe. null returns []. |
Returns PresencePeer[], refreshing on joins, leaves, and identity re-stamps.
import { useDocumentPresenceById } from "@vvd/sdk"
export function PanelHeader({ documentId, title }: { documentId: string; title: string }) {
const peers = useDocumentPresenceById(documentId)
return (
<header>
<h2>{title}</h2>
<span>{peers.filter((p) => !p.isSelf).length} others</span>
</header>
)
}See also: useDocumentPresence (when you do have the handle) ·
PresencePeer.
The host
The single capability seam a tool or block reaches its environment through. The app
implements HostServices once and provides it via <HostProvider>; the same tool runs
against the editor, a wiki, and a published page because only the host object changes.
HostServices
The full capability interface behind useHost() — a lightly namespaced object where each
key is one capability a host may (or must) provide. It exists so a consumer declares exactly
what it needs, a host can honestly omit what it doesn't have, and a missing namespace fails
loud at the boundary instead of as an undefined deref three frames deep.
Every capability, what it gives you, and how a tool consumes it:
| Capability | What it gives you | Consume via |
|---|---|---|
identity | Who's viewing (me, null when anonymous) + the per-document canEdit(coords) gate. Always present. | useHost().identity |
scope | Where this mount's data lives — world canon or a project overlay. Always present. | useHost().scope / useScope() |
media | Resolve media ids to URLs (placeholder sync, resolve async), plus edit-host extras: pick, upload, importUrl, stock, stockGenres. Always present. | useHost().media, useResolvedMedia |
search | Document search for @mentions, pickers, Cmd-K (query, optional create). Grant-gated (readsWorld). | useHostCapability("search") |
refs | Batched id → name/type metadata (meta) and the id → DocumentCoords bridge (coordsFor). Always present. | useHost().refs |
types | The world's entity-type taxonomy: entityType, all, property schema, property mutations, watch, renderIcon. Always present. | useHost().types |
nav | openDocument, peekDocument, hrefFor, canNavigate, renameDocument, and the app subtree route. Always present. | useHost().nav, peekOrOpenDocument(nav, coords) |
embeds | Projected content of referenced documents (fetch/subscribe) + the host's Embed renderer for whole documents. Always present. | useEmbeddedDocument, <HostEmbed> |
presence | Remote collaborators on a document (others, subscribe). Optional — published/anon hosts omit it. | useDocumentPresence (preferred) |
feedback | User feedback about this tool, host-routed (submit, shouldPrompt). Inherited — never in needs. | useFeedback |
tutorial | Per-user tour progress: isCompleted, complete, show(stepId). Inherited. | useHost().tutorial |
analytics | track(name, props?, value?) + screen(name) — fire-and-forget custom actions. Inherited. | host.analytics?.track(...) |
access | canView / canEdit / requireEntitlement on instances — the paid/membership axis. Grant-gated (sharing). | useHostCapability("access") |
projects | List/create app instances (projects) and the documents inside them. Grant-gated (writesWorld). | useHostCapability("projects") |
tab | The project this tab is bound to (project) + setProject to re-bind. Present only when mounted as a tab. | host.tab? |
publish | Freeze one document to a public read-only link (publish). Grant-gated (sharing). | host.publish? — see encodeDocumentSnapshot |
sitePublish | Publish an app instance to the world's domain: status, claimSlug, publish, versions, restore, audiences, entitlements. Grant-gated (sharing). | useHostCapability("sitePublish") |
install | Browse the Workshop catalog + install/manage tools in the world. Edit hosts only — the Workshop tool's special permission. | useHostCapability("install") |
appAssets | The asset-resolution context: bundle base URL, installed versions, per-app manifests. Host-owned. | useAsset, useAssetUrl |
world | The world's catalog, read-only and live: documents, eras, projects, media, index, meta, documentFields, graph. Grant-gated (readsWorld). | useWorldQuery, useWorldMeta |
documents | Typed writes to canonical world documents: applyField, create, archive, restore. Grant-gated (writesWorld). | useHostCapability("documents") |
collab | Where this mount's shared collaborative state lives (stateCoords). Default-granted. | useCollabState |
audio | The one platform audio engine per tab: play, update, stop, buses, focus, spatial, getLevels. Inherited. | useSound, useAudioPlayer |
server | Call your own api/ endpoints as call(path, args) — the tool id is bound per-mount by the host. Present when your creation ships a server bundle. | useApi() |
icons | The icon engine: get, search, packs, resolveSvg over namespaced immutable ids. Inherited. | useIcon, useIcons, <HostIcon> |
menus | The one right-click menu layer: open(request) with pure-data items, close. Inherited. | useContextMenu |
dnd | Cross-surface drag coordination around the native channel: drag state, hover, morph previews. Inherited. | useDocDragSource, useDocDropTarget, useDndState |
undo | The undo/redo engine: registerScope (live providers) and push (invertible structural commands). Inherited. | useUndoScope, useUndoableCommand |
reactions | Like/comment/rating/report on any subject { type, id }: getFeed, toggleLike, postComment, submitReport, … Inherited. | useReactions(subject) |
theme | The viewer's light/dark mode + the resolved token cascade, live. Inherited. | useHostTheme() |
locale | The reader's language + the catalogs your creation shipped, live-switchable. Inherited. | useHostLocale(), useT() |
defaults | Per-world tool defaults: one opaque JSON blob per (toolId, key) — get, set, subscribe. Inherited. | useToolDefaults |
focusMode | The shell's chrome-hidden view: getState, set, toggle, subscribe. Inherited. | useFocusMode() |
runtime | Document-runtime config (socket/token/readOnly). Host-internal — never call it from a tool. | — |
Three kinds, three habits: always-present capabilities you just read; grant-gated
ones you list in needs and consume via useHostCapability (fails loud, by name);
inherited ones you never declare — their hooks degrade quietly under a host that omits
them (icons fall back to a glyph, sound goes silent, a right-click hands the event back to
the browser).
import { useHost } from "@vvd/sdk"
export function RollButton({ onRoll }: { onRoll: () => number }) {
const host = useHost()
return (
<button
onClick={() => {
const value = onRoll()
host.analytics?.track("rolled", { value }) // inherited: optional-chain, never in needs
}}
>
Roll
</button>
)
}See also: useHost · useHostCapability — the full
kind-by-kind treatment in
Tools reference → Host capabilities and the
Host guide.
loadThree
The platform's shared three.js, loaded lazily. Three.js is around a megabyte, so no creation bundles its own copy: the platform ships one, code-split, and a creation that wants 3D just awaits this. A plain export rather than a capability — call it anywhere, not through the host — and nothing is fetched until the first call, so a creation that never shows a model pays nothing.
loadThree(): Promise<ThreeKit> — no parameters. Resolves to ThreeKit: three's module
namespace merged with the bundled example loaders (OBJLoader, GLTFLoader,
STLLoader, PLYLoader, FBXLoader), so T.Scene and new T.OBJLoader() both resolve
off the one object. The promise is cached: repeat and concurrent callers share a single
in-flight import, and every creation in the session shares the one three instance — which is
what keeps loader output and instanceof checks agreeing across creations. (ThreeKit is
typed loosely — three ships no SDK types.)
import { loadThree } from "@vvd/sdk"
export async function loadModel(url: string) {
const T = await loadThree()
const model = await new T.OBJLoader().loadAsync(url)
return model
}See also: the 3D Model starter kit — a whole viewer built on it.
useHost
Returns the full HostServices for this mount — the whole seam in one read. Reach for it in
code touching several namespaces at once (a tool's main edit view); it throws outside a
<HostProvider>, which in practice means "you rendered a tool outside a host — wrap tests
in createFakeHost".
No parameters. Returns HostServices.
import { peekOrOpenDocument, useHost } from "@vvd/sdk"
export function LinkedCard({
worldId,
id,
name,
}: {
worldId: string
id: string
name: string
}) {
const { nav, refs } = useHost()
return (
<button
onClick={() =>
peekOrOpenDocument(nav, refs.coordsFor(id) ?? { worldId, documentId: id })
}
>
{name}
</button>
)
}See also: useHostCapability · HostServices ·
createFakeHost — the Host guide.
useHostCapability
Returns exactly one named capability, throwing a named error when the host doesn't provide
it. Reach for it for every grant-gated capability (world, projects, search,
documents, install, …): the failure surfaces at the boundary, with the capability's
name, instead of as an undefined deref deep in a leaf.
| Parameter | Type | What it does |
|---|---|---|
name | HostCapabilityName | The namespace key — any key of HostServices. |
Returns the non-null capability. Throws
Host capability "<name>" is not provided by this host … when absent — the message you'll
see when a grant is missing from vvd.json.
import { useHostCapability } from "@vvd/sdk"
export function NewSpaceButton() {
const projects = useHostCapability("projects") // needs the writesWorld grant
return (
<button onClick={() => projects.create("lobby", "A new space")}>New space</button>
)
}See also: useHost · HostServices — grant mapping in
Apps reference → Owning a space.
Capability hooks
The per-capability hooks the HostServices table's "Consume via" column
names — each wraps one namespace with the right consumption habit built in (defensive for
inherited capabilities, fail-loud for grant-gated ones). Short entries: what it is, its
signature, the capability it rides.
useAsset
Resolves a tool/app asset slot to a URL through the override cascade — doc override
?? install override ?? the shipped default from the AssetManifest.
Rides appAssets (host-owned) + media.
useAsset(app: string, slotKey: string, overrides?: AssetOverrideLayers): string | null
null until resolved, or when the slot is neither overridden nor declared. "Revert to
default" is just dropping the slot from overrides.
useAssetUrl
Resolves any AssetRef (a bare world_media id string, { kind: "media", id }, or
{ kind: "app", app, key, version? }) to a URL in two phases: the sync placeholder paints
first, then the async truth settles. Rides appAssets + media.
useAssetUrl(ref: AssetRef | null | undefined): string | null
The consumer can't tell whether the bytes came from the app bundle or the world media pool — which is the point.
useDndState
Live discrete drag state — dragging / payload / hovered zone — re-rendering on coarse
changes only, never at pointer rate. Rides dnd (inherited).
useDndState(): DndSnapshot
Under a dnd-less host it returns the inert snapshot, so if (state.dragging) affordances
simply never light up.
useDocDragSource
Makes an element a cross-surface document drag source: spread the result
({ draggable, onDragStart, onDragEnd }) on the row or handle. Rides dnd (inherited).
useDocDragSource(payload: DocDragPayload | null): DocDragSourceProps
Writes the payload to dataTransfer at dragstart and mirrors it into the host's engine so
targets can validate during dragover and the platform ghost can morph; under a dnd-less
host it degrades to a plain native drag. null payload renders the element non-draggable.
useDocDropTarget
Makes an element a document drop target on both input channels: spread the returned
props (native HTML5 drag events) and attach the returned ref (pointer/touch drags) to
the same element. Rides dnd (inherited).
useDocDropTarget(opts: { zone: string; preview?: DropPreviewKind; accepts?: (payload) => boolean; onDrop: (payload, point) => void }): { props; ref; isOver; canDrop }
accepts filters during dragover and is re-checked on drop; onDrop gets the payload
plus the drop point in client coordinates. Works unchanged under a dnd-less host — native
drops still land, nothing morphs.
useEmbeddedDocument
The projected-embed read for a referenced document (map tiles, timeline spans): prefers
the host's live subscribe, falls back to fetch on published/baked surfaces — the tool
never branches on host. Rides embeds (always present).
useEmbeddedDocument<T>(coords: DocumentCoords | null): { data: T | null; loading: boolean }
null coords (an unresolvable ref) answers { data: null, loading: false } — settled and
empty, never an eternal spinner. For a raw-Yjs embed of another document, use
useDocument with refs.coordsFor(id) instead.
useFeedback
Submit user feedback about this creation through the host's one feedback layer — the host
stamps which tool it came from (un-spoofable) and routes it. Rides feedback (inherited).
useFeedback(): { submit(feedback: FeedbackSubmission): Promise<boolean>; shouldPrompt(trigger: string): boolean }
submit resolves false — touching nothing — under a feedback-less host or on empty text;
shouldPrompt is the host-owned cooldown gate for proactive prompts.
useFocusMode
The shell's chrome-hidden view as a switch your UI can read and drive. Rides focusMode
(inherited).
useFocusMode(): { available: boolean; active: boolean; set(active: boolean): void; toggle(): void }
available is false under hosts with no chrome to hide (published/anon/test) — hide your
toggle button on it; the mutators no-op there.
useHostLocale
The reader's language plus the locale catalogs your creation shipped, re-rendering live
when the reader switches language. Rides locale (inherited).
useHostLocale(): HostLocaleSnapshot — { locale, supported, defaultLocale, messages, defaultMessages }
Read it for the language list a picker offers (supported is the creation's locales, not
the platform's) or the active code; for words, use useT. Under a locale-less
host it returns the frozen en fallback.
useOpenWorldDocumentFields
Live typed field values from documents currently open in this client — the host performs
the codec read, so a tool receives only fields, never another tool's Y.Doc or codec (the
zero-copy bridge projections like Table use). Rides world (grant-gated, readsWorld).
useOpenWorldDocumentFields(input: { documentType: string; entityTypeId?: string; fields: { id: string; field: WorldDocumentField }[] }): readonly WorldDocumentFieldRow[]
Closed documents are intentionally absent — the durable world index remains their source.
useReactions
A subject's like/comment/report feed with optimistic mutations — one fetch seeds it, every
gesture patches it, the service confirms or rolls back. Rides reactions (inherited).
useReactions(subject: { type, id } | null | undefined, opts?): ReactionsState
Returns the feed plus isLoading, unavailable (no capability, or the viewer can't see
the subject — render nothing), signedOut, can(type), and toggleLike /
toggleDislike / postComment / deleteComment / report. Reports are deliberately
not optimistic — filing one waits for the server.
useScope
The mount's Scope — the same value as useHost().scope, readable without pulling the
whole host object. Rides scope (always present).
useScope(): Scope — { type: "world", worldId } or { type: "project", worldId, projectId }
Throws outside a <ScopeProvider>; the host's dispatch (ToolHost) wraps every tool mount
in one.
useT
Your creation's own strings in the reader's language — the translator over
useHostLocale. Rides locale (inherited).
useT(): Translator — t(key, params?), plus t.plural(key, count), t.date(when), t.number(n), t.locale
Never throws and never renders an empty box: a missing catalog falls to the base language, then the creation's default, then the key's own text — so writing default-language strings as the keys is a fine way to adopt it one string at a time.
useUndoableCommand
Push an invertible structural command onto the host's undo engine — the data-plane half
of undo. Mutate first (optimistically, through your normal write path), then record the
inverse. Rides undo (inherited).
useUndoableCommand(): (command: UndoableCommand) => boolean
A command is { labels, undo(): Promise<boolean>, redo(): Promise<boolean>, isStale?, batchKey? } —
consecutive pushes sharing a batchKey coalesce into one Cmd+Z. Returns false (recording
nothing) under an undo-less host; the mutation itself already happened.
useUndoScope
Register a live undo provider (a library's own stack — tldraw, a custom model) with the
host's engine, anchored in the focus tree so Cmd+Z routes to the surface the user is
looking at. Rides undo (inherited).
useUndoScope(handle: UndoScopeHandle | null): void
The handle is { canUndo(), canRedo(), undo(), redo(), label? }; pass null to stand down
(read-only views). useDocument-backed tools never call this — the document surface
registers for them. No-op under an undo-less host.
World data
Live, read-only reads over the world's catalog — the same collections the platform renders
from, projected through the contract. All of it rides the world capability (the
readsWorld grant); content stays behind the document seam — a row gives you ids and names
to feed useDocument or an embed.
useUseCase
What the world you're mounted in is for — its declared use cases, first one primary. Reach for it to match the platform's voice ("roll for it" in a campaign world, "try it" elsewhere) without owning a variant schema; it's a hint, so match the values you know and fall through to neutral wording.
No parameters. Returns:
| Property | Type | What it does |
|---|---|---|
all | readonly string[] | Every declared use case. Freeform — may include retired ids. |
primary | string | null | The first entry — the world's "voice". null on older hosts, published surfaces, and worlds that declared nothing. |
import { useUseCase } from "@vvd/sdk"
export function EmptyState() {
const { primary } = useUseCase()
return <p>{primary === "campaign" ? "Roll for it." : "Try it."}</p>
}See also: useWorldMeta (the row it reads from) — the
World data guide.
useWorldMeta
The world's own header — identity, presentation, theme — live. Reach for it to render the
world's name and wear its look; returns null when the host doesn't provide it, and fails
loud only on the missing world capability itself.
No parameters. Returns WorldMetaRow | null:
| Property | Type | What it does |
|---|---|---|
id | string | The world id. |
name | string | The world's name. |
slug | string | The world's slug. |
description | string | null | The world's description. |
avatarMediaId | string | null | The world's avatar — resolve via the media capability. |
creator | WorldCreator | null | The owner, display-resolved (name, avatarUrl). Optional — render no byline when absent. |
genres | readonly string[] | The world's genre ids. Optional. |
useCases | readonly string[] | What the world is for — see useUseCase. Optional. |
theme | Record<string, string> | CSS custom-property tokens derived host-side. {} = platform default. |
import { useWorldMeta } from "@vvd/sdk"
export function WorldHeader() {
const meta = useWorldMeta()
if (!meta) return null
return <h1 style={{ color: meta.theme["--primary"] }}>{meta.name}</h1>
}See also: useWorldQuery · the theme row of
HostServices — the World data guide.
useWorldQuery
Live React reads over the world's catalog — the component re-renders as the world changes on any client. This is the standard read for "list the world's X": no fetch, no cache of your own, no snapshot that silently goes stale.
useWorldQuery(kind, filter?):
kind | filter | Row type (key fields) |
|---|---|---|
"documents" | { type?: string } | WorldDocumentRow — id, name, slug, documentType, updatedAt, card-header extras (entityTypeId, avatarMediaId, aliases, isViewable). |
"eras" | — | WorldEraRow — id, name, color, themeConfig. |
"projects" | — | WorldProjectRow — id, name, slug, kind, status. |
"media" | { type?: string } | WorldMediaRow — id, mediaType, filename. |
"index" | { type?: string; where?: IndexWhere } | WorldIndexRow — id, docType, fields (what the tool's index selectors extracted), updatedAt. where filters client-side on those fields. |
Fails loud (naming the capability) when the install didn't grant readsWorld. Three
siblings cover the edges: useWorldDocumentsOptional / useWorldMediaOptional return []
instead of throwing on a world-less host (for UI that merely enriches), and
useOpenWorldDocumentFields streams typed fields from
documents currently open in this client (the zero-copy bridge projections like Table use).
import { useWorldQuery } from "@vvd/sdk"
export function Roster() {
const cards = useWorldQuery("documents", { type: "card" })
return (
<ul>
{cards.map((c) => (
<li key={c.id}>{c.name}</li>
))}
</ul>
)
}See also: useWorldMeta · useHostCapability — the
World data guide; the index grammar in
Tools reference → Search and the index.
Testing
The dependency-free harness: a complete host of fakes, so a tool renders and unit-tests
headless against the same useHost() contract it ships against.
createFakeHost
Builds a complete HostServices of inert fakes — no network, no Yjs server, permissive
identity — for rendering a tool under <HostProvider> in tests, Storybook, and previews.
Reach for it in every tool test; swap in a recording fake for any capability you want to
assert the tool actually called.
| Parameter | Type | What it does |
|---|---|---|
overrides | Partial<HostServices> | Any capability you supply replaces the fake — pass a recording fake here, or your own fixture (types.schema, a seeded world). Default scope: { type: "world", worldId: "fake-world" }. |
Returns a full HostServices. The recording twins, each returning the
capability plus an array you assert on:
| Factory | Records |
|---|---|
createRecordingAnalytics() | events — every track(name, props) call. |
createRecordingAudio() | commands — plays, stops, patches. |
createRecordingDefaults() | sets — every defaults set. |
createRecordingDnd() | commands — drag/hover coordination. Pair with createDocDataTransfer(payload) for drop events. |
createRecordingFeedback() | submissions — every feedback.submit. |
createRecordingFocusMode() | commands — set/toggle calls. |
createRecordingIcons(seed?) | requests — icon resolutions. |
createRecordingLocale(seed?) | snapshots — every published locale snapshot — plus a setLocale(code) test driver. Seed it with the creation's own catalogs ({ locale?, catalogs?, defaultLocale?, supported? }), drive setLocale("fr"), and assert every string changed. |
createRecordingMenus() | commands — every opened menu request. |
createRecordingNav() | commands — opens, peeks, renames. |
createRecordingReactions() | commands — likes, comments, reports. |
createRecordingSearch() | queries — every search query. |
createRecordingTheme() | a driveable theme snapshot. |
createRecordingUndo() | commands — registered scopes, pushed commands. |
import { render, screen } from "@testing-library/react"
import { HostProvider, createFakeHost, createRecordingAnalytics } from "@vvd/sdk"
import sheetTool from "./tool"
it("renders and tracks under a fake host", () => {
const { analytics, events } = createRecordingAnalytics()
const host = createFakeHost({ analytics })
const Tool = sheetTool.render
render(
<HostProvider services={host}>
<Tool
document={{
id: "d1",
worldId: host.scope.worldId,
type: "sheet",
title: "Sheet",
scope: host.scope,
visibility: "shown",
}}
context={{ scope: host.scope, canEdit: true, view: "editor" }}
/>
</HostProvider>,
)
screen.getByRole("button", { name: /add row/i }).click()
expect(events.map((e) => e.name)).toContain("row_added")
})See also: useHost · HostServices — the
Testing guide; server-side twins (createFakeServerContext) in
Tools reference → Server endpoints.