bounda.config.ts exports defineConfig({...}) from @bounda-dev/core/config. boot() reads it,
and createApp takes the same object; createTestApp does not read it. The configuration is
checked when the app is created: an unknown key, a value of the wrong type or a duration it cannot
read throws a ConfigurationError that lists every problem with its path.
import { defineConfig } from "@bounda-dev/core/config";import { postgresql } from "@bounda-dev/postgresql";
export default defineConfig({ storage: postgresql({ url: process.env.DATABASE_URL! }), ports: { order: { notifier: "smtp" } }, runtime: { role: "worker", policies: { timeout: "1m" } },});Durations are a number of milliseconds or a string: a number followed by ms, s, m, h
or d ("250ms", "1.5s", "7d"). A duration read from the environment is a string, which the
types refuse; wrap it with asDuration(process.env.X ?? "1m"), which checks it.
Top level
Section titled “Top level”storage, required. The adapter that holds the store:sqlite({...}),postgresql({...})orcloudflare(), whosebindingnames the Durable Object binding the Worker reaches it through,"STORE"by default. See adapters.readModels, default{}. A different adapter for some read models, by read model name:{ orderSummary: postgresql({ url: READ_URL }) }. The rest stay instorage.ports, default{}. The implementation each port uses, by module and port:{ order: { notifier: "smtp" } }. A port with one implementation may be left out; one with several must be named. The names are typed from the project. See ports.runtime. How the app runs.
runtime
Section titled “runtime”role, default"all". Whatapp.start()runs in this process:"all"and"worker"run the dispatcher and the scheduled-command worker,"web"runs neither. See roles.
Commands
Section titled “Commands”commands.timeout, default"30s". How long one run of a command handler may take. Past it the dispatch rejects withHANDLER_TIMEOUT, the handler’ssignalaborts and nothing it returns is stored. Loading the aggregate and storing its events do not count.commands.concurrencyRetries, default3. How many times a command runs again when its append finds the stream moved, before aConcurrencyErrorreaches the caller. Each run gets its owntimeout.
Policies
Section titled “Policies”policies.timeout, default"30s". How long one run of a policy handler may take. Past it the run is abandoned: its commands still running stop, later ones are refused withREACTION_ABANDONED, and itssignalaborts.policies.retry, default exponential, 3 attempts, 1 s to 30 s. How a failed policy run, or a failed command scheduled withdelay, is retried. Underoverrides, a policy run takes the setting of the aggregate the policy belongs to, and a scheduled command that of the aggregate it is for. See retry.policies.maxChainDepth, default25. How many hops a chain of reactions may take from the command that started it before a command is refused withCHAIN_DEPTH_EXCEEDED. Reaching it usually means two policies answer each other.
Processes
Section titled “Processes”processes.timeout, default"7d". How long a process stays open beforeat-timeout.tsruns, for a process whoseconfigdeclares notimeout.processes.handlerTimeout, default"30s". How long one run of a process handler, for an event or a deadline, may take. Past it the run is abandoned, as a policy’s is.processes.retry, default the same aspolicies.retry’s, not whatpolicies.retryis set to. How a failed process step or deadline is retried.
Each time limit also decides how long a claim lasts, so how soon another instance runs again what a stopped one left: see delivery guarantees.
Dispatcher
Section titled “Dispatcher”dispatcher.pollInterval, default"100ms". How long the dispatcher waits between passes when it polls, and the pace of the scheduled-command worker.dispatcher.idleInterval, default"30s". With an adapter that pushes notifications (PostgreSQL), how long an idle dispatcher waits for one before polling anyway. Ignored without notifications.dispatcher.batchSize, default100. Events read per subscriber per pass.dispatcher.projectionBatchTime, default"250ms". How long a projection batch keeps its transaction open before it commits what it has and leaves the rest for the next one. Rebuilds honour it.dispatcher.backoff.baseDelay,.maxDelay, default"1s","30s". How long background passes leave a subscriber alone after a failed batch:baseDelayafter the first failure, doubling up tomaxDelay. A batch that goes through resets it. See a projection that keeps failing.dispatcher.catchUp.timeout,.pollInterval, default"2s","15ms". How longcatchUpReadModels({ through }), and read-your-writes with it, waits for the read models a command changed, and how often it looks while another process holds them. Pasttimeoutit logs a warning and the request reads what is there.
Per aggregate
Section titled “Per aggregate”overrides, default{}. Settings for one aggregate, by aggregate name:commands.timeout, andpoliciesandprocesseswith the keys above. What an override leaves out comes from the app’s settings.
A process can set its own lifetime in its config,
({ events }) => ({ startedBy: [...], timeout: "48h" }), which wins over processes.timeout and
over an override.
policies.retry and processes.retry take the same shape. strategy is required whenever
retry is given; the other keys fall back to the defaults.
| Key | Default | What it does |
|---|---|---|
strategy |
"exponential" |
"none" gives up on the first failure; "fixed" waits baseDelay each time; "linear" waits baseDelay times the retry number; "exponential" doubles baseDelay with each retry. |
maxAttempts |
3 |
Runs in all, the first one included, before the failure becomes a dead letter. |
baseDelay |
"1s" |
The first wait. |
maxDelay |
"30s" |
No wait is longer than this. |
Only failures that may pass are retried. A rejection is an answer, not a failure; a payload that
does not validate, a missing module, a broken configuration, a chain that went too deep or events
that do not fit the aggregate (VALIDATION_FAILED, NOT_FOUND, INVALID_CONFIGURATION,
CHAIN_DEPTH_EXCEEDED, CREATION_ORDER) become a dead letter at once, since they would fail the
same way again. See errors.
runtime: { policies: { retry: { strategy: "exponential", maxAttempts: 5, maxDelay: "2m" } }, overrides: { // a policy whose command can never succeed on a second try; the commands scheduled for // `order` are not retried either order: { policies: { retry: { strategy: "none" } } }, },},Outside bounda.config.ts
Section titled “Outside bounda.config.ts”- React Router: the
bounda()Vite plugin takesconsistencyanddebounceMs; see its options. - Cloudflare:
createBoundaObjecttakespassesPerAlarm,createWorkertakestenantOfandconsistency, andconnecttakesconsistency; see the Cloudflare adapter. With React Router,app/tenant.tsnames the tenant; see on Cloudflare. - Tests:
createTestApptakes its ownportsandconfig; see testing.