Skip to content
Guides— browse docs
On this page

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, share and publish each 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

CommandWho can open it afterwards
vvd saveonly you. An immutable version — a private draft.
vvd shareyour world / your team. They can install and open it.
vvd publishanyone, 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"
Expected output:
→ 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:

  1. 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.
  2. Your codec was executed to derive the agent surface and the index — see below.
  3. A pre-flight showed you exactly what was about to be uploaded, including whether the code has actually changed since your last save.
  4. 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.

PartWhat it is
The client bundleYour 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 bundleThe same for your api/ folder, built only if you have one and never shipped to a browser — External APIs and secrets.
The manifestA small JSON document describing what this creation is and what it may touch. Derived, not written.
sha256The 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:

the manifest vvd save uploads
{
  "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 can setName or addToTraits over the REST API and MCP without you writing a line of server code. notes is 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
Expected output:
→ 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
Expected output:
→ 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
Expected output:
  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

Warning:

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 save re-pins the fresh draft there, which is what makes vvd run and vvd save feel 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            # unpublish

What 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 --json

With --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

  • save mints an immutable private version; share points your world at one; publish submits it to the Workshop. Visibility moves only with share and publish.
  • A version is a bundle, a derived manifest, and a sha256 the loader verifies — you write vvd.json, and the agent/index blocks 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 status shows 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.