Skip to content
Reference— browse docs
On this page

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:

PropertyTypeWhat it does
pathstringWhere the file lives inside the bundle's assets/ dir: "audio/rain.ogg". Required.
kind"audio" | "image" | "font" | "video" | "data"What the slot holds. Required.
metaRecord<string, unknown>Computed at build (size, sha256, image w/h, audio durationMs) plus declared semantics the machine can't infer (loop, gainDb).
replaceablebooleanMay an end-user override this slot? Default true (content); set false for UI chrome.
acceptstring[]Accept filter for a replacement, e.g. ["audio/*"].
aliasesstring[]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.

src/fixtures.ts
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.

PropertyTypeWhat it does
idstringStable id — registry key, per-world enablement key, app-switcher key. Required.
namestringHuman label — the world-shell tab and app-switcher entry. Required.
templateNamestringWhat the app calls itself in a template list (a layout picker), where every row is already this kind of app. Absent = name.
routestringThe URL segment it mounts at: /worlds/<world-slug>/<route>. One lowercase segment; two apps can't share one. Required.
subRoutesbooleanThe 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.
HostComponentType<AppHostProps>The app's data-bound useHost() provider — it renders <HostProvider services={…}> around children. Required.
SurfaceComponentType<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.
toolsToolRegistryThe tools this app composes — dispatch by document type happens against this registry. Required.
readOnlybooleanStatic edit stance (a shell-level hint). Runtime truth stays identity.canEdit(coords). Default false.
toursTutorialTour[]Guided walkthroughs the host plays — same declarative contract as a tool's tours.
versionstringThe app's own semver, for listing and updates. Default "0.0.0".
enginesPartial<AppEngines>Minimum contract versions it needs. Default { host: 1, app: 1 } — leave it alone until you have a reason.
capabilitiesHostCapabilityName[]The install-consent surface. Derive it with deriveCapabilities(tools, extra) so it can't drift from what the code calls. Default [].
authorAppAuthorAttribution for the listing: name plus optional url.
iconstringIcon id (resolved by the host's icon engine) for the switcher / listing.
descriptionstringOne-line human description for the switcher / listing.
categorystringLauncher/storefront grouping slug — e.g. "site" for apps whose job is "be a website for your world".
stateClassstringThe 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.
assetsAssetManifestThe developer-owned, version-locked asset pack this app ships (audio, images, fonts), resolved via useAsset.
publishAppPublishConfigThe 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.
customizationAppCustomizationParam[]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().
accessAppAccessPolicyReserved 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.

src/app.tsx
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>.

PropertyTypeWhat it does
typestringThe block's registration key — what a section stores and <BlockSlot type> resolves. Required.
namestringHuman 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.
needsHostCapabilityName[]Capabilities this block consumes. BlockHost fail-fasts on a missing one with a named boundary in the block's place, not an undefined deref.
renderComponentType<BlockRenderProps<TData>>The renderer. Required.
addablebooleanMay 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>:

PropTypeWhat it does
blockBlockInstance<TData>id, type, and the typed data bag from the host document's section.
canEditbooleanMirror of the host's edit gate for the owning document.
onUpdate(updates: Partial<TData>) => voidPatch block.data through the owning document's codec — a block never opens its own connection.
handleDocumentHandle | nullThe 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".
src/blocks.tsx
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.

PropertyTypeWhat it does
read(doc) => TDataRead a typed snapshot. No Yjs leaks past the return value. Required.
observe(doc, onChange) => () => voidWatch the parts that affect the snapshot; return the unsubscribe. Required.
actions(doc) => TActionsTyped mutators bound to the document.
isEmpty(doc) => booleanIs 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.
seedUint8ArrayPlaceholder state (build with buildSeed) applied only to a fresh, empty doc in local no-socket mode — previews, Storybook. Never applied to a live doc.
toolIdstringThe id of the definition that interprets this document (dispatch metadata).
schemaVersionstringCurrent data-schema semver this codec reads/writes. Default "1.0.0".
minSchemastringOldest version it can still read + migrate. A document below it gates as upgradeRequired instead of silently breaking.
migrate(doc, fromVersion) => voidWithin-major migration up to schemaVersion. Must be idempotent and CRDT-convergent — it can run on multiple clients concurrently.
agentActionsRecord<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.
agentSummarystringOne line describing what a document of this type is, for agent discovery.
stateShapeStateShapeSpecThe 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.
historyCodecHistory<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.
src/codec.ts
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.",
})
Warning:

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:

