Deployment
The same app runs as one Bun process on your own machine, as static files on any static host, or as a Cloudflare Worker.
Pick a target
One process: Rust serves HTTP, Bun workers run your code. Needs Bun on the host, no Rust toolchain.
$ bun run build
$ BRUST_PORT=8080 bun dist/index.js
[brust] listening on 127.0.0.1:8080 (io: hyper(tokio))
[brust] ready (10 workers)Every page rendered at build time and written as a file. Any static host: Cloudflare Pages, Workers Static Assets. No server.
$ brust build routes.tsx --target static
$ wrangler pages deploy dist/staticThe same app as a Worker: the prebuilt wasm host (@brust/core-cf) plus your bundled code. Needs wrangler.
$ brust build routes.tsx --target cloudflare
$ wrangler deploy -c dist/cf/wrangler.jsoncThe Bun server
$ bun install --production
$ bun run build
$ BRUST_ADDR=0.0.0.0 BRUST_PORT=8080 bun dist/index.jsbun dist/index.jsis the same asbrust startwith thatdist/. Keepdist/inside the app: it importsreactand@brust/core(andelysiafor actions) fromnode_modulesat runtime.- The host needs Bun. The Rust server comes from the npm package for the platform: macOS or Linux (glibc or musl), arm64 or x64.
- Run it under a supervisor (systemd, a container restart policy): a dead worker stops the process with exit 1 after a drain, and it does not restart itself.
- There is no TLS in
brust start. Put a proxy in front, and passHostthrough (or setX-Forwarded-Host), or same-origin action POSTs get a 403. - The page cache is per process. For several instances, add a cache adapter so they share pages and invalidations: Cache adapters.
Static files
--target static builds the normal dist/, then renders every page through the real server and writes it to dist/static/. A written page is byte-identical to what brust start sends for that path: the same templates, jobs, islands and scripts. brust start still works on the same dist/.
$ brust build routes.tsx --target static
$ brust build routes.tsx --target static --strict
$ wrangler pages deploy dist/static- A static pattern (
/,/docs/intro) writes one page. A dynamic pattern needsssg.params, which returns one record per page; without it the build fails withssg-params-missing.ssg: falsewrites nothing for that route. ssghas one field,params, and only on leaf routes. There is no client fallback for a path the build did not write: it is a 404.- The top-level
*route becomes404.html. Without one the build writes a minimal page and warnsssg-404: with no404.html, Cloudflare Pages answers every unknown path with the home page.
{
path: '/poke/{name}',
Component: Poke,
loader: ({ params }) => getPokemon(params.name),
ssg: { params: async () => (await listPokemon()).map((p) => ({ name: p.name })) },
},
{ path: '/live/{id}', Component: Live, ssg: false },dist/static/index.html /
dist/static/docs.html /docs
dist/static/docs/intro.html /docs/intro
dist/static/404.html the * route (or a default one, with a warning)
dist/static/_brust/client/*.js runtime, native chunks and island chunks
dist/static/public/** ./public
dist/static/_headers nosniff, X-Powered-By, cache-control for the assets
dist/static/_redirects one line per redirect()
dist/static/ssg.json pages, redirects, skipped routes, the 404 route, warnings- There is no manifest, template, job or actions file in
dist/static/: only what a browser fetches. - Loaders see an empty request: no headers, cookies or query. Each read is a warning
ssg-request-read;--strictmakes it a build error and keeps the previousdist/. - A
redirect()becomes a line in_redirects. Any other non-200 status fails the build (ssg-render), and headers a loader sets are dropped with a warning (ssg-headers). - A route whose
cache.prefixorcache.bypassreads a header, cookie, query or host fails withssg-varies: a file is one page for everyone.ttl_secondsand[cache]do not apply to files. - Cloudflare Pages serves
/docsfromdocs.html, applies_headersand_redirects, and answers unknown paths with404.html. Workers Static Assets works too, withnot_found_handling = "404-page".
Static pages + actions on Pages
With [actions] configured, --target static also writes a Cloudflare Pages Worker that serves only the action prefix. One Pages project serves the pages as free static files and the actions on the same domain.
dist/static/_worker.js/index.js the Worker: the action prefix -> your actions, anything else -> the static files
dist/static/_worker.js/core/ the --target cloudflare worker tree (wasm host + your actions.ts)
dist/static/_routes.json only the prefix and the deploy inputs reach the Worker
dist/static/.assetsignore _worker.js, _routes.json, .assetsignore: never public assets
dist/wrangler.jsonc the Pages project file: name, compatibility date, pages_build_output_dir_routes.jsonlists the prefix and the deploy inputs only, so pages and assets never invoke the Worker.- The Worker sends the prefix to your actions and anything else that reaches it to the static files. The deploy inputs themselves (
_worker.js,_routes.json,.assetsignore) are a 404 and are never served. - Admission still runs in Rust, exactly as on Workers: method gate, body cap,
Content-Encoding, the cross-site guard with its 403. - A
[cache]adapter that needs a Cloudflare binding (KV, Durable Object) fails withssg-actions-cache: the pages are files and the Worker serves actions only. - This output is for Cloudflare Pages only. Do not point Workers Static Assets at it.
Deploy from dist/, where the project file is. It carries no bindings and no secrets: set those with wrangler pages secret put or the dashboard.
$ brust build routes.tsx --target static
$ cd dist && wrangler pages deploy --project-name my-siteTo keep the actions on their own domain instead, build the pages with [actions] base_url and the actions with --actions-only: see Actions on their own origin.
Cloudflare Workers
The template has the two scripts; deploying needs wrangler login or an API token.
$ bun run dev:cf
$ bun run deploy:cf[server]and[workers]do not apply on Workers; the build warns and ignores them.bun:*imports fail withcf-unsupported; swap them with[cloudflare.alias].cache.invalidatereaches one isolate unless a cache adapter broadcasts it. No gzip in the Worker (Cloudflare compresses at the edge), no streaming HTML.
The full Workers guide, with D1 and the cache on KV and Durable Objects, is in Cloudflare.