Configuration
One optional file, brust.toml, next to package.json. Every key has a default; a missing file is fine, a malformed one stops the server with a message.
Where settings come from
For brust start, an environment variable beats a command-line flag, a flag beats brust.toml, and brust.toml beats the default. An empty variable counts as unset. When you start the server from your own file with run(), the config you pass takes the place of the flags: environment, then run({ config }), then brust.toml, then the defaults.
Server and workers
[server]
address = "127.0.0.1" # default localhost
port = 1337 # 1..65535, default 1337
max_loader_response_bytes = 8388608 # 256 KiB..64 MiB, default 8 MiB (env BRUST_MAX_LOADER_RESPONSE_BYTES)
[workers]
count = 4 # 1..1024, default one per CPU core
slot_bytes = 262144 # 64 KiB..16 MiB, default 256 KiB (env BRUST_SLOT_BYTES)Each worker is a Bun thread that runs loaders, jobs and actions. Rust handles HTTP on its own threads, one per core by default.
Configure from code
brust start takes --port, --workers, --dist-dir and --entry, and nothing else. To set the rest without environment variables, call run() from your own file.
// server.ts: run it with 'bun server.ts' after 'brust build'
import { run } from '@brust/core'
await run({
distDir: 'dist', // default 'dist'
entry: 'routes.tsx', // the routes module; default 'routes.tsx'
config: {
host: '127.0.0.1', // default 'localhost'
port: 8080, // 0..65535 (0 = any free port); default 1337
workers: 4, // 1..1024; default one per CPU core
renderSlots: 4, // 1..64 per worker; default min(cores, 16)
ioThreads: 4, // 1..256 Rust I/O threads; default one per core
callTimeoutMs: 30_000, // a call still running after this answers 504
drainTimeoutMs: 10_000, // how long a graceful shutdown waits
bootTimeoutMs: 30_000, // start waits this long for every worker
slotBytes: 262_144, // one render slot's shared buffer; 64 KiB..16 MiB, default 256 KiB
maxLoaderResponseBytes: 8_388_608, // a bigger loader reply crosses by heap up to this; 256 KiB..64 MiB, default 8 MiB
},
})Build first with brust build, then start with bun server.ts. run() still reads brust.toml from the current directory, and an environment variable still wins over the value you pass. A value outside its range stops the start with a message that names it.
Once the server is up, run() does not return; the process lives until a signal drains it.
What only brust.toml sets
[cors] is read when the server starts, from brust.toml only. The config object of run() has no effect on it today.
[actions], [cache] and [build] are read by brust build and recorded in the manifest, so they are decided at build time. A build with no brust.toml has no actions and no shared cache, whatever server.ts says.
Try it
Set the pokedex server's port, worker count and actions; the file and the one-run override follow.
# pokedex/brust.toml
[server]
address = "127.0.0.1"
port = 1337
[workers]
count = 4
[actions]# the same, for one run, without editing the file
$ BRUST_PORT=1337 BRUST_WORKERS=4 bun run startResponse size
Every render slot is a 256 KiB shared buffer, and each worker has renderSlots of them. Memory for them is 256 KiB × render slots × workers: 4 workers with 4 slots each is 4 MiB.
A loader or job response has to fit its slot. The size counted is the JSON the loader or job returns (the page props and the job results), in UTF-8 bytes, not the HTML. One that does not fit fails the request with a plain 500, and the server's log carries the reason.
$ curl -si http://127.0.0.1:8080/big | head -1
HTTP/1.1 500 Internal Server ErrorERROR loader threw route=r1 error=response too large: 300029 > 262144The slot size is not configurable yet. Keep what a loader returns small: return what the page renders, and fetch the rest from an action.
Actions are different. An action body larger than the slot crosses by heap transfer, up to [actions] max_response_bytes (8 MiB by default); a bigger one is a 500 with brust: action response exceeds <max_response_bytes> bytes. Request bodies are capped by max_body_bytes (1 MiB by default).
Environment variables
| Variable | Range | Also in brust.toml |
|---|---|---|
BRUST_ADDR | non-empty | [server] address |
BRUST_PORT | 1 to 65535 | [server] port |
BRUST_WORKERS | 1 to 1024 | [workers] count |
BRUST_RENDER_SLOTS | 1 to 64 (default: cores, at most 16) | no |
BRUST_WORKER_THREADS | 1 to 256 Rust I/O threads (default: one per core) | no |
BRUST_BOOT_TIMEOUT_MS | default 30000 | no |
BRUST_CALL_TIMEOUT_MS | default 30000 | no |
BRUST_DRAIN_TIMEOUT_MS | default 10000 | no |
BRUST_DIST_DIR, BRUST_APP_ENTRY | paths | no (flags --dist-dir, --entry) |
The other sections
[actions] turns actions on; see Actions. [cors] is read from the file only:
[cors]
origins = ["https://pokedex.example"] # required, at least one
methods = ["GET", "POST"]
headers = ["content-type"]
expose_headers = []
credentials = false # true with origin "*" is an error
max_age_seconds = 600[build] server_only names modules that must never reach the browser; reaching one from client code fails the build with server-only-in-client.
[build]
server_only = ["lib/db", "bun:sqlite"] # import prefixes or app-relative paths kept out of the browser[cache] configures a shared cache adapter (see Cache adapters), and [cloudflare] the Workers target (see Workers target).
[cache]
adapter = "redis" # redis | dragonfly | cloudflare-kv | cloudflare-do | custom
[cache.redis]
url = "env:REDIS_URL" # always an env reference, never a literal URLbrust start has no TLS option. Put a proxy or a load balancer in front of it for HTTPS.