Skip to content
Apps— browse docs
On this page

Step 7: Ship it

The whole thing, and the three commands that put it in a world with real people in it.

This is not a step to complete. It's src/codec.ts and src/tool.tsx, everything you've built since Step 2 in one file — the morphing palette, live collaboration, presence, the world link — still editable, if you want to keep poking at it.

sketch, complete
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…

What changes when you swap in tldraw

Step 4 described useTldrawYjsStore; here's what actually calling it looks like, once you're running this for real (this block is illustrative — it isn't compiled on this page, for the reasons Step 2 and Step 4 both explained):

import { CollaborativeCanvas } from "@vvd/editor-sdk"
import { useDocument, useDocumentAwareness } from "@vvd/sdk"

function SketchCanvas({ document, context }: ToolRenderProps) {
  // coords: the same useMemo'd { worldId, documentId } SketchView builds above.
  const { handle } = useDocument(coords, codec)
  const awareness = useDocumentAwareness(handle)
  // identity: useHost().identity — the same host-stamped { userId, name, color }
  // the PresenceStrip's peers carry.
  //
  // handle._doc is the runtime-owned Y.Doc. The underscore means a tool never
  // reads or writes it directly — but forwarding it whole is a deliberate
  // collaboration seam: it's the same doc useTldrawYjsStore binds
  // "tldraw-store" on, the canvas analogue of forwarding a handle to
  // <CollaborativeText>.
  return (
    <CollaborativeCanvas
      doc={handle?._doc ?? null}
      awareness={awareness}
      user={{ id: identity.userId, name: identity.name, color: identity.color }}
      readOnly={!context.canEdit} // the same context every render gets
    />
  )
}

Both imports are real, not sketch-pseudocode: useDocumentAwareness ships in @vvd/sdk and CollaborativeCanvas in @vvd/editor-sdk (which can't run in this page's sandbox — the Step 2 callout). The Blocks starter kit is where the awareness wiring appears in a full working creation.

Everything in strokes: field.map<Stroke>() on this page becomes doc.getMap<string>("tldraw-store") under the hood, and your hand-rolled <svg> becomes tldraw's own renderer — pressure-sensitive ink, undo, SVG/PNG export, all for free. Nothing about the shape of what you built changes: same document, same presence, same link.

Make it real

vvd save --yes
Expected output:
→ Building sketch…
derived from @/codec: 2 field(s) → agent · 2 → index

Ready to save  → a new version

name             sketch
kind             app
version          0.1.0
document types   sketch
capabilities     reads world

● code changed

✓ Saved sketch v0.1.0
● installed in Test Bench — still visible only to you

vvd publish  put it in the Workshop   ·   vvd share  let your world open it
vvd share
Expected output:
→ Sharing with your world…

✓ Shared sketch v0.1.0 with your world.
vvd publish
Expected output:
→ Submitting to the Workshop…

✓ 🎉 You shipped sketch! It's in the review queue. v0.1.0
A real person usually takes a look within a day.

Ship it covers what each command does, who can see the result afterwards, and how to roll back or unshare.

Recap

  • strokes: field.map<Stroke>() is the whole document — a per-key map of strokes, drawn continuously.
  • Which pen you're holding is local state; the morph is one bar whose width and children change together.
  • CollabPreview/PresenceDemo proved the collaboration was never simulated — real field.map merges, real awareness.
  • linkedDocumentId: field.value<string | null>(null) is what turns a drawing into a linked artifact in the world graph.
  • Swapping the <svg> for useTldrawYjsStore changes the pixels, not the document, the presence, or the link.

Where to next