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.mdTwo 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.
"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:
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
✓ 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.
{
"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
| Field | What it is |
|---|---|
id | Permanent. The platform keys documents and routes on it. It's derived from the name at create time and can never be edited afterwards. |
name | The display name. Change it freely — vvd rename "Dragon Dice". |
kind | tool or app. Locked once the project has been shared or installed. |
version | Semver. vvd set version --bump minor moves it. |
icon | Any lucide icon name. vvd set icon --list browses the curated suggestions. |
documentTypes | Tools only. Which document types this tool renders. Defaults to [id]. |
codecModule | Tools only. Points at your one data-model module. |
route | Apps only. The URL segment the app mounts at. |
entryModule | Which module exports your defineTool / defineApp. |
needs | Host capability namespaces your code calls by name — see The host. |
capabilities.readsWorld | Read the world's catalog. See Reading the world. |
capabilities.writesWorld | Create and mutate world documents. |
capabilities.sharing | Publish a public site of your own — what a wiki needs. |
category | Wikis 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
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
✓ name → Bestiarium ✓ reads world → on
vvd set version --bump minor
✓ 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-readableThree 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) andsrc/tool.tsx(the UI). - There's no
package.jsonbecause React, Yjs and@vvd/sdkare platform singletons injected at load. You can still add npm packages. vvd.jsonis your identity;idandkindare permanent, and the CLI validates every other field as it writes it.- Keeping
src/codec.tspure is what letsvvd savederive your agent surface and your search index from it.
Next steps
- Storing data —
src/codec.ts, in depth and editable.