PropertyTypeWhat it does
toolIdstringPassed through to the codec (dispatch metadata).
agentSummarystringOne line describing what a document of this type is — required for every registered type's agent discovery.
schemaVersionstringData-schema semver. Adding fields never needs a bump — a missing field reads as its default. Default "1.0.0".
minSchemastringOldest readable version (the support window).
migrate(doc, fromVersion) => voidNon-additive evolution (renames, reshapes) — the standard codec migrate.

The returned codec's actions (what useDocument(...).actions gives you):

ActionSignatureWhat it does
setset(name, value)Set a value field (last-write-wins).
updateupdate(name, fn)Read-modify-write a value field in one transaction.
listlist(name)Positional ops on a list field: push, insert(index, …items), remove(index, count?), replace(index, item), move(from, to).
mapmap(name)Per-key ops on a map field: set(key, value), delete(key).
transacttransact(fn)Batch several actions into one undo step / one broadcast.
src/codec.ts
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.

PropertyTypeWhat it does
idstringStable forever — analytics, tours, secrets and the Workshop key off it. Required.
namestringWhat a human sees: the Workshop listing, the "new document" menu. Required.
documentTypesstring[]The document types this tool renders/edits — its only registration. Required.
renderComponentType<ToolRenderProps<TContent>>The one render, mounted in all three views. Required.
capabilitiesToolCapabilitiesThe tool's relationship to world data: readsWorld, writesWorld, supportsProjectScope, supportsSharing (all optional booleans). Distinct from needs, which names host services.
needsHostCapabilityName[]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.
SkeletonComponentTypeConnecting-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.
panelTitlebooleanWhether the host draws its rename-in-place document title above the tool. Default true; set false when your own UI already shows the title.
versionstringThe tool's own semver — pins which immutable asset bundle its app-asset refs resolve against. Default "0.0.0".
assetsAssetManifestThe asset pack this tool ships (audio, images, fonts), overridable per-document, resolved via useAsset.
systembooleanMarks an internal editor surface (not a catalog tool) — analytics skips it entirely. Absent = a real catalog tool.
toursTutorialTour[]Declarative guided walkthroughs the host plays — pure data, no components or handlers.
indexToolIndexSpecWhat's queryable about this tool's documents — serializable jsonpath selectors extracted into projection rows on every save.
codecDocumentCodecThe 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.
src/tool.tsx
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.

ConstructorSignatureMerge behaviorNotes
field.valuefield.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.listfield.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.mapfield.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.prosefield.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.

src/codec.ts
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.

PropTypeWhat it does
handleDocumentHandle | nullThe opaque handle from useDocument — connects the editor to the open document. Pass the handle, not data. Required.
fieldstringThe prose field's token, read straight off your typed snapshot (data.notes). Required.
editablebooleanYours to set — the component does not read host permissions. Default true.
placeholderstringShown while the field is empty.
seedunknownThe 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.
classNamestringStyling passthrough.
extensionsExtensionsExtra TipTap extensions (mentions, slash commands). Must be a stable reference — a fresh array each render rebuilds the editor and drops the caret. Default [].
src/tool.tsx
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.

PropTypeWhat it does
statusDocumentStatusFrom useDocument. auth_error / error / disconnected → the shared <DocumentError> card; connecting → the skeleton; ready → children. Required.
onRetry() => voidWire to useDocument().retry — the error card's Retry button.
skeletonComponentTypeConnecting-state component. Lowercase here; capital Skeleton on the tool definition. Falls back to DefaultDocumentSkeleton.
childrenReactNodeRendered when ready. Required.
src/tool.tsx
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(...).

ParameterTypeWhat it does
handleDocumentHandle | nullThe 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.

src/publish-button.tsx
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.

PropTypeWhat it does
documentVvdDocument<TContent>Identity only — turn it into live data with useDocument.
contextToolContextWhat the platform can't otherwise tell you about this mount.

document (VvdDocument):

PropertyTypeWhat it does
idstringThe document id.
worldIdstringThe world it lives in.
typestringThe document_type discriminator — what resolved your tool.
titlestringThe document's name (host-owned; rename via nav.renameDocument).
scopeScopeWhere the data lives: { type: "world", worldId } or { type: "project", worldId, projectId }.
visibility"shown" | "hidden"Whether it is shown or hidden.
contentTContentOptional tool-specific payload on hosts that inject one — live data still comes from useDocument.

