Skip to content
Apps— browse docs
On this page

Step 2: Scaffold the app

vvd create --app, and a bare draw-only surface — one pen, no toolbar yet.

Start the way every app does:

vvd create sketch --app
Expected output:
→ Creating app sketch in /Users/you/sketch — from the Hello World template

✦  sketch — a brand-new app, ready to come alive.

Next:
cd sketch
vvd run    # render it live in your world — hot-reloads as you edit
vvd save   # save a new version (a private draft)

✓ Created sketch (app) → /Users/you/sketch

An app is defineApp plus three things — a Host, a Surface, and a tools registry — and what actually draws pixels is a tool inside that registry, exactly like every tool you've seen elsewhere on this site:

src/app.tsx
import { ToolRegistry, defineApp, deriveCapabilities } from "@vvd/sdk"
import sketchTool from "./tool"

const tools = new ToolRegistry().register(sketchTool)

export default defineApp({
  id: "sketch",
  name: "Sketch",
  route: "sketch",
  version: "1.0.0",
  engines: { host: 1, app: 1 },
  capabilities: deriveCapabilities(tools),
  Host: SketchHost,     // the useHost() provider — see Apps → defineApp
  Surface: SketchSurface, // the chrome the tool renders inside
  tools,
})

vvd create --app generated SketchHost and SketchSurface for you, in this same src/app.tsx — you didn't write them, and this build never edits them. That split is Apps → defineApp territory, and this build doesn't repeat it. Everything from here down is sketchTool — split across src/codec.ts (the document shape) and src/tool.tsx (everything that renders) — the part you'll spend this whole build on, and the part the playground below can actually run.

Note:

Why a tool is running on this 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's nothing on a docs 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. Run vvd run at the end of this build to see the whole app, route and all.

The document: a growing list of strokes

src/codec.ts
export type Stroke = {
  points: number[] // flattened [x0, y0, x1, y1, …] — DOCUMENT space
  color: string
  width: number
}

export const codec = defineStateCodec({
  // A per-KEY map: two people drawing different strokes at once BOTH land.
  strokes: field.map<Stroke>(),
})

Compare that to the pin board's codec — pins: field.map<Pin>(), where a Pin is an {x, y, label}. Same CRDT shape, completely different content: a pin is a single point you place once and can move; a stroke is a path you draw continuously, and once it exists it never moves.

A bare canvas — one pen, draw-only

No toolbar yet. Pointer down starts a stroke, pointer move extends it, and every point goes straight into strokes — that's the whole drawing loop.

A bare canvas
src/codec.ts + src/tool.tsx

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

Running · your edits, live
Starting the example…

You should see: a parchment-coloured square. Draw on it — a dark line follows your pointer, and it's already durable: every point you dropped is sitting in data.strokes the moment you let go.

Warning:

This canvas isn't tldraw yet

vvd's real canvas tools draw with tldraw — a proper vector editor with pressure, undo, and export built in. This page's <svg> is a hand-rolled stand-in, on purpose: the playground on this site compiles only against @vvd/sdk, with no bundler, and tldraw (via @vvd/editor-sdk) can't run inside that sandbox — the same reason @vvd/design-system doesn't appear in any example on this site either. Nothing about the data is a stand-in, though: field.map<Stroke>() is real CRDT state, and Step 4 wires it to the exact binding tldraw itself uses. Read that as "the pixels are simplified, the collaboration is not."

Next steps