Skip to content
Guides— browse docs
On this page

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": true in vvd.json actually 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 cards3 characters, 4 locations, 2 factions, 2 items — several carry aliases (search matches those too)
1 mapAtlas of the Interior
1 tableCaravan ledger
2 notesa route survey and a treatise on salt tides
2 erasThe First Crossings · The Long Drought
2 projectsa 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

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

Every card in the world
Running on example data.
Starting the example…

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

KindReturns
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.

Tip:

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:

vvd.json
{
  "capabilities": { "readsWorld": true },
  "needs": ["world"]
}
  • capabilities.readsWorld is 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) and search (full-text world search).
  • needs lists 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
Expected output:
✓ reads world → on

Then vvd save and re-share. Existing installs will be asked to approve the new capability.

Warning:

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": true in vvd.json is 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.

Next steps

  • Presence — the other live thing you get for free.
  • The host — where world came from, and what else is on that object.