Ship it
save, share, publish — three pointers over immutable versions, and what vvd save actually builds.
Your tool works on the live tab. Now you need it to exist for somebody other than you — and the thing that trips people up is that saving doesn't share anything.
That's deliberate. Every save mints an immutable version, and visibility is moved by its own verbs — so you can save fifty versions in an afternoon without a single one of them appearing to your team.
You will learn
- What
save,shareandpublisheach do, and who can see the result - What a version actually is — the bundle, the derived manifest, the hash
- Why a new save doesn't reach someone who already installed your creation
Three verbs, three audiences
| Command | Who can open it afterwards |
|---|---|
vvd save | only you. An immutable version — a private draft. |
vvd share | your world / your team. They can install and open it. |
vvd publish | anyone, via the Workshop — after a human reviews it. |
Not three steps of one thing: each is a separate pointer over your saves. Nothing is ever overwritten, you can point any of them at an older version, and that's why rolling back is instant and why submitting for review can't take your live version down.
vvd save — mint a version
vvd save --yes "first cut"
→ Building Bestiary… derived from @/codec: 4 field(s) → agent · 4 → index Ready to save → a new version name Bestiary kind tool version 0.1.0 document types bestiary capabilities reads world, writes world message first cut ● code changed ✓ Saved Bestiary v0.1.0 — first cut ● installed in Test Bench — still visible only to you vvd publish put it in the Workshop · vvd share let your world open it your versions: https://beta.vvd.world → Workshop → Developer
Four things happened there worth knowing:
- Your manifest was validated against the same parser the server runs, so anything that would be rejected on upload is rejected on your machine, in the same words.
- Your codec was executed to derive the agent surface and the index — see below.
- A pre-flight showed you exactly what was about to be uploaded, including whether the code has actually changed since your last save.
- In a solo world, the fresh draft was auto-installed — still visible only to you. In a world with other members, nothing moves for them.
Useful flags:
vvd save --dry-run # build + preview and stop. Works signed out.
vvd save --if-changed # exit 0 without saving when the bundle is byte-identical
vvd save --yes # skip the confirm (a pipe skips it automatically anyway)What a version is
One immutable package: a bundle, a manifest, and the hash that ties them together.
| Part | What it is |
|---|---|
| The client bundle | Your src/ compiled to a single module by esbuild — with React, @vvd/sdk and Yjs left out, because the host injects its own instances at load. Project anatomy has the externals story. |
| The server bundle | The same for your api/ folder, built only if you have one and never shipped to a browser — External APIs and secrets. |
| The manifest | A small JSON document describing what this creation is and what it may touch. Derived, not written. |
sha256 | The hash of the bundle text. The loader verifies it before evaluating a byte. |
The manifest is derived, not hand-written
You write vvd.json — identity, kind, a few booleans
(every field). The platform
computes the manifest from that and from your actual code. Here's what a save uploads
for a bestiary tool whose codec declares name, threat, traits and notes:
{
"format": "vvd-package/2",
"kind": "tool",
"id": "bestiary",
"name": "Bestiary",
"version": "0.2.0",
"engines": { "host": 1, "app": 1 },
"capabilities": ["collab", "read"],
"icon": "book-open",
"sha256": "b6f0…c41a",
"documentTypes": ["bestiary"],
"agent": {
"version": 1,
"state": {
"name": { "kind": "value", "default": "Unnamed creature" },
"threat": { "kind": "value", "default": 1 },
"traits": { "kind": "list", "default": [] },
"notes": { "kind": "prose" }
}
},
"index": {
"version": 1,
"fields": {
"name": { "path": "$.name", "type": "string" },
"threat": { "path": "$.threat", "type": "number" }
}
}
}Almost all of that was computed: format and engines are platform constants, sha256 is
the bundle hash, and capabilities is your readsWorld-style booleans projected onto the
grant vocabulary ("collab" is on by default). Because vvd.json names a codecModule,
the build reads your codec and derives two blocks:
agent— what's operable. Each field's kind becomes generated operations, so an agent cansetNameoraddToTraitsover the REST API and MCP without you writing a line of server code.notesis prose, so it's declared but gets no ops — character-level merge has no data description.index— what's queryable. Only value fields with an inferable scalar type make it; which member of a list is worth indexing is a judgement call the build refuses to guess.
Add a field to your codec and both blocks follow on the next save. That's the whole point — the alternative was declaring your shape in two places and watching them drift.
Why is the manifest data instead of code?Deep dive
Nothing runs an installed third-party creation's code on the server — that's what makes
installing a stranger's tool safe. But hand-writing that data reintroduces drift: declare
the shape in defineStateCodec and in vvd.json, add a field six weeks later, and the
agent block silently goes stale. Being data is a serialization requirement, not an
authoring one — so the build serializes what you already wrote, and the server still
consumes inert JSON.
The one constraint on you: codecModule must resolve to a pure module — no React —
because the build imports it directly. If your data model can't be described as a shape,
omit codecModule and either declare agent/index by hand or ship neither.
Packages are format-versioned, and immutability makes that a promise you can build on:
today's vvd-package/2 hosts still accept /1 bundles, new fields between bumps are added
additively (an older host ignores what it doesn't know), and a host that meets a newer
format refuses it with UPGRADE_REQUIRED rather than half-working.
vvd share — let your world open it
vvd share
→ Sharing with your world… ✓ Shared Bestiary v0.1.0 with your world.
That points your world at your latest save. Everyone in the world can now install and open
it. --version=0.1.0 targets an older save instead of your latest; --undo stops sharing.
vvd publish — submit it to the Workshop
vvd publish
→ Submitting to the Workshop… ✓ 🎉 You shipped Bestiary! It's in the review queue. v0.1.0 A real person usually takes a look within a day — we'll keep your current public version live until yours passes. track it in Workshop → Developer → Versions.
A human reviews it. Your current public version — if you have one — stays live until the new one passes, so submitting is never a risk to what's already out there.
Review states you'll see: in review · changes requested (the notes are in Workshop →
Developer → Versions) · rejected (with a reason) · approved. --undo unpublishes.
vvd status — who can open it right now
This is the command to run when you're not sure what's live. It's the truth, in one screen.
vvd status
Bestiary — who can open it you Version 3 your world Version 2 workshop Version 1 (in review) ⏳ in review — a real person will take a look soon change it: vvd publish · vvd share · or the Versions tab
Three independent pointers. You're on your newest save, your world is one behind, and the
Workshop is looking at your first. That's a completely normal state, and it's why save and
share are separate verbs.
Installing pins an exact version
An installation is a hard version pin. When someone installs your creation, the exact
version they installed is recorded, and that is the version they keep running. A later
vvd save — or even vvd share — does not move them onto it.
Which is correct from their side: their world shouldn't re-render differently because you were mid-refactor at 2am. But it means "I fixed it, why are they still seeing the bug?" has a specific answer. How an installed creation moves forward:
- Your own solo world bumps automatically —
vvd savere-pins the fresh draft there, which is what makesvvd runandvvd savefeel immediate. - A world with other people in it never bumps as a side effect of your saving. Somebody updates the pin deliberately, from the Workshop or over the platform API.
The gate is about safety: everyone in a world can read and execute a pinned bundle, so auto-pinning your private draft would push untested — and unshared — code into other people's hands.
Two consequences. Secrets are not pinned: they resolve at call time, so changing one reaches every version on its next request. And a published version you've replaced doesn't disappear — someone pinned to it keeps running it.
Rolling back
Because versions are immutable, rolling back is moving a pointer:
vvd share --version=0.1.0 # point your world at an older save
vvd publish --version=0.1.0 # submit an older save to the Workshop
vvd share --undo # stop sharing entirely
vvd publish --undo # unpublishWhat there isn't: a delete. Saved versions are immutable, and there is currently no command
that permanently deletes a creation or a version — vvd remove, vvd unshare, and
vvd unpublish only change who can see it (an uninstall, a cleared pointer), and every
version you've ever saved stays listed in Workshop → Developer → Versions.
Optional: every push becomes a version
If you'd rather ship from git than from your terminal:
vvd github link
vvd github connect --repo you/bestiary --folder . --watch --deploy-as private
vvd github status --jsonWith --watch, every push to the connected branch and folder becomes a version
(--deploy-as draft|private|public; review still applies to public). A push that fails the
manifest check is kept as a draft, and vvd github status tells you exactly why.
When it doesn't work
vvd share/publish errors with no version.
These verbs move pointers over your saves. There has to be one. vvd save --yes first, and
vvd status will show you what exists.
vvd.json isn't valid: on save.
The same parser the server runs rejected it. Fix what it names —
Project anatomy covers the fields, including the
permanent ones behind ID_TAKEN and KIND_LOCKED.
couldn't read @/codec to derive the agent/index blocks.
Something in src/codec.ts can't run outside a browser — usually a UI import. The save
still succeeds using whatever vvd.json declares by hand, but you ship without the agent
surface. Make the codec pure and save again.
A field you added has no agent operations.
Either the derive step failed (see above — check the save output), or vvd.json declares an
agent block by hand. A hand-written block always wins, because it's the more specific
statement; delete it to go back to deriving.
Not signed in. / your login expired.
vvd login --token <code> with a fresh code from Workshop → Developer. The CLI warns you
when your token is inside 14 days of expiring — vvd whoami shows it.
You saved, but your teammate still can't see it — or still sees the bug.
Both are the design. Saving never changes visibility (vvd share), and an existing install
is pinned until somebody moves it (see above).
Recap
savemints an immutable private version;sharepoints your world at one;publishsubmits it to the Workshop. Visibility moves only withshareandpublish.- A version is a bundle, a derived manifest, and a
sha256the loader verifies — you writevvd.json, and theagent/indexblocks are computed from your codec so they can't drift. - Installing pins an exact version. Saving updates your own solo world and nobody else's; secrets are the exception, resolved at call time.
- Rolling back is pointing a verb at an older version, and
vvd statusshows all three pointers when you're not sure.
Next steps
That's the whole common surface: the model, the CLI loop, the codec, the actions, world data, presence, the host, the harness, and shipping. One page left — the four of them in one tool.