Deploy a dynamic Cloudflare app before signup. Keep it if it works.
Up gives coding agents a Worker, Static Assets, and platform bindings before anyone creates an account, opens OAuth, or copies an API token.
app folder → Worker + assets + bindings → public URL → keep or disappear
Up is an independent, user-land experiment. It is not an official Cloudflare product and is not supported by Cloudflare.
A dynamic Up app is an ordinary folder:
my-app/
├── public/
│ ├── index.html browser UI
│ └── app.js
├── worker/
│ ├── index.js dynamic Worker entry point
│ └── helpers.js normal module imports work
└── up.json platform bindings
Inspect first; this creates no account and makes no remote request:
bunx github:acoyfellow/up inspect ./my-appOr open the same read-only plan in a friendly localhost UI:
bunx github:acoyfellow/up open ./my-appThe composer uses a random local path, binds only to 127.0.0.1, and shows exact assets, Worker modules, bindings, exclusions, public/temporary risk, credential isolation, and the deploy command before mutation. After separate public-exposure and Terms confirmations, it runs the same pinned CLI and streams claim-redacted Wrangler progress to the page, then links the live app. It restores active or expired project state on reopen, counts down from Wrangler’s authoritative expiry, shows partial health-check failures, and supports a clean retry.
For an installed private Up workspace, start the localhost bridge once:
bunx github:acoyfellow/up bridgeThe authenticated web editor can then save and deploy a Worker through local Wrangler. The bridge listens only on 127.0.0.1, accepts the configured private Up origin, requires explicit public/Terms consent, and never returns credentials or ownership links to the hosted instance.
Then deploy:
bunx github:acoyfellow/up deploy ./my-appDeploying dynamic app with 2 assets without a Cloudflare account
Bindings: CACHE, DB, ROOMS…
Live now
https://up-a1b2c3d4e5.example-account.workers.dev
Expires in about 60 minutes unless claimed.
Public: anyone with this URL can open it.
Keep it: run `up claim ./my-app --open` to open the ownership flow.
Up stores the sensitive link in project-scoped local state and does not print it.
No signup, login, permanent token, repository, or Up server is involved. Interactive use asks you to accept Cloudflare’s Terms and Privacy Policy. Agents and other non-interactive sessions must pass the explicit approval flag:
up deploy ./my-app --accept-cloudflare-termsTo keep the account:
up status ./my-app
up claim ./my-app --openRemove only the local cache and ownership metadata—never remote resources:
up forget ./my-appIf it is not claimed within about 60 minutes, Cloudflare deletes the Temporary Account, Worker, bindings, and data.
The browser flow gives you ownership of the Temporary Account. The Worker, bindings, and data stay there. Your source code stays in the local folder.
Connect Wrangler with OAuth—no Up API key and no copied Cloudflare API key:
bunx wrangler@4.103.0 login
bunx wrangler@4.103.0 whoamiIf whoami lists more than one account, choose the account created by the ownership flow and copy its account ID. Then reconnect the exact Worker name shown in the original workers.dev URL:
up handoff ./my-app exact-worker-name \
--account-id 0123456789abcdef0123456789abcdefUp first confirms that Worker already exists in that account. It then deploys without --temporary; Wrangler inherits the existing KV and D1 resources from the Worker's bindings instead of creating replacements.
Ownership is not authentication. The app remains public after you keep it. Before adding sensitive data, add Cloudflare Access or another login and verify an anonymous request is denied.
Create a scoped Cloudflare API token only when you later add CI/CD. Local development should use wrangler login.
Continue this existing Cloudflare Worker from ./my-app.
1. Ask me to complete `wrangler login` in the browser. Do not ask for or copy an API key.
2. Run `wrangler whoami` and ask me which account was created by the ownership flow if several are listed.
3. Use the exact Worker name from its workers.dev URL.
4. Run `up handoff ./my-app <worker-name> --account-id <account-id>`.
5. Do not create replacement KV or D1 resources. Preserve the existing bindings by name.
6. Test the deployed URL and every binding.
7. Remind me that the URL is public. Ask before adding Cloudflare Access or creating a scoped CI token.
See After you keep an app for the full checklist.
up.json declares the resources the app expects:
{
"bindings": {
"kv": ["CACHE"],
"d1": ["DB"],
"durableObjects": [{ "binding": "ROOMS", "className": "Room" }]
},
"checks": [
{
"name": "Binding state",
"path": "/api/state",
"status": 200,
"jsonKeys": ["pageViews", "roomVisits", "notes"],
"bindings": ["CACHE", "DB", "ROOMS"]
}
]
}Checks are bounded same-origin GET requests. The composer always checks /; declared checks prove the app routes that exercise specific bindings and report expected top-level JSON keys.
Up turns that into a temporary Wrangler graph. Wrangler auto-provisions KV and D1, deploys Durable Object classes with append-only SQLite migrations, binds Static Assets as env.ASSETS, and uploads the Worker and browser files together. Added DO classes append a migration; deletion or rename fails before deployment.
export class Room {
constructor(state) {
this.state = state;
}
async fetch() {
const visits = ((await this.state.storage.get('visits')) || 0) + 1;
await this.state.storage.put('visits', visits);
return Response.json({ visits });
}
}
export default {
async fetch(request, env) {
const cached = await env.CACHE.get('key');
const rows = await env.DB.prepare('SELECT * FROM notes').all();
const room = env.ROOMS.get(env.ROOMS.idFromName('main'));
if (new URL(request.url).pathname === '/api/state')
return Response.json({ cached, rows, room: await room.fetch(request).then((r) => r.json()) });
return env.ASSETS.fetch(request);
}
};Browser code in public/ gets a normal same-origin API. Worker modules in worker/ get real Cloudflare bindings. Legacy root index.html + _worker.js folders remain accepted as a migration path, but cannot be mixed with the canonical layout. Using the ownership flow keeps the entire Temporary Account and the supported resources created inside it.
| Primitive | Anonymous Up 0.0.1 | Temporary Account contract |
|---|---|---|
| Worker runtime | Yes | Dynamic request handling on workers.dev |
| Static Assets | Yes | 1,000 files; 5 MiB each |
| KV | Yes | Auto-provisioned from up.json |
| D1 | Yes | One database; 100 MB total |
| Durable Objects | Yes | Class binding plus SQLite migration |
| Queues | Supported upstream; Up wiring next | Temporary Accounts allow up to 10 |
| Hyperdrive | Supported upstream; Up wiring next | Two configs; requires an existing database |
| Certificates | Account operation | Supported by Temporary Accounts; configured outside up.json |
| R2, Workers AI, Access | No | Not in the current Temporary Account matrix |
| Workflows, Browser Rendering, Containers, Sandboxes | No | Not in the current matrix |
The boundary is deliberately upstream-shaped. Up does not fake unavailable bindings or proxy them through a permanent service account.
Capa turns OpenAPI specs into generated Cloudflare service-binding Workers. Capa main at 382359f contains 14 API capabilities and 5,998 generated operations—not 5,998 reviewed or equally safe actions—with { result, evidence } returned from every call.
An isolated spike proved the intended composition inside one Temporary Account:
Up app Worker
└── service binding → Capa capability Worker
└── provider credential secret
A generated read-only capability returned a real 200/pass evidence receipt, survived caller redeployment, and leaked no credential. Capa main Stripe also reached the real upstream API with a deliberately fake token and returned a bounded 401/fail receipt without exposing authorization data.
The runtime composition is proven; click-to-connect is not shipped yet. Capa needs immutable per-capability install bundles before Up can safely offer a local connector UI. Existing permanent Capa Workers cannot be bound cross-account; each selected capability Worker must install into the same Temporary Account as the app.
Read the integration and local composer contract and spike receipt.
examples/binding-lab is a framework-free dynamic app using:
- a Worker API;
- Static Assets;
- KV for an edge counter;
- D1 for notes;
- a Durable Object for one coordinated room.
Deploy it anonymously:
up deploy examples/binding-lab binding-lab --accept-cloudflare-termsA real isolated smoke test returned HTTP 200 from both its page and /api/state, with live values from all three bindings.
Up deliberately stays close to Cloudflare’s bleeding edge instead of recreating its deployment APIs:
- validates and snapshots the app into a private staging directory;
- reads a narrow
up.jsonbinding manifest; - generates a temporary Wrangler graph for Worker, assets, KV, D1, and Durable Objects;
- starts pinned Wrangler in an isolated Up-owned home;
- removes every current and deprecated Cloudflare credential variable;
- runs
wrangler deploy --temporarywith experimental resource provisioning; - takes the authoritative public URL from Wrangler output;
- stores the sensitive ownership link locally without printing it.
Wrangler owns proof-of-work, Terms acceptance, short-lived credentials, resource provisioning, upload, account reuse, and dashboard claiming. Up does not reimplement the unpublished provisioning protocol.
The isolated state lives under ~/.up/anonymous with private permissions. Repeated deploys during one active session reuse the Temporary Account, so one sensitive ownership link controls every app and binding in that session.
| Contract | Anonymous Up deployment |
|---|---|
| Runtime | Dynamic Worker plus same-origin Static Assets |
| Visibility | Public; URL possession is enough to read it |
| Lifetime | Up to/about 60 minutes unless claimed |
| Credentials | Existing Cloudflare credentials are removed from the child process |
| Name | Stable path fingerprint, or a strictly validated explicit name |
| Ownership | Claim URL grants the whole temporary account; treat it as a secret |
| Production | Not intended for production or CI/CD |
Official primitive: Claim deployments (Temporary Accounts).
bunx github:acoyfellow/up initThis writes .up/SKILL.md. Ask an agent to read it, build the app, deploy it, fetch the real Worker URL, exercise its bindings, revise, and redeploy without stopping for browser authentication.
The original company-private experiment remains available as a secondary mode:
up private ./dist team-tool --origin https://up.example.comThat path requires a customer-owned Up installation, Cloudflare Access, and browser-mediated CLI authentication. It is retained for comparison and dogfood; it is no longer the default positioning.
The old product asked an organization to install infrastructure before anyone could publish. The new product moves the dynamic graph ahead of ownership:
old: install → authenticate → configure bindings → deploy
new: declare bindings → deploy → exercise the real stack → decide whether to own it
The point is not anonymous static hosting. The point is letting an agent discover how far a real Cloudflare application can get before signup becomes necessary.
cli/up.ts staging, binding graph, anonymous deploy/claim
examples/binding-lab/ Worker + Assets + KV + D1 + Durable Object proof
tests/anonymous-cli.test.ts subprocess, binding config, isolation, claim proof
skills/up/SKILL.md agent dynamic-app contract
docs/anonymous-first-brief.md product and security boundary
docs/capa-integration.md proven composition and local composer contract
src/ retained company-mode runtime
bun install
bun run check
bun run test:e2eOrdinary tests never create anonymous accounts. The CLI suite uses a real subprocess and fake Wrangler executable to verify staging, generated binding configuration, argument boundaries, credential removal, claim redaction, and public URL handling. Live Temporary Account tests are deliberate manual checks because they create rate-limited remote resources.
- Anonymous-first source brief
- Security
- Capa integration contract
- Historical company-private source brief
- Binding Lab
MIT © Jordan Coeyman