How to develop and deploy the platform. The design is in architecture.md.
Local development needs the first two steps. Deploying needs all of them, once per host and account. After that every command below runs without a person.
The toolchain is Vite+. Use vp for every install, check, build and script, and vpx in place of npx. Never call npm or npx directly.
vp install.- Copy
secrets.env.exampletosecrets.envand fill it in. It is never committed. - Install the
cfCLI and runcf auth login. Every deploy step and every account call runs with that login. Wrangler is only the bundlercfcalls, and is never logged in to. - In the Cloudflare dashboard, on the zone of the host suffix, add one DNS record: type
AAAA, name*, value100::, proxied. The wildcard route needs a record to attach to, and thecflogin has no DNS permission, so no script can add it. - For email, in the same dashboard: onboard the zone in Email Service for sending, enable Email Routing, and optionally verify the owner's address as a destination, so mail to it is free. The deploy sets the catch-all rule itself.
The account needs the Workers Paid plan. docs/architecture.md lists every resource under "Infrastructure inventory".
All from this directory, all scripts in package.json, run with vp run.
| Command | Does |
|---|---|
vp install |
install the workspace |
vp run check |
check types, lint and formatting in every package |
vp run check:fix |
apply lint and formatting fixes, then check types |
vp run test |
the test suites |
vp run dev |
the local platform and the editor page against it, see "Local development" |
vp run dev:remote |
the editor page on localhost against the deployed platform, see "Local development" |
vp run push <path> |
upload one applet directory to the deployed platform; push:local for the local one. A new applet is private, and --visibility family or --visibility public opens this one; without the flag a push leaves the setting alone |
vp run remove <name> |
delete one applet from the deployed platform with its versions, storage and blobs; remove:local for the local one |
vp run deploy |
build the editor, cf deploy the bundler, the browser, the editor and the router, upload the router's secrets, and point the mail catch-all rule at the router. The platform deploys when the platform changes, not when an applet does |
vp run deploy <worker>... |
the same for only the workers named, any of bundler, browser, editor and router, always in that order |
vp run ship |
deploy, then push examples/preact-app and examples/mailbox as public, each checked to answer 2xx |
vp run templates |
embed the directories under examples/ into packages/api/src/templates.json as the templates a new applet starts from, after changing one |
vp run destroy |
list what the platform holds on the account; vp run destroy --yes deletes it: the four workers with every applet's storage, the registry and both buckets. The zone's DNS record, Email Routing and destination addresses stay |
vp run tail |
follow the deployed router: one line per request, and the error it logged, if any |
Secrets and the host suffix live in secrets.env, copied from secrets.env.example, never committed.
Every capability is an API route first; the editor, the CLI and the MCP tools call those routes' handlers and never add logic of their own. A new MCP tool starts as a new route.
What to run for each kind of change, once "One-time setup" is done.
| You changed | Run |
|---|---|
| the editor page, and want to see it while you work | vp run dev:remote for the page against the deployed platform, or vp run dev against the local one |
the router, the bundler or @std, and want to try it |
vp run dev, then vp run push:local examples/preact-app to fill the local registry |
| the editor page, and want it live | vp run deploy editor |
the router, the bundler or @std |
vp run deploy router bundler. @std is bundled into the bundler, so it counts as a bundler change |
| the page and the backend, or you are not sure | vp run ship, which deploys all four and pushes two examples as the check |
| the registry schema, a Durable Object class, a resource name, or anything else the deployed state would fight | vp run destroy --yes, then vp run ship. It costs one new magic link |
vp run destroy without --yes is always safe: it prints what exists and deletes nothing.
There are two ways to run the platform on your machine. Both open the same editor page on Vite, with hot reload, at http://localhost:5173. They differ in which platform the page talks to.
vp run dev |
vp run dev:remote |
|
|---|---|---|
| Use it for | work on the router, the bundler or @std, with or without the page |
work on the editor page with real data |
| Router and bundler | local, one cf dev each, the router on port 8787 |
the deployed ones |
| Registry, blobs, applet storage | local files under .wrangler/ |
the real D1 database, R2 buckets and Durable Objects |
| Touches the Cloudflare account | never | every read and write, including Deploy and Remove |
| A backend change shows up | on save, cf dev reloads |
after vp run deploy |
| Needs | secrets.env |
secrets.env with the public suffix, and a deployed platform |
vp run dev starts two processes and stops both when either exits:
- one
cf deveach for the router, the bundler and the browser, the router on port 8787, joined over the local dev registry. D1, R2 and Durable Object state are local files underpackages/router/.wrangler/. Only the browser's Browser Run binding reaches the account, as a remote binding - the editor page on Vite, which proxies
/api/tohttp://api.localhost:8787with the admin token
<applet>.localhost resolves to loopback in every browser, so the router routes by hostname with no DNS. Before it starts, the script writes packages/router/.dev.vars from secrets.env, always with the .localhost suffix whatever the file names.
The local registry starts empty. vp run push:local <path> uploads one directory, examples/preact-app to start.
Received mail can be simulated locally: the dev server's POST http://localhost:8787/cdn-cgi/handler/email?from=<sender>&to=<applet>@localhost with an RFC 822 message as the body reaches the router's email handler and the applet named by the local part. Sent mail is accepted locally and goes nowhere.
Nothing local asks for sign-in. The script writes the owner's email to .dev.vars as DEV_USER, and the local router treats every request without a bearer key as that user, who is the admin. So a private applet answers curl http://<applet>.localhost:8787/ and opens in a browser. The router only reads DEV_USER when its suffix is .localhost, and a deploy never uploads it.
To reset the local state, stop vp run dev and delete packages/router/.wrangler/.
The editor tests stub fetch with rows in the contract's shape. The client decodes every answer against packages/api, so a stub missing one field is a refused response and the page shows the decode error; when a fixture breaks, every test waits out its matcher timeout, which looks like a stall. Run one test with -t first.
The sign-in flow itself does not work locally, because *.localhost cannot share a cookie across subdomains. So the magic link, the 401 and the redirect for a missing session, and the users table are only checked on the deployed platform. The local platform also leaves the editor worker out: the Vite page is the local editor.
vp run dev:remote starts only the editor page on Vite. It proxies /api/ to https://api<suffix> with the admin token from secrets.env, so the page needs no session. It shows the real applets, logs and versions, and every action in it is real: Deploy makes a live version, Remove deletes the applet and its storage.
The router and the bundler do not run locally in this mode. A backend change needs vp run deploy before the page sees it. The live preview points at the real applet hosts, so a private applet shows the sign-in page there until the browser has a session for the suffix.
There is no mode that runs a local router against the real database. Wrangler's remote bindings do not cover Durable Objects or the Worker Loader, and the router is mostly those two.
applets/
README.md what this is and why
docs/ applets.md, platform.md, development.md and architecture.md
package.json the Vite+ workspace and its everyday scripts
packages/
router/ the first script, the only one with a route
bundler/ the second script, esbuild-wasm plus an npm installer
browser/ the third script, Puppeteer over Browser Run
std/ the library applets import
cli/ the repo's scripts: push, deploy, dev
editor/ the third script, a Vite React page over the API
ui/ the design system both pages build on: shadcn components on Base UI, one theme
examples/ one working applet per shape, deployed after every platform change
tools/ the vendored anti-slop lint plugin