Hosting on Cloudflare
Share the canvas, pages, tasks and agents with your whole team on Cloudflare, always available, with every branch captured in its own container.
A hosted Bud shares your canvas, pages and tasks with everyone you give a password: every route of main and your activated branches at every viewport, with their comments, differences and tasks, and with the factory its agents, which work on boat.dev. It's for a GitHub repository, opens instantly at any time, and nothing runs while nobody looks.
The bud.sh team's own runs at app.bud.sh: Bud for this website, from the hosting file in its repository (apps/web-marketing/bud.hosting.json), with a production build for captures and the factory on boat.dev.
How it runs
- Bud is served by a Worker: its interface as static files, and its server in a Durable Object that reads pictures and maps from R2 and tasks and comments from D1. It's the same Bud you run locally, with the canvas, Pages, Tasks and, with the factory, Agents. Pages shows main's captured pictures instead of your running site, at the viewport you pick, without browser controls; Files needs a checkout, so a hosted Bud leaves it out.
- Branches are captured in a container of their own per commit. It checks out the commit, installs dependencies, starts your dev server in development mode, captures every route in
bud.config.tsat every viewport with its element map, and is destroyed. One capture runs at a time; one that takes longer than 45 minutes is stopped, and one whose container disappears is captured again once. - The canvas opens on main. Comments made on a branch stay with that branch, and on the viewport they were made on. They're signed by the hosted user.
- Changes are found every five minutes by reading the heads of main and your activated branches from GitHub. No webhook is installed. A branch that moved is captured on its own at most once every 4 hours, at its newest head; a branch's first capture starts at once. Capture now beside a branch in the sidebar captures its head right away, also when its pages changed without a commit, as content from an API does.
What you need
- A Cloudflare account on Workers Paid, with an API token that can edit Workers, Durable Objects, Containers, R2, D1 and Queues.
- A GitHub repository, and a fine-grained token for it (and its submodules) with Contents read and write: Bud reads branches and checks out commits with it, and the factory pushes its
dev/branches with it. - Docker, which builds the capture image.
Put the credentials in the workspace's .env, which is never committed:
Terminal
CF_API_TOKEN=…
GITHUB_TOKEN=…Deploy
Describe the project in a hosting file, kept out of the repository:
JSON
{
"project": "/path/to/your/checkout",
"repository": "owner/repository",
"submodules": { "ds": "owner/design-system" },
"main": "main",
"files": ["bud.config.ts"],
"captureIntervalHours": 4
}project is your checkout's folder with bud.config.ts, relative to the hosting file. path is that folder inside the repository when it isn't the root, such as a site in a monorepo ("path": "apps/site"); captures install the whole repository with the lockfile at its root (Bun, pnpm or npm) and run Bud in that folder. domain serves Bud on a host name of your own on the same Cloudflare account, such as "domain": "app.example.com"; without it, Bud is on your workers.dev subdomain. captureIntervalHours sets how long a branch's automatic captures wait after its last one (4 by default).
Captures build each branch with your branches.build recipe, or your Vite dev server without one. A dev server compiles every page as it's asked for, so a capture loads only two pages at a time and takes a while. For a faster capture, give the hosting file its own recipe and how many pages to load at once:
JSON
{
"branchBuild": {
"build": "npm run build",
"serve": "npm run preview -- --port $PORT"
},
"captureConcurrency": 6
}Captures walk your flows too, with each branch's own scripts. A flow script your repository doesn't hold yet can come from your checkout through files, such as "files": ["bud.config.ts", "bud/flows/booking.ts"].
branchBuild replaces branches.build for hosted captures only, so local Bud keeps your dev server. Its commands run in the branch's checkout and find the captured paths in BUD_ROUTES, one per line, for builds that render only those pages; serve listens on 127.0.0.1:$PORT. A production build serves pages quickly enough for six or more at once. Keep source maps reachable next to the built files, so comments still name their component and source line.
Captures run one at a time, five minutes apart: one started straight after another ran slow or failed. A capture that fails, such as a build that stalled, is tried once more at the next check; if that fails too, the branch waits for its next commit. A deploy waits for a capture under way to finish first, since it replaces the capture container; --force deploys at once.
Then deploy from a checkout of Bud's repository, where the hosting scripts live (they aren't part of the npm package):
Terminal
node packages/app-cloudflare/scripts/deploy.ts --hosting .hosted/project.jsonThe script builds Bud, reads your bud.config.ts (routes, viewports, logo) into the Worker, creates the bud-previews bucket, the bud-work database and the bud-captures queue when they're missing, generates a user and password into .env (BUD_HOSTED_USER, BUD_HOSTED_PASSWORD), stores every credential as a Worker secret and deploys the bud-hosted Worker. It then signs in as that user and fails unless the page loads this build's interface and assets. It only creates or changes resources whose names start with bud-. Deploy again after changing bud.config.ts.
The factory
A hosted Bud runs the factory on boat.dev, whatever sandbox plugin the project uses for development. Give the hosting file the factory as the host runs it, which replaces the project's own:
JSON
{
"factory": {
"setup": "npm install",
"dev": { "command": "npm run dev -- --port $PORT --host 127.0.0.1", "port": 5173, "path": "/" }
}
}and add to .env:
Terminal
BOAT_API_KEY=…Agents read the task database from Bud's public address when you add it to the Worker's settings as BUD_PUBLIC_URL, such as your workers.dev URL; without it they work without. Sandboxes clone the repository and push their run's dev/ branch with GITHUB_TOKEN, which each of those commands gets alone; it never stays in the sandbox, where the agent works. Bud pushes nothing but run branches; to stop the token itself from touching main, protect main with a ruleset and use a token of an account the ruleset doesn't let through. Bud's server carries runs on with its own timer while visitors are away, and checks pushed branches for a merge through GitHub. A dev server that checks host names must allow .on.boat.dev; for Vite, __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS in dev.env does without changing your config. Statuses: open Tasks and choose Add factory statuses.
On your own server
Your company platform can serve the same Bud behind its own sign-in, on the Worker's pictures, tasks, comments and branches. Install @bud.sh/app there and hand it every request once your platform has signed the person in; they author the request's comments and task edits:
TypeScript
import { createNodeHost } from '@bud.sh/app/node-host'
const bud = await createNodeHost({ url: process.env.BUD_URL, token: process.env.BUD_REMOTE_TOKEN })
// In your server, after sign-in, for every path of Bud's site:
return bud.fetch(request, { author: { name: user.name, email: user.email } })Bud answers every path of its site, its files included, so give it a host name of its own. Factory runs are carried on by the Worker: your server passes its factory requests on, with the signed-in person, and needs no keys of its own. BUD_URL is the Worker's address; the Worker opens its remote API, /__bud/remote/*, only to BUD_REMOTE_TOKEN: add a long random value to .env and deploy again, since a Worker without one has no remote API. Keep @bud.sh/app at the version the Worker was deployed with.
To try a change first, node packages/app-cloudflare/scripts/dev.ts --hosting .hosted/project.json runs the same Worker on your machine, with local storage or, with --remote, your account's.
To work on Bud itself against what your team sees, add --hmr: Bud's own development server then serves the hosted app with hot reloading, on your account's pictures, tasks and comments with --remote. Tasks and comments you write there reach the hosted Bud. Pictures and maps are kept on your machine after their first download, since they never change. With Portless running it's at https://hosted.bud.localhost, otherwise at http://127.0.0.1:5175.
Terminal
node packages/app-cloudflare/scripts/dev.ts --hosting .hosted/project.json --remote --hmrTime and cost
Measured with a project of 131 routes at two viewports (1440 and 390 pixels) on standard-4 capture containers (4 vCPU, 12 GiB):
| Step | Time |
|---|---|
| Opening Bud after a while without visitors | 1–2 s |
| Opening Bud while someone uses it | about 0.1 s |
| A branch head: checkout, submodules and cached dependencies | about 20 s |
| All 131 routes captured at both viewports with their maps | 13–18 min |
A branch capture costs memory and disk for the minutes it runs, and CPU only while it works. Captured from a dev server, two pages at a time, 131 pages at two viewports took 20 to 40 minutes and about $0.10–$0.20; from a production build with branchBuild and six pages at a time, the same capture takes about 10 minutes, build included, and about $0.07. One viewport takes about half as long and costs about half as much. With one automatic capture per branch every 4 hours, main and one other branch that both keep moving cost at most about $25 a month with a production build (about $45 from a dev server), plus any captures started by hand. Serving Bud costs almost nothing: Worker requests and the server object's time while pages are open, a few dollars a month even if a tab stays open all day. Pictures, tasks and comments stay within the storage Workers Paid includes.
Captures use Chromium inside the container, the version your local Bud uses, so hosted and local pictures match. Browser Run was measured too: every page's modules crossing from the browser to the container made each page take about 25 seconds instead of 5, and its older Chrome renders differently.
Limits
| Limit | Value |
|---|---|
| Capture containers at a time | 1 |
| Longest capture | 45 minutes |
| Pictures of a deactivated branch kept | 30 days |
| Branch check | every 5 minutes |
| Automatic captures per branch | every 4 hours |