Skip to content
Apps— browse docs
On this page

Step 2: The shape of an app

The one sentence that separates an app from a tool, and the three things defineApp needs from you.

Every tool you've written calls useHost() — that's how it asks for the world, media, navigation. The tool never knows who's answering. It asks.

An app is the thing that answers:

// A tool CONSUMES the host — it asks.
function LobbyScreen() {
  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. Hold on to that — it's why the tool you host inside your app never has to know it's inside your app rather than someone else's.

You will learn

  • defineApp's three required parts: Host, Surface, and a tools registry
  • AppHost — the SDK helper that composes them into one mount
  • Why a bad manifest throws while your file is still loading, not while it's rendering

Three things, and nothing else is required

export default defineApp({
  id: "lobby",
  name: "Lobby",
  route: "lobby",
  Host: LobbyHost, // 1. answers useHost() for everything inside
  Surface: LobbySurface, // 2. the chrome a document renders inside
  tools: new ToolRegistry().register(lobbyScreen), // 3. what you compose
})

Host answers useHost() for the tools you mount. Surface is the window frame around a document — a tool declares surface: "plain" when it draws its own, "panel" when it wants yours. tools is the registry AppHost dispatches into by document type.

The playground below can't mount an app directly — an app owns a route and needs the real shell, so this page does what the shell does for real: wrap your app in <AppHost> and show you what it draws. That's the whole three-part composition, running.

No edit to make yet — try something else first (keep reading below the code).

Host, Surface, tools
src/app.tsx Line 24 highlighted.

Edit and the example re-runs. Tab indents; press Escape to leave the editor.

Running · your edits, live
Starting the example…

You should see: "Welcome to the lobby." — your Host answered useHost(), your Surface drew the frame, and AppHost dispatched to lobbyScreen by document type.

Our LobbyHost is a passthrough — it hands the platform's real host straight through, which is the normal case; wrap with HostProvider only when you want to override specific answers. See Hosting tools.

Now break it on purpose. Change line 24's route: "lobby" to route: "My Lobby" — a space, like you'd type without thinking. The whole example vanishes, and the strip above the editor reads:

Invalid app manifest for "lobby": route "My Lobby" must be a single lowercase URL segment
(letters, digits, hyphens; no slashes or spaces)

That's not a bug in this page. defineTool is a pure identity function — it hands your object back unchanged, and a typo in it only surfaces whenever the typo happens to matter. defineApp is different: it derives your manifest and validates it the moment the function is called — which is while your module is still loading, before a single component mounts. A route with a space in it fails right here instead of as a 404 nobody can explain three clicks later. So when you get a blank screen and a message like this in the console during vvd run, don't go looking in your render function — nothing rendered.

Put the space back to a hyphen-free "lobby" and the app returns.

Note:

defineApp reports every problem at once, joined with ; — a bad route and a bad version show up together, not one fix at a time. The full checklist (id, route, version, engines, capabilities…) is on defineApp.

Next steps