The host
One object connects your creation to the platform — and it is the reason your tool runs unchanged in four different places.
Your creation will run in more places than you're picturing. The world editor. Inside somebody's card as an embedded block. On a published wiki that a stranger reads without an account. On a share link. In the examples on this page.
In three of those there is no session to authenticate with, and in one of them there is no
server at all. So the moment you write fetch("/api/…") — or reach for a database client —
your creation stops being portable and starts being a thing that works on exactly one
surface.
The host is the alternative.
You will learn
- What
useHost()anduseHostCapability()give you - Which capabilities you declare in
needs, and which you get without asking - Why a missing capability throws instead of returning
undefined
One object, many namespaces
import { useHost, useHostCapability } from "@vvd/sdk"
const host = useHost() // the whole surface
const nav = useHostCapability("nav") // one namespace, fail-fastuseHost() is the whole thing — reach for it when you're touching several namespaces.
useHostCapability("nav") is the discoverable form, and the one you should usually use: if
the host doesn't provide that namespace, it throws there, naming it, instead of handing
you undefined to deref three frames deeper.
What's on it
| Namespace | For |
|---|---|
identity | who the reader is, and whether they may edit |
scope | which world and project you're in |
media | resolving media ids to URLs, and letting the user pick or upload |
nav | opening a document, building an href, pushing a route |
refs | metadata about a referenced document |
types | the world's entity types (Character, Location, …) |
world | the world's live catalog — Reading the world |
search | full-text search across the world |
embeds | fetching and subscribing to embedded document content |
documents · projects | creating and mutating world documents and app instances |
runtime | the document connection itself — the SDK uses this; you don't |
The two kinds of capability
This distinction saves you a lot of confusion:
Capabilities you declare. world, search, documents, projects, publish,
access — these carry world data or change things, so they're gated by an install grant and
belong in needs and capabilities in your manifest.
Capabilities you inherit. Icons, audio, context menus, drag-and-drop, analytics, theme,
reactions — these are cross-cutting. Every host provides them, they carry no world content,
and they never go in needs. Under a host that omits one, they degrade quietly rather
than throwing: a sound doesn't play, a right-click falls through to the browser's own menu.
import { HostIcon, useHost } from "@vvd/sdk"
// Inherited — no declaration, no consent prompt, works everywhere.
<HostIcon icon="compass" size={16} />
useHost().analytics?.track("creature_logged", { threat: 3 })Reading the host, live
The tool below reads three things straight off the host: the icon engine, identity, and
whether this surface allows editing. Everything it shows is real — this docs page is
another host.
Swap the host and every one of those values changes, without the tool changing at all. That is the entire argument for the seam.
needs and consent at mount
needs is the list of host capability namespaces your code calls by name:
{
"needs": ["world", "nav"],
"capabilities": { "readsWorld": true }
}When someone installs your creation, the platform derives the consent screen from that declaration — so what the user is asked to approve can't drift from what your code actually calls. When it mounts, the host it gets is built with only the granted namespaces on it, so a missing one throws at the boundary, naming the capability — a creation cannot quietly exceed its consent. Reading the world shows the failure, and the fix, in full.
The rule, stated once
Reach the platform through the host, never around it.
No fetch to vvd's API from inside a creation. No database client. No localStorage for
anything that ought to be shared. If you find yourself wanting one of those, the thing you
want is almost always a host capability — and if it genuinely isn't, it's a
server endpoint in your project's api/ folder, which is the one sanctioned
way to talk to the outside world.
Two practical consequences worth internalising:
- Media never gets a hardcoded URL. Store a media id, resolve it through
host.media. URLs expire, and private media is only reachable through the platform's own door. - Permission is never computed by you.
context.canEditandhost.identityalready fold in membership, sharing, published read-only views, and live permission changes.
When it doesn't work
useHost() must be used within a <HostProvider>.
You're rendering your component outside the tool mount — usually in a test. Wrap it:
<HostProvider services={createFakeHost()}>. See Testing.
Host capability "x" is not provided by this host.
Either the install wasn't granted it (add it to capabilities / needs and re-save), or
you're on a surface that genuinely doesn't have it — a logged-out published page has no
search, for example. Decide which, and either declare it or handle its absence.
An icon renders as a blank square.
The id didn't resolve. Icon ids are namespaced ("tabler:sword"); bare lucide names like
"compass" still resolve for compatibility. An unknown id falls back rather than throwing.
host.analytics?.track seems to do nothing.
It's fire-and-forget, and there's no analytics sink on every surface. That's why it's
optional-chained. Events land in your creation's Analytics tab, not in the console.
Recap
useHost()is the one seam between your creation and the platform — media, navigation, identity, the world.- A tool never calls
fetchor touches a database, which is what lets it run unchanged in four different places. needsdeclares the capabilities you call by name, so a missing one fails loudly instead of as anundefined.- Cross-cutting capabilities — icons, audio, menus, analytics — are inherited, never declared, and degrade quietly.
Next steps
- Testing — swapping the host for a fake one, which is how every example on this site runs.