app.tsx, a single default-exported React component. The
row that owns it is an AppDocument with format vendo/app@1, and every user
owns their own copy — import, fork, sharing, and publishing mint a fresh app_
id, so artifacts carry no data, grants, or authority.
Zero config: layer 1 needs nothing. createVendo() mounts the apps block
unless you pass apps: false, and a screen renders in the host-embedded
renderer with no sandbox account, no key, and no flag. Layer 2 is gated on one
thing, a configured sandbox adapter.
The screen
A screen is plain TSX with a small, closed surface:- Two imports, no more:
react(its hooks) and@vendo/screen. There is no bundler and nonode_modulesinside a screen. - Data through
useQuery("tool_name", input?). The tool name and the input are written-out literals, because a screen’s queries are read out of the file and executed before the component renders — so no prop, state, or other query’s result can reach one. Only read tools, one result per tool. - Actions through
tools.tool_name(args), from a handler. A tool call in the render body would fire on every render, so it is refused. - No DOM.
document,fetch, timers,processand<div>do not exist here. Layout is<Stack>/<Row>/<Grid>; text is<Text>.
@vendo/screen exports is exactly the component Kit plus the components
your host registered — so a name the screen renders is a name your app really
has. Props are checked against each component’s own schema.
A screen stores no tree. Its tree is what rendering it produces, so the screen
is re-run on every open: the queries resolve against the world as it is that
instant, and the payload carries today’s numbers. There is no snapshot to go
stale.
The checks
Every save faces the same five stages, whoever wrote it — the generation loop, Claude Code, or a person with an editor:- Compile — it has to be valid TSX.
- Scan — the two rules a compiler cannot state: the import surface, and the query/tool discipline above.
- Type-check — the real TypeScript compiler, against declarations derived from the Kit’s schemas and your tools’ own input and output schemas. A misspelled response field or a wrong payload key is a type error.
- Run once — the query plan is executed for real and the screen is booted on the answers a tool actually gave.
- Tree — the rendered tree is validated, and every component in it must be one the host registered.
The receipt your agent gets
Whoever calledvendo_make gets four fields of words, never pixels:
say,
close to verbatim, and stop.
"failed" means the bindings claimed data your host does not return, or the
props did not type-check. "partial" is the one a host branching on status
most needs: a half-built app reads as half-built instead of as plain success.
In process the call returns fast. The first streamed view part carries the app’s
permanent id, so your loop gets a vendo/app-ref@1
(envelope contract) while the build streams over the
wire.
Where the screen lands
By default a screen lands in the person’s own list of views. A slot puts it in your product instead, and a slot is your markup:@vendoai/ui as a direct dependency. Empty, the slot renders your children
untouched with no wrapper, so you can inline one anywhere. Filled, it shows the
build skeleton, then the live view, then a failure with a retry. Never a blank
hole. One view per slot per person, and a second view evicts the first.
When a person presses a guarded button inside the view — one that parks on the
approval guard — VendoSlot auto-mounts the approval modal
centered over the page. Nothing to wire. If you render a bespoke slot with
AppFrame or TreeView, use useApprovalModal
to mount it yourself.
Your agent aims at a slot by id:
appId in the vendo/app-ref@1 envelope
your loop already received:
Building a document or block editor? That is this shape. One
<VendoSlot>
per block, with the block’s own id as the slot id. A generated view then lives
in a block like any other: it moves when the block moves, it is scoped to the
person with that document open, and your editor keeps owning layout.Moving a screen after the fact
“Put that on my dashboard”, said about a view they already have, isvendo_apps_pin. It answers with what it displaced, so your agent can say what
moved:
vendo_apps_unpin clears the slot, and the view itself is untouched. Both are
writes, so under a cautious policy the first one parks in your approvals
queue, where the person sees the tool, the view, the slot, and the exact
arguments before anything moves. Routes:
HTTP routes.
The three layers
- Screen app: no server.
app.tsx, rendered by the host-embedded renderer, interactive through React state and guarded host tools. Most apps stay here. Server-shaped needs (schedules, away runs) usually ride an automation on the same app, not a machine. - Screen app + machine: the same screen plus a persistent per-app sandbox
where execution lives — custom server code, third-party egress with secrets,
heavy logic, working data. The machine never draws UI. Gated by one thing: a
configured
sandboxadapter (createVendo({ sandbox })). There is no flag beside it. - Machine everything: the machine also serves a real web app and the host embeds its URL as the app surface. Same sandbox gate; see Layer 3 reachability and venues for what it additionally needs and its current caveats.
The machine
A graduated app owns one persistent sandbox. It sleeps as a snapshot, wakes in about a second when poked, and auto-sleeps after five minutes idle. Vendo owns only the boundary of the box (the environment in, HTTP on$PORT out, and a
vendo.json manifest); inside it, any language and framework works. A coding
agent lives in the box and does every server build and edit there, verifying its
own endpoints before reporting done.
The box holds no host credentials. Its single authority path is the /box
callback surface on the host’s Vendo server, authenticated by a per-app bearer
token: durable rows over plain HTTP, and host tools through the same guard-bound
registry chat uses, with approvals and audit intact.
Anything the box wants beyond that boundary, a secret value or an outbound
domain, is an approve-once owner grant. See
Secrets and outbound requests for the exact
grant and injection rules.
Progressive generation
A build writes a plan first, and the plan’s skeleton is on screen within seconds. The screen itself paints once — when it is saved and the five stages have passed — replacing the skeleton in place under the same stream id. The stock ai-SDK client reconciles the payload rather than appending duplicates. You do not need to change your client. Existing consumers ofdata-vendo-view
parts see the skeleton followed by the finished screen under a single stable
part id.
Graduation
Layer 1 to 2 is invisible and additive. When an instruction needs server capability (a schedule, egress with secrets, heavy logic, app-owned state), the runtime provisions a machine, sends the build to the in-box agent, syncs the box’svendo.json declarations, and parks any needed egress approval. The
existing screen keeps serving throughout.
Layer 2 to 3 is an honest UI rewrite in the same box, gated behind the
experimental served-apps flag. The box agent builds a real web app beside the
/fn endpoints, the screen keeps serving through the whole build, and the
document flips to ui: "http" only after the host itself verifies the served
root answers. Opening a served app wakes the machine and embeds its public URL in
a sandboxed iframe; the wake latency is the loading state, and the URL is only
valid for the current wake.
Failure handling for both hops is specified in
Graduation rollback.
Wire reference
The exact flags, environment contracts, and failure rules behind the model above.Component limits
These three bound a stored wire document’s generated components, and the document validator enforces all three. A generated app carries at most 16 generated components, each at most 64 KB of source, and 256 KB across all of them together. Anapp.tsx screen has no byte budget — it is measured for compilation, types,
and one real render, never for size.
Environment
The app machine environment lists the exact variables injected into the box and the/box callback routes.
Layer 3 reachability and venues
Layer 3 has no gate of its own: it is a narrowing of layer 2, so a configuredsandbox adapter is the only opt-in. What it additionally needs is a door to be
served THROUGH — the mounted wire (createVendo().handler, which answers
/apps/:appId/serve/**) plus a VENDO_BASE_URL to make that URL absolute. A
deployment missing either is told so before it plans: the layer-3 lane is shut,
and “this host cannot serve its own web pages for an app” arrives as a plain
line in the plan instead of a build that runs and then fails.
Every served app — the owner’s own included — is opened at that authenticated
proxy URL, never at the sandbox provider’s public ingress. The proxy re-checks
can(viewer) against live rows on every request, so a revoke bites the next one,
and it wakes the machine only after that check passes.
Layer 3 works on both venues: BYO e2b (sandbox: e2bSandbox(), which reads
E2B_API_KEY as its credential) and the Vendo Cloud hosted sandbox. Cloud ingress is a
single-label hostname on the existing *.vendo.run certificate:
https://<id-suffix>-m.vendo.run, where <id-suffix> is the machine id minus
its two-character prefix. The console mints that canonical-port URL; for any
other port the SDK rewrites the host string SDK-side, in your Node process,
inserting the port before the suffix as <id-suffix>-<port>-m.vendo.run.
Graduation rollback
A failed server build rolls back to the pre-edit snapshot; a failed screen rewrite keeps the working screen and reports the miss. In both cases the last working surface keeps serving.Secrets and outbound requests
Machine egress is deny-by-default at the provider network layer, and both secrets and egress are approve-once owner grants:- An app declares secret names. Each declared secret needs one owner approval before its value enters the box environment. Only declared and granted secrets inject, at provision and at the env re-injection before each in-box edit. An ordinary wake resumes the snapshot’s environment, so a grant or revocation decided while the machine slept fully lands at the next edit or re-provision. Grants are never carried by shares, forks, or publishes.
- An app declares the domains it talks to in
vendo.json. Each declared domain parks one approval card; a machine never provisions or wakes with an unapproved declared domain. An app that declares nothing can reach only its own boundary (the host callback origin and the inference endpoint).