Copying an app
Export writes a.vendoapp archive. Import mints a fresh app_ id before it even validates the document, because an artifact’s own id is never trusted.
fork does the same in one call and records forkedFrom so the lineage is readable. share and publish hand the copy to Vendo Cloud, and both need VENDO_API_KEY.
A copy re-approves its own egress before it can run anything.
Forking one of your components
Wrap a component and a user can make it theirs. What they get is your own component:vendo sync ports it into the dialect a generated screen speaks, and the wish edits that code. A remix is an edit of the component, not a fresh screen wearing its name.

Hook the wiring up in both places
The wrapper alone does nothing.vendo sync generates .vendo/generated/remix-wiring.ts — one const, keyed by slot, binding each ported component back to the functions and components you already wrote — and your app hands the same const to both createVendo and <VendoProvider>.
vendo.ts
VendoRoot.tsx
vendo sync in predev and prebuild.
Instant and reviewed
Review never affects who can see a remix, because remixes are personal, always. It decides one thing only: where the fork executes.Instant
The default. The remix appears immediately and runs sandboxed, forever, and the ✦ mark becomes its management handle.No queue, no approval ceremony.
Reviewed
<Remixable review>. Your original keeps rendering, untouched — never a half-trusted render — and the verdict is reported in the conversation that asked for the remix.On approval the remix mounts natively in your page. On rejection it survives with the reviewer’s note, so the user can edit and resubmit.What crosses the fork boundary
The JSON-serializable props your call site passes follow the page. On mount, and again whenever they change, the wrapper couriers them to the server, the server repaints the remix on them, and the surface reopens. A remix moves with the page it was forked from rather than freezing at the values it was made on. Only props the captured baseline declares are admitted, and only serializable values cross — functions, elements, and class instances (Dates included) are dropped. The valuesvendo sync captured are the fallback, and only that: what a remix paints in the moment between its row landing and its first courier.
Host functions do not cross. Callbacks, your router, and context-style hooks are dropped, and a fork’s behavior is rewired through your API instead, exactly like any generated app.
Sync notices this for you. A component that reaches into host plumbing gets a warning suggesting
review, because plumbing does not cross the boundary and an approved reviewed remix runs natively where it would have kept working.The port
vendo sync splits every <Remixable> component in two.
The ported half
Real TSX in the screen dialect, carrying your JSX and your class names. A wish edits this, and it runs sandboxed.
The home half
The parts a sandbox cannot run, cut out and kept in your app. It runs natively, in your own bundle.
ported.source, and it is the file the agent opens and edits when a wish mints the remix.
That is the whole difference between a remix and a lookalike. The agent starts from your code with the ask applied to it, so everything the ask did not name survives.
The home half never leaves your app. It is generated into .vendo/generated/remix-holes/<Slot>.tsx and rendered by your own React.
Holes
A real component is rarely portable whole. Its chart is hand-rolled<svg>, its icons are inlined <path> data, its count-up hook reads requestAnimationFrame. None of that may enter the dialect — and none of it has to.
Sync cuts each unportable subtree out of the port and leaves a named element where it stood. The cut code is copied verbatim into the home half and rendered natively, in your page, by your bundle. That is why a remix keeps the real chart and the real icons rather than a sandbox’s impression of them.
A hole is a bet sync has to prove. A cut it cannot prove — a hook whose value paints more than one element, a prop whose type cannot ride a tree node — refuses the whole slot instead of shipping a guess.
Buttons
A plain<button> becomes the Kit Button. It is a tag rename, so your className and your onClick survive verbatim and your CSS goes on styling it. Only type is dropped, because the Kit Button already defaults to type="button".
style keys are narrowed on the same law: background becomes backgroundColor, and a key the paint allowlist does not carry is removed rather than guessed at. No value is ever inspected.
Where a handler calls one of your imported functions, sync generates one tool per action — reachable only from a handler — and the wiring binds it to that function. It joins the one registry under the name it declares, so your guard grades it and the audit trail records it exactly as it does any host tool. A press inside a remix is an audited intent, not a private call.
When a component cannot be ported
Fail-closed, at every layer. A slot sync could not split gets noported, so the generated wiring never names it, so <Remixable> never offers a ✦ on it — not a disabled one, not a greyed one, none. Your own markup renders alone, and the server refuses to mint a remix for that component even if something asks directly.
The reason is loud. Sync prints one warning per slot and one line per issue, naming the construct that defeated the split.
<Remixable> wrapper that cannot be captured at all is the harder failure, and it is an error that exits 2.
The ✦ gate is on the offer only. A component that stops porting keeps mounting and managing a remix a user already has, because revert is the way back.
What sync captures
vendo sync scans your source for <Remixable> wrappers, resolves each wrapped child through its import, and snapshots it to .vendo/remixable/<slot>.json.
The slot name is the component’s own exported identifier, so remixes survive call-site refactors. Wrapping the same component in several places is legal: one capture, many mount points.
Two more files are generated rather than captured, and both are rewritten whole on every run:
.vendo/generated/remix-wiring.ts, the const both call sites take, and one .vendo/generated/remix-holes/<Slot>.tsx per slot the carver cut. A slot that stops needing a home module loses its file, so nothing lingers for the wiring to dangle on.
Sync never skips a wrapper without saying so. Inline JSX, an unimported component, an anonymous default export, or source outside your project root each print one error: line naming the file, the line, and the fix, and the run exits 2.
A component that is intentionally never capturable can be acknowledged instead.
.vendo/overrides.json
Drift and reseed
Update the component, runvendo sync, and the new baseline overwrites the old one. Existing forks are now drifted: the hash they recorded no longer matches the captured baseline.
Drift is a warning, never an action. A drifted fork keeps rendering its own content, untouched.
open() attaches a server-authoritative seedDrift to the payload, edit results carry the same field, and the renderer shows an in-surface notice above the fork.
Update — the ✦ menu’s own item, and client.apps.reseed underneath it — rebuilds the remix on the new baseline by replaying every wish the seed recorded, oldest first. The remix is the whole list, so replaying only the wish it started from would silently undo everything asked for since.
conflict in two cases: an app that was not created from a host component, and a component that has not changed since the app was created.
Where to go next
Host components
Register the components a fork starts from, and see what sync captures.
Generated apps
The screen a fork becomes, and the checks every save faces.