What is an app?
A lens that provides a host and owns a space — the exact opposite of a tool, on exactly the same contract.
You've built a tool. It makes one kind of document, it shows up in the sidebar, it embeds as a block. That is a lot of mileage from a small thing.
Then you want a place. A campaign tracker with its own screen. A story with chapters under it. A tab your world's members click into, that owns its own set of documents and that you can hand to someone as one thing.
That's an app.
You will learn
- The one sentence that separates an app from a tool, and why it's the same contract
- The three things every app supplies: a
Host, aSurface, and a tool registry - What being an app buys you — a tab, a space, a URL subtree, one unit to publish
- How to tell whether your idea wants an app or a tool
A tool consumes the host. An app provides one.
This is the sentence to hold on to, and it is worth slowing down for.
Every tool you write calls useHost() — that's how it asks for media, navigation,
references, the world's catalog. The tool never knows who is answering. It asks.
An app is the thing that answers.
// A tool CONSUMES the host — it asks.
function MapView() {
const nav = useHostCapability("nav")
nav.openDocument(coords)
}
// An app PROVIDES one — it answers.
function LobbyHost({ children }) {
return <HostProvider services={buildServices()}>{children}</HostProvider>
}Same contract, opposite roles. That's the whole difference, and everything else follows from it:
Tool (defineTool) | App (defineApp) | |
|---|---|---|
| Role | consumer — a leaf | provider — a composer |
| The host | consumes useHost(), declares needs | supplies HostServices to everything inside it |
| Documents | one document type, a flat sidebar citizen | a parent plus its sub-documents |
| Where it appears | a document you open, a block you embed | a tab, a full screen, its own URL |
| Owns a space | no — it owns one document | yes — a bounded, named one |
Because the seam is the same in both directions, the tools an app hosts don't know they're inside your app. They ask; you answer. Which is also why a tool is testable at all — a test host and your app are the same kind of thing wearing different clothes.
The three things you supply
An app is a definition with three components in it, and nothing else is required:
export default defineApp({
id: "lobby",
name: "Lobby",
route: "lobby",
Host: LobbyHost, // 1. the useHost() provider
Surface: LobbySurface, // 2. the chrome a document renders inside
tools: new ToolRegistry().register(lobbyView), // 3. the tools you compose
})HostanswersuseHost()for everything below it. Inside a real world the platform hands you the services; you decide what to pass along.Surfaceis the window chrome around a document — a frosted panel, a bare frame, a site card. Same tool, different look, because the app owns the frame.toolsis the registry the app dispatches into. A document of typexopens the tool that declaredx.
Each of those gets its own page. Start with defineApp.
What being an app buys you
- A tab. Your app mounts at
/worlds/<world>/<route>and the world shell lists it. - A space. A parent document plus sub-documents that belong to it — the sidebar filters them out of the flat list, and your app is what shows them.
- A URL subtree, if you want one —
subRoutes: trueand every path under your route is yours, with real back/forward. - Publish and share as one unit. A tool owns one document, so the most you can hand over is that document. An app owns a space, so the space is the thing you freeze, grant access to, or put in the Workshop.
- Consent that can't drift, because the capabilities the installer approves are derived from the tools you actually compose.
So do you want an app, or a tool?
Answer honestly — the wrong choice costs a rewrite:
| If you… | build a |
|---|---|
| make one new kind of document people open and edit | tool |
| want it to appear inside another document | block |
| own a parent document plus sub-documents | app |
| want a tab and a screen of your own | app |
| want to compose other people's tools inside your UI | app |
| want a themed public website over the whole world | wiki — an app with category: "site" |
A tool with a fullscreen view feels app-like. It still isn't one: it consumes a host,
something else provides it. When you want it to compose tools, own sub-documents, and get
a tab, you wrap it in defineApp. Leaf becomes provider — an explicit upgrade, never a
grey area.
An app's screen, running
Here is the screen half of a world lobby, live. It reads the world through the host, so it follows the world as the world changes:
import { type ToolRenderProps, defineTool, useWorldMeta, useWorldQuery } from "@vvd/sdk"
// The SCREEN an app shows is an ordinary tool. The app composes it — this is
// exactly the component your Host and Surface will end up wrapping.
export default defineTool({
id: "lobby",
name: "Lobby",
documentTypes: ["lobby"],
needs: ["world"],
surface: "plain",
render: function Lobby({ context }: ToolRenderProps) {
const meta = useWorldMeta()
const cards = useWorldQuery("documents", { type: "card" })
return (
<div>
<h1>{meta ? meta.name : "Your world"}</h1>
<p>
{cards.length} card{cards.length === 1 ? "" : "s"} ·{" "}
{context.canEdit ? "you can edit" : "read-only"}
</p>
{cards.map((card) => (
<div key={card.id}>{card.name}</div>
))}
</div>
)
},
})Why a tool is running on the Apps page
The examples on this site mount a tool against a fake host, in your browser. An app provides a host and owns a route, so it needs the real shell — there is nothing on this page for it to be a lens over. What you can honestly show is the app's screen, and the app's screen is a tool in its registry. Everything above this line is the app; everything inside the frame is what it composes.
To see the whole thing, run it: vvd run puts your app in a real world in a couple of
seconds, hot-reloading as you type.
Make one
vvd create Lobby --app
→ Creating app Lobby in /Users/you/lobby — from the Hello World template ✦ Lobby — a brand-new app, ready to come alive. Next: cd lobby vvd run # render it live in your world — hot-reloads as you edit vvd save # save a new version (a private draft) ✓ Created Lobby (app) → /Users/you/lobby
In a terminal, vvd create goes straight into vvd run from the new folder — no cd, no
second command. The starter it writes is a world lobby: a shared headline everyone can
rewrite live, a pinboard of world documents, and a live directory. That's the example the
rest of this section grows.
If you haven't installed the CLI or picked a world yet, that's all in Getting Started — this section assumes it.
Recap
- A tool consumes the host; an app provides one. Same contract, opposite roles.
- An app is
defineAppplus three things:Host,Surface, and atoolsregistry. - Being an app buys you a tab, a space of sub-documents, an optional URL subtree, and one unit to publish or share.
- A tool with a fullscreen view is still a tool. Wrapping it in
defineAppis an explicit upgrade, never a grey area.