context (ToolContext):

PropertyTypeWhat it does
scopeScopeThe world (and optionally project) this mount is scoped to.
canEditbooleanMay 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.
handleDocumentHandle | nullPopulated after useDocument for blocks that bind <CollaborativeText>.
src/tool.tsx
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):

ParameterTypeWhat it does
coordsDocumentCoordsWhich document: worldId, documentId, optional scope, optional era (pin to a specific era; omitted = the reader's ambient lens).
codecDocumentCodec<TData, TActions>How to interpret it — from defineStateCodec or defineDocumentCodec. Treat as mount-constant.

Returns UseDocumentResult<TData, TActions>:

PropertyTypeWhat it does
dataTData | nullTyped, reactive snapshot — null until the document is open. Referentially stable between unrelated re-renders.
statusDocumentStatus"connecting" | "ready" | "disconnected" | "auth_error" | "error" — feed it to <DocumentGate>.
actionsTActions | nullTyped mutators — null until open. Writes sync automatically.
handleDocumentHandle | nullOpaque handle for SDK collaborative components (<CollaborativeText>, presence hooks, encodeDocumentSnapshot). Never read its internals.
retry() => voidDestroy + rebuild the connection on the same document — for the error states. No-op until open.
canEditOverrideboolean | nullLive server permission override (null = none seen). The host folds it into identity.canEdit.
schemaVersionstring | nullThe document's data-schema version after lazy within-major migration.
upgradeRequiredbooleantrue when the document is a different major than the codec (or below minSchema): render read-only and offer an explicit upgrade — data still reads.
src/tool.tsx
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.

PropertyTypeWhat it does
userIdstringStable user id — the dedupe key (one user in two tabs counts once).
namestringDisplay name, host-stamped.
colorstringThe user's stable presence/cursor color.
avatarMediaIdstring | nullTheir profile picture's media id — resolve via the media capability; fall back to color + initial.
isSelfbooleanTrue for the local user's own awareness state.
src/avatar.tsx
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:

PropertyTypeWhat it does
peersCollabPresencePeer<T>[]Every peer: userId, name, color, avatarMediaId, isSelf, plus state: T | null — their last ephemeral payload.
setLocal(state: T | null) => voidPublish your own ephemeral state (null clears it).
src/cursors.tsx
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:

PropertyTypeWhat it does
peersPresencePeer[]Who's in this shared state right now (deduped, self marked).
coordsDocumentCoordsWhere the state lives — for advanced composition (presence, undo).
src/app-view.tsx
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.

ParameterTypeWhat it does
handleDocumentHandle | nullFrom 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).

src/hands.tsx
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.

ParameterTypeWhat it does
handleDocumentHandle | nullFrom useDocument. null (not yet open) returns [].

Returns PresencePeer[] — deduped by userId, the local user marked isSelf.

src/presence-strip.tsx
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.

ParameterTypeWhat it does
documentIdstring | nullThe document whose open connection to observe. null returns [].

Returns PresencePeer[], refreshing on joins, leaves, and identity re-stamps.

src/panel-header.tsx
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:

