Reading the world
Query the world's documents, eras, projects and media — live, read-only, and gated by a grant that fails loud.
Let's say you're building a tool that lists every character in the world, so a reader can pick one instead of typing a name. Or a wiki that generates an index page. Or a travel planner that needs the map documents that already exist.
That data isn't in your document. It belongs to the world, and the platform hands it to you live — but only if you asked for it in your manifest.
You will learn
- How to query the world's catalog with
useWorldQuery - What
"readsWorld": trueinvvd.jsonactually grants - What happens when you forget it (it fails loudly, on purpose)
The sample world
Every live example on this site reads the same hard-coded sample world: The Salt
Road, a desert trade-route setting. World queries and host search both answer from
it, so what you see in an example is what you'll get back from a real world — same
shapes, same fields, real values.
| It contains | |
|---|---|
| 11 cards | 3 characters, 4 locations, 2 factions, 2 items — several carry aliases (search matches those too) |
| 1 map | Atlas of the Interior |
| 1 table | Caravan ledger |
| 2 notes | a route survey and a treatise on salt tides |
| 2 eras | The First Crossings · The Long Drought |
| 2 projects | a wiki (Route gazetteer) and a quill (The Caravan Chronicle) |
Every document row arrives in the WorldDocumentRow shape. This is an actual row from
the sample world, exactly as useWorldQuery("documents") hands it to you:
{
"id": "doc-aria",
"name": "Aria of the North",
"slug": "aria-of-the-north",
"documentType": "card",
"updatedAt": "2026-07-28T09:14:00.000Z",
"entityTypeId": "character",
"avatarMediaId": null,
"aliases": ["The North Star"],
"isViewable": true
}Signed in? World-reading examples grow a picker so you can point them at one of your own worlds instead — the code doesn't change, only the rows do.
The query
import { useWorldQuery } from "@vvd/sdk"
const cards = useWorldQuery("documents", { type: "card" })
const maps = useWorldQuery("documents", { type: "map" })
const eras = useWorldQuery("eras")That's it — no useEffect, no loading flag, no cache. The rows are live: when somebody
adds a card in another tab, on another machine, your component re-renders with it.
Running against a real world
The example below is a real tool calling useWorldQuery("documents", { type: "card" }),
reading the sample world above — eleven cards, a map, a table and two notes. Nothing about
the code changes when it reads a real one — the rows arrive through the same host capability
either way, which is exactly why a tool can be developed against fixtures and shipped
unchanged.
Notice the filter is doing real work: the world has maps and tables in it too, and
{ type: "card" } leaves them out.
What you can query
| Kind | Returns |
|---|---|
useWorldQuery("documents", { type }) | every document in the world — id, name, slug, document type, last-updated, and (for cards) entity type, avatar and aliases |
useWorldQuery("eras") | the world's eras |
useWorldQuery("projects") | app instances in this world |
useWorldQuery("media", { type }) | the world's media library |
useWorldQuery("index", { type, where }) | the queryable fields other tools declared about their documents |
Rows carry identity, not content. You get a card's id and name; you don't get its body.
That's deliberate — content stays behind the document seam, so when you need it you feed the
id to useDocument and open it properly, with permissions and live sync intact.
Reference, don't retype. If something already exists in the world, point at it by id rather than copying its text into your document. A stored id resolves live: rename the card and your tool follows. A stored copy goes stale the moment somebody edits the original.
The grant
Two lines make that query legal:
{
"capabilities": { "readsWorld": true },
"needs": ["world"]
}capabilities.readsWorldis the consent declaration. When someone installs your creation, this is what they're asked to approve. It grants two host capabilities:world(the queries above) andsearch(full-text world search).needslists the host capability namespaces your code calls by name. The platform unions them into the consent screen so what the user is asked to grant can't drift from what your code actually does.
Writing is a different grant. capabilities.writesWorld is what lets you create documents
and projects — reading and writing are consented separately, because they're very different
asks.
Forget it and it fails loud
This is worth seeing, because the failure is deliberate and it happens at a place you might not expect.
Ship a tool that calls useWorldQuery without readsWorld in its manifest, and the install
carries no read grant. The host that mounts your tool is then built without the world
namespace at all, and the very first query throws:
Host capability "world" is not provided by this host. Either this surface should
not render a consumer that needs "world", or <HostProvider services> must include it.Consent is enforced at mount, not recorded in a database and forgotten. A creation cannot quietly exceed what it was granted, and you find out immediately rather than in a bug report from a user whose install predates your new feature.
The fix is one command:
vvd set reads=on
✓ reads world → on
Then vvd save and re-share. Existing installs will be asked to approve the new capability.
Adding a capability to a shipped creation re-prompts your users. Declare what you genuinely need, and add capabilities in a deliberate release rather than drifting into them — every one of them is a question somebody has to answer about your creation.
When it doesn't work
Host capability "world" is not provided by this host.
Missing readsWorld — see above. Also check needs includes "world".
The list is empty on first render, then fills in. That's normal and correct: the collection resolves asynchronously behind a synchronous API. Render an empty state; don't render a spinner that never clears.
Some hosts return nothing at all. A published, baked page and a logged-out share link genuinely have no live world behind them. If your tool must render on those surfaces, handle the empty case as a first-class state — your example above already does.
You want the world but only sometimes.
If world data merely enriches your UI and the tool must still work without it, that's a
different hook (useWorldDocumentsOptional), which returns [] instead of throwing. Use it
sparingly: a tool that genuinely needs the world should fail loud, so the missing grant gets
fixed instead of silently degrading.
Recap
useWorldQuery(collection, filter)reads the world's catalog live and read-only."readsWorld": trueinvvd.jsonis what grants it; without it the boundary fails loudly and names the capability.- The rows are deliberately slim — enough to render a list, not the document's contents.
- Consent is enforced every time your creation mounts, not recorded once at install.