brust 2.0.0-alpha.5
Guide / Deployment

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

Live demoSwitchPanels · native

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)

The Bun server

/srv/pokedex
$ bun install --production
$ bun run build
$ BRUST_ADDR=0.0.0.0 BRUST_PORT=8080 bun dist/index.js
  • bun dist/index.js is the same as brust start with that dist/. Keep dist/ inside the app: it imports react and @brust/core (and elysia for actions) from node_modules at 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 pass Host through (or set X-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/.

~/my-site
$ 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 needs ssg.params, which returns one record per page; without it the build fails with ssg-params-missing. ssg: false writes nothing for that route.
  • ssg has 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 becomes 404.html. Without one the build writes a minimal page and warns ssg-404: with no 404.html, Cloudflare Pages answers every unknown path with the home page.
routes.tsx
{
  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/
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; --strict makes it a build error and keeps the previous dist/.
  • 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.prefix or cache.bypass reads a header, cookie, query or host fails with ssg-varies: a file is one page for everyone. ttl_seconds and [cache] do not apply to files.
  • Cloudflare Pages serves /docs from docs.html, applies _headers and _redirects, and answers unknown paths with 404.html. Workers Static Assets works too, with not_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/
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.json lists 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 with ssg-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.

~/my-site
$ brust build routes.tsx --target static
$ cd dist && wrangler pages deploy --project-name my-site
Note

To 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.

~/pokedex
$ 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 with cf-unsupported; swap them with [cloudflare.alias].
  • cache.invalidate reaches one isolate unless a cache adapter broadcasts it. No gzip in the Worker (Cloudflare compresses at the edge), no streaming HTML.
Note

The full Workers guide, with D1 and the cache on KV and Durable Objects, is in Cloudflare.