Skip to content
Guides— browse docs
On this page

Project anatomy

Every file the scaffold writes, what it's for, and every field in vvd.json.

You ran vvd create and got a folder. Before you start changing things, it's worth two minutes to know what's in it — mostly because of what isn't.

You will learn

  • What every file in a fresh project is for
  • Why there's no package.json, and what that means when you want a library
  • Every field in vvd.json, and which ones you can change

The folder

bestiary/
├── vvd.json                     the manifest — identity, kind, capabilities
├── src/
│   ├── codec.ts                 your DATA MODEL, alone, pure
│   └── tool.tsx                 your UI            (apps: src/app.tsx)
├── tsconfig.json                strict TS; @/* → src/*; @vvd/sdk types from ~/.vvd
├── .gitignore                   ignores .vvd/ (local state), .vvd-build/, node_modules/
├── README.md
├── AGENTS.md                    the front door for a coding agent
├── ANALYTICS.md                 how to instrument THIS project
├── docs/
│   ├── references.md            "reference, don't retype" — pickers, refs, embeds
│   ├── documents-and-data.md    the codec API and merge semantics
│   ├── collaboration-and-presence.md
│   ├── canvases.md              maps, graphs, boards the vvd way
│   └── server-endpoints-and-secrets.md
└── .claude/skills/vvd-tool/SKILL.md

Two files are yours. The rest is reference material, and you can delete any of it.

src/codec.ts — the data model

The shape of your document, and nothing else. Keep it pure — no UI imports. That's not a style rule: when you ship, vvd save executes this module against a data-only stub to derive two things from your one declaration — which operations an agent can perform on your documents over the API, and which of your fields are searchable. Import React here and that derivation fails.

Storing data is the whole page on this file.

src/tool.tsx — the UI

A React component plus a defineTool call. For an app it's src/app.tsx and defineApp.

What is not there

No package.json. No node_modules. No lockfile. No bundler config.

A fresh project has zero dependencies, because React, the CRDT library, and @vvd/sdk are platform singletons the host injects when your bundle loads. Your project can't have its own copy of React — there is exactly one on the page and you share it.

Note:

"But I want a library." You can have one. npm init -y && npm install date-fns and the CLI bundles it into your artifact like any bundler would. The npm ecosystem is available — it's not required to start, and most projects never need it.

With no node_modules, where does the SDK you import actually come from?Deep dive

Your bundle is built with a short list of externals: react, react-dom and its entrypoints, react/jsx-runtime, @vvd/sdk (and its pure subpaths), yjs, @vvd/editor-sdk, and next-intl. Everything else you import is bundled in normally.

An external isn't resolved at build time at all — the bundler leaves the require call where it is. So the artifact the CLI produces isn't a script; it's a module whose default export is a factory:

the built artifact (excerpt)
export default function create(__deps) { … your code … }

__deps is the platform's own instances, handed in by the loader when your creation mounts. A tiny require shim inside the factory maps each external onto one of them: "react" → __deps.React, "@vvd/sdk" → __deps.sdk, "yjs" → __deps.sdk.Y.

Two payoffs and one rule fall out of that. Your project carries no dependencies and no lockfile, and every creation on a page shares one React and one Yjs — which is load-bearing rather than tidy: a second copy of Yjs throws the moment your new Y.Map() meets the host's document, because Yjs refuses content from a different instance. React would give you the quieter version of the same bug, where a context nobody's provider wrote reads as undefined.

The rule is the error message. Import a platform module by a path the shim doesn't map and you get vvd bundle: unexpected require(<pkg>) when the tool loads — not at build time, because as far as the bundler is concerned an external is somebody else's problem. Import from @vvd/sdk and its documented subpaths only, and it can't happen. Ordinary npm packages are never externals, so they bundle normally and never produce that error.

tsconfig.json

Strict TypeScript, @/* mapped to src/*, and @vvd/sdk pointed at the SDK type sources the CLI keeps under ~/.vvd. Your editor gets full types with no install step.

If your editor says it can't find @vvd/sdk, the type sources weren't on disk when the project was scaffolded. Building and running are unaffected — the host injects the real SDK at run time — but fix the editor with:

vvd doctor --fix-types
Expected output:
✓ Types wired to the installed SDK bundle at /Users/you/.vvd/sdk (3 @vvd/* aliases).

AGENTS.md and .claude/

If you use a coding agent, these are its briefing: the loop, the five principles, and pointers into docs/. If you don't, ignore them — nothing in the platform reads them.

.vvd/ and .vvd-build/

Local state, gitignored. .vvd/dev.json remembers which world you last ran in — that's why the second vvd run needs no --world=.

vvd.json — the manifest

This is your project's identity, and the thing the platform validates when you ship.

vvd.json
{
  "id": "bestiary",              // FIXED identity — the document type / route the host keys on
  "name": "Bestiary",            // human-facing; change it whenever you like
  "kind": "tool",                // "tool" | "app"   (a wiki is an app + category: "site")
  "version": "0.1.0",
  "icon": "book-open",           // any lucide icon name
  "documentTypes": ["bestiary"], // tools: the document types this tool edits
  "codecModule": "@/codec",      // tools: the ONE data-model module `vvd save` derives from
  "needs": [],                   // host capabilities you require by name
  "capabilities": {
    "readsWorld": true,          // grants `world` + `search` — live read-only world queries
    "writesWorld": true,         // grants `projects` + `documents` — create/mutate world docs
    "sharing": false             // grants publishing a public site (wikis)
  },
  "entryModule": "@/tool"        // apps: "@/app"; apps also carry "route": "<id>"
}

Field by field

FieldWhat it is
idPermanent. The platform keys documents and routes on it. It's derived from the name at create time and can never be edited afterwards.
nameThe display name. Change it freely — vvd rename "Dragon Dice".
kindtool or app. Locked once the project has been shared or installed.
versionSemver. vvd set version --bump minor moves it.
iconAny lucide icon name. vvd set icon --list browses the curated suggestions.
documentTypesTools only. Which document types this tool renders. Defaults to [id].
codecModuleTools only. Points at your one data-model module.
routeApps only. The URL segment the app mounts at.
entryModuleWhich module exports your defineTool / defineApp.
needsHost capability namespaces your code calls by name — see The host.
capabilities.readsWorldRead the world's catalog. See Reading the world.
capabilities.writesWorldCreate and mutate world documents.
capabilities.sharingPublish a public site of your own — what a wiki needs.
categoryWikis only. "site".

Edit it with the CLI, not by hand

The CLI validates as it writes, which saves you a round trip through a failed vvd save.

vvd info
Expected output:
  Bestiary (tool)

name             Bestiary
id               bestiary  (fixed)
kind             tool
version          0.1.0
icon             book-open
document types   bestiary
description      —
capabilities     reads world, writes world
vvd set name=Bestiarium reads=on
Expected output:
✓ name → Bestiarium
✓ reads world → on
vvd set version --bump minor
Expected output:
✓ version 0.1.0 → 0.2.0  (minor bump)

Editable fields: name, description, icon, version, route (apps only), reads, writes, share. Everything else is either derived or permanent.

Other useful forms:

vvd set icon --list          # browse the curated icon suggestions
vvd set version --bump minor # 0.1.0 → 0.2.0  (major | minor | patch)
vvd set --unset description  # clear an optional field
vvd rename "Dragon Dice"     # display name only — the id NEVER changes
vvd edit                     # walk every field interactively
vvd info --json              # the same thing, machine-readable

Three failures come out of this file, and all three are about the permanent fields:

vvd.json isn't valid: on save. vvd save runs the same parser the server runs, so anything that would be rejected on upload is rejected on your machine first, in the same words. Fix what it names and save again.

that id is taken (ID_TAKEN / ID_RESERVED). Something in the catalog already has that id. Because the id follows the name at create time, the fix is a fresh project with a different name — an existing project's id can't be edited.

this project's kind is locked (KIND_LOCKED). A project that's been shared or installed can't move between tool and app. Create a new project for the other kind and move your src/ across.

Recap

  • Two files are yours: src/codec.ts (the data model) and src/tool.tsx (the UI).
  • There's no package.json because React, Yjs and @vvd/sdk are platform singletons injected at load. You can still add npm packages.
  • vvd.json is your identity; id and kind are permanent, and the CLI validates every other field as it writes it.
  • Keeping src/codec.ts pure is what lets vvd save derive your agent surface and your search index from it.

Next steps