CapabilityWhat it gives youConsume via
identityWho's viewing (me, null when anonymous) + the per-document canEdit(coords) gate. Always present.useHost().identity
scopeWhere this mount's data lives — world canon or a project overlay. Always present.useHost().scope / useScope()
mediaResolve media ids to URLs (placeholder sync, resolve async), plus edit-host extras: pick, upload, importUrl, stock, stockGenres. Always present.useHost().media, useResolvedMedia
searchDocument search for @mentions, pickers, Cmd-K (query, optional create). Grant-gated (readsWorld).useHostCapability("search")
refsBatched id → name/type metadata (meta) and the id → DocumentCoords bridge (coordsFor). Always present.useHost().refs
typesThe world's entity-type taxonomy: entityType, all, property schema, property mutations, watch, renderIcon. Always present.useHost().types
navopenDocument, peekDocument, hrefFor, canNavigate, renameDocument, and the app subtree route. Always present.useHost().nav, peekOrOpenDocument(nav, coords)
embedsProjected content of referenced documents (fetch/subscribe) + the host's Embed renderer for whole documents. Always present.useEmbeddedDocument, <HostEmbed>
presenceRemote collaborators on a document (others, subscribe). Optional — published/anon hosts omit it.useDocumentPresence (preferred)
feedbackUser feedback about this tool, host-routed (submit, shouldPrompt). Inherited — never in needs.useFeedback
tutorialPer-user tour progress: isCompleted, complete, show(stepId). Inherited.useHost().tutorial
analyticstrack(name, props?, value?) + screen(name) — fire-and-forget custom actions. Inherited.host.analytics?.track(...)
accesscanView / canEdit / requireEntitlement on instances — the paid/membership axis. Grant-gated (sharing).useHostCapability("access")
projectsList/create app instances (projects) and the documents inside them. Grant-gated (writesWorld).useHostCapability("projects")
tabThe project this tab is bound to (project) + setProject to re-bind. Present only when mounted as a tab.host.tab?
publishFreeze one document to a public read-only link (publish). Grant-gated (sharing).host.publish? — see encodeDocumentSnapshot
sitePublishPublish an app instance to the world's domain: status, claimSlug, publish, versions, restore, audiences, entitlements. Grant-gated (sharing).useHostCapability("sitePublish")
installBrowse the Workshop catalog + install/manage tools in the world. Edit hosts only — the Workshop tool's special permission.useHostCapability("install")
appAssetsThe asset-resolution context: bundle base URL, installed versions, per-app manifests. Host-owned.useAsset, useAssetUrl
worldThe world's catalog, read-only and live: documents, eras, projects, media, index, meta, documentFields, graph. Grant-gated (readsWorld).useWorldQuery, useWorldMeta
documentsTyped writes to canonical world documents: applyField, create, archive, restore. Grant-gated (writesWorld).useHostCapability("documents")
collabWhere this mount's shared collaborative state lives (stateCoords). Default-granted.useCollabState
audioThe one platform audio engine per tab: play, update, stop, buses, focus, spatial, getLevels. Inherited.useSound, useAudioPlayer
serverCall 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()
iconsThe icon engine: get, search, packs, resolveSvg over namespaced immutable ids. Inherited.useIcon, useIcons, <HostIcon>
menusThe one right-click menu layer: open(request) with pure-data items, close. Inherited.useContextMenu
dndCross-surface drag coordination around the native channel: drag state, hover, morph previews. Inherited.useDocDragSource, useDocDropTarget, useDndState
undoThe undo/redo engine: registerScope (live providers) and push (invertible structural commands). Inherited.useUndoScope, useUndoableCommand
reactionsLike/comment/rating/report on any subject { type, id }: getFeed, toggleLike, postComment, submitReport, … Inherited.useReactions(subject)
themeThe viewer's light/dark mode + the resolved token cascade, live. Inherited.useHostTheme()
localeThe reader's language + the catalogs your creation shipped, live-switchable. Inherited.useHostLocale(), useT()
defaultsPer-world tool defaults: one opaque JSON blob per (toolId, key) — get, set, subscribe. Inherited.useToolDefaults
focusModeThe shell's chrome-hidden view: getState, set, toggle, subscribe. Inherited.useFocusMode()
runtimeDocument-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).

src/tool.tsx
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.)

src/viewer.ts
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.

src/tool.tsx
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.

ParameterTypeWhat it does
nameHostCapabilityNameThe 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.

src/spaces.tsx
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:

PropertyTypeWhat it does
allreadonly string[]Every declared use case. Freeform — may include retired ids.
primarystring | nullThe first entry — the world's "voice". null on older hosts, published surfaces, and worlds that declared nothing.
src/empty-state.tsx
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:

PropertyTypeWhat it does
idstringThe world id.
namestringThe world's name.
slugstringThe world's slug.
descriptionstring | nullThe world's description.
avatarMediaIdstring | nullThe world's avatar — resolve via the media capability.
creatorWorldCreator | nullThe owner, display-resolved (name, avatarUrl). Optional — render no byline when absent.
genresreadonly string[]The world's genre ids. Optional.
useCasesreadonly string[]What the world is for — see useUseCase. Optional.
themeRecord<string, string>CSS custom-property tokens derived host-side. {} = platform default.
src/header.tsx
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?):

kindfilterRow 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).

src/roster.tsx
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.

ParameterTypeWhat it does
overridesPartial<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:

FactoryRecords
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.
src/tool.test.tsx
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.