Skip to content
Apps— browse docs
On this page

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, a Surface, 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)
Roleconsumer — a leafprovider — a composer
The hostconsumes useHost(), declares needssupplies HostServices to everything inside it
Documentsone document type, a flat sidebar citizena parent plus its sub-documents
Where it appearsa document you open, a block you embeda tab, a full screen, its own URL
Owns a spaceno — it owns one documentyes — 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
})
  1. Host answers useHost() for everything below it. Inside a real world the platform hands you the services; you decide what to pass along.
  2. Surface is 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.
  3. tools is the registry the app dispatches into. A document of type x opens the tool that declared x.

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: true and 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 edittool
want it to appear inside another documentblock
own a parent document plus sub-documentsapp
want a tab and a screen of your ownapp
want to compose other people's tools inside your UIapp
want a themed public website over the whole worldwiki — 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:

src/lobby-tool.tsx
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>
    )
  },
})
The Lobby screen
Running on example data.
Starting the example…
Note:

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
Expected output:
→ 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 defineApp plus three things: Host, Surface, and a tools registry.
  • 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 defineApp is an explicit upgrade, never a grey area.

Next steps