Skip to content

    Reference01

    CLI

    The bounda command: generate the registry and the types, rebuild a read model, deal with dead letters, read the runtime's status, migrate its tables and declare a restore.

    @bounda-dev/cli installs the bounda command.

    Terminal windowShell
    pnpm add -D @bounda-dev/cli typescript

    TypeScript 7 or newer is an optional peer: the generator needs it only to infer the state of aggregates without state.ts.

    Every command prints its result on standard output and everything else on standard error: the app’s log lines, through Bounda’s logger or straight to the console, the access log records, warnings, progress and errors. Standard output carries nothing but the result, so bounda dead-letters list --json and bounda forgotten pipe into another program as they are:

    Terminal windowShell
    bounda dead-letters list --json | jq '.[].id'

    The app’s logger writes one line each, [bounda] <level> <message> <fields as JSON>, at info and above.

    Standard error often ends up in a log, and the message of an error a handler throws may quote personal data it read. So a command that fails on such an error prints its class and code, not its message, and the app’s log lines carry no error’s text, as runtime.telemetry.errorText says by default:

    error: ValidationError (VALIDATION_FAILED); its message may quote personal data: run again with --error-text to print it

    --error-text, on every command that loads the project, prints the message and lets the log lines carry the text of errors for that run; BOUNDA_ERROR_TEXT=include in the environment or in .env, or a configuration that sets errorText: "include", does the same without it, and BOUNDA_ERROR_TEXT=omit keeps the text out whatever the configuration says. What fails before any handler runs is printed in full: a configuration or a registry that cannot be loaded, a read model that is not in the registry, a list forget cannot read, and errors whose message Bounda writes from names and ids alone, such as a dead letter that is not found or was already settled. To read what a reaction failed on, read its letter: bounda dead-letters list opens the text it keeps encrypted, an access the access log records. What a subscriber, a projection say, failed on last is in bounda status --error-text, recorded the same way.

    --actor <id> names who runs a command that acts or reads personal data: rebuild, dead-letters list, retry and discard, status, export and forget. The command acts as that id, as client.as(actor) does in code: the events it writes carry it in their metadata, its log lines name it, and so does every access log record it causes.

    Terminal windowShell
    bounda dead-letters retry 019a0c4e-3c9e-7a1b-9f1e-2b3c4d5e6f70 --actor ops-ana
    bounda forget --from forgotten.jsonl --actor ops-ana

    Give an id, such as the operator’s account, never a name or an email: events keep it in the clear and are never deleted. Without --actor the command names nobody, as before. An id that is not 1 to 255 characters is refused with exit code 1. bounda forgotten takes none: it prints ids the stream holds in the clear and records nothing, and bounda generate loads no app.

    Reads the project layout and writes .bounda/registry.ts, .bounda/register.d.ts, .bounda/types.ts and one +types/<name>.ts next to every module. The registry carries, for every aggregate, a stateFingerprint of the modules that shape its state and of the type inferred for it, which its snapshots are valid for. Files whose content did not change are left alone; +types files whose module is gone are removed, and so are the folders their removal empties; a folder holding nothing but +types (and Finder’s .DS_Store) is not taken for a module.

    Terminal windowShell
    bounda generate
    bounda generate --watch
    bounda generate --root ./apps/shop --app-dir src --no-infer
    Option Default Meaning
    --root <dir> current directory The project root: where .bounda/ goes and tsconfig.json is looked for
    --app-dir <dir> app The application directory under the root, with domain/ and read/
    --tsconfig <file> <root>/tsconfig.json The TypeScript project used to infer state
    --no-infer Do not start TypeScript; aggregates without state.ts get UnknownState
    --watch Regenerate after each burst of changes under the application directory

    Output lists every file written or removed, then a summary:

    written .bounda/registry.ts
    written app/domain/order/+types/order-placed.ts
    1 aggregate, 1 read model, 10 files (2 written, 8 unchanged, 0 removed)

    Warnings go to stderr and do not change the exit code. They come from a layout that is most likely wrong without breaking a convention (see what the generator warns about) and from state inference:

    warning: order: field "cancellation" (set by orderCancelled) has a type that is not visible
    from .bounda/types.ts (Cannot find name 'Cancellation'.); it is typed as unknown. Export the type
    or add state.ts
    Code Meaning
    0 Generated, possibly with warnings
    1 The layout breaks a convention. Every problem is listed with its path; nothing is written
    2 Something else failed, such as a file where .bounda/ should be

    --watch regenerates after each burst of changes, 100 ms after the last one, and ignores changes to +types and .bounda directories. It makes its first run only once it is listening, so a module saved while that run is going is regenerated right after it, and prints watching app/ for changes once that run has succeeded, on standard error. To know it is listening, it writes a file named .bounda-watch-<uuid> into the application directory until it sees the change come back, then removes it: the file system can start listening a moment after it is asked to, and miss what changes before. When it has not seen it after 20 tries, a second, it prints a warning that watching may miss changes, removes the file and goes on. A run that fails prints its problems and watching goes on; a first run that fails outright, such as a file where .bounda/ should be, ends it. Ctrl-C ends it too.

    For an aggregate without state.ts, the generator writes a first pass in which the state is UnknownState, opens the project with TypeScript, reads the return type of every event’s begin and evolve and unions the fields it finds. The fields every begin always sets are required once the aggregate exists, and a command handler sees that state or the one of an aggregate that does not exist yet, every field undefined:

    TypeScript
    export type OrderCreatedState = {
    readonly customerId: string;
    readonly lines: readonly import("../app/domain/order/order-placed.ts").Line[];
    readonly paidWith?: "card" | "transfer";
    readonly status: "cancelled" | "paid" | "placed";
    };
    export type OrderState = core.NotCreated<OrderCreatedState> | OrderCreatedState;

    evolve gets OrderCreatedState. Without any begin, there is only OrderState, every field optional, since any event could come first.

    A field an evolve computes from the state, such as reminders: state.reminders + 1, has no type in that first pass. The generator reads every evolve again against the state it has inferred so far, until nothing changes, so the field takes the type the other events give it. One that only ever comes from itself, with no begin or other event to give it a type, becomes unknown, and a warning names it: set it in a begin, or add state.ts. So does a chain of fields computed one from another that has not settled after a few passes. A field that is any on purpose, set from JSON.parse or a z.any() payload, stays as it is.

    Types exported from your modules are referenced through import(...). A type that is not exported cannot be named from .bounda/types.ts; that field becomes unknown and a warning says which field and which events set it. Export the type, or add state.ts, to fix it. Without TypeScript installed, or when it cannot open the project, the state stays UnknownState and the warning says so.

    Rebuilds one read model from the whole global stream without taking it offline. It loads bounda.config.ts and the generated registry the way boot() does, so it runs from the project root with the same environment as the app.

    Terminal windowShell
    bounda rebuild orderSummary
    bounda rebuild orderSummary --root ./apps/shop --config bounda.config.ts --registry .bounda/registry.ts
    Option Default Meaning
    --root <dir> current directory The project root, where the configuration and .bounda/ are
    --config <file> bounda.config.ts The configuration module under the root
    --registry <file> .bounda/registry.ts The generated registry module under the root
    --actor <id> Who runs it, named in its log lines; see who runs a command
    --error-text Print the message of the error it fails on, and log the text of errors; see error text

    The projections run into a fresh table that takes the live table’s place once it has caught up; an interrupted rebuild resumes where it stopped, and a second one started meanwhile takes over. Rebuilding a read model says how it works and when to run it in a deploy.

    rebuilt read model "orderSummary": 48213 events, checkpoint at position 48213

    Use it after fixing a projection, and when a view loses a field or changes a field’s type, which the app refuses to do on start. Exit codes: 0 rebuilt, 1 bad arguments, 2 the project could not be loaded, the read model is not in the registry, or a projection failed.

    Lists, retries or discards the handler runs that gave up. It boots the project the way boot() does, without starting the background work, so it runs from the project root with the app’s environment.

    Terminal windowShell
    bounda dead-letters list
    bounda dead-letters list --kind policy --handler order.notifyOnOrderPlaced
    bounda dead-letters list --status retried --limit 20 --json
    bounda dead-letters retry <id>
    bounda dead-letters discard <id>
    Option Applies to Meaning
    --kind <kind> list policy, process or scheduled
    --status <status> list failed (default), retried or discarded
    --handler <name> list The policy or process name as the letter records it, e.g. order.notifyOnOrderPlaced, or the scheduled command’s type, e.g. PlaceOrder
    --limit <n> list At most this many letters: a whole number, 0 or more
    --json list Print the letters as JSON
    --root, --config, --registry, --error-text all As for bounda rebuild
    --actor <id> all Who lists, retries or discards: named in the access log record each one writes, in the events a retry writes and in the log line of a retry or discard
    019a0c4e-3c9e-7a1b-9f1e-2b3c4d5e6f70 failed policy order.notifyOnOrderPlaced
    OrderPlaced on order:o-1, 1 attempt, last 2026-09-22T14:03:11.402Z (terminal)
    ValidationError (VALIDATION_FAILED): mail server rejects it
    019a0c51-8d2f-7c4e-a1b2-3c4d5e6f7a80 failed process order.orderPayment
    OrderPaid on order:o-2, 3 attempts, last 2026-09-22T14:05:40.118Z (retriable_exhausted)
    Error: payment provider timed out
    2 events are parked behind it; retrying it handles them in order
    2 dead letters

    A letter retried or discarded shows when after its status (retried 2026-09-23T08:00:00.000Z). The third line of each letter is the error’s class, its code when it is one of Bounda’s, and its message. The message is kept encrypted with the key of the person the work names, so it is left out once that person was forgotten; see what a letter holds.

    When retry fails, because the handler fails again or the letter cannot be retried or was settled first, the error is printed, as error text says, and the exit code is 2. For a process letter it also says when one of the parked events failed the process again. What a retry runs for each kind is in Dead letters.

    Prints where the runtime’s background work stands, as the database says. It boots the project as bounda dead-letters does.

    Terminal windowShell
    bounda status
    bounda status --json
    bounda status --error-text --actor ops-ana
    tables
    storage storage 3 of 3
    storage checkpoints 1 of 1
    forgetLedger forget-ledger 4 of 4
    restores found while running: 0
    subscribers (head at 1204)
    projection:customerDirectory at 1197, 7 behind
    failing: Error on CustomerRegistered at 1198, 4 attempts since 2026-11-09T10:02:11.000Z
    policies at 1204, 0 behind
    personal data backfill: sealed through position 1204, caught up
    forget ledger: reconciled, applied through 2026-11-08T22:15:00.000Z, last run 2026-11-09T10:00:00.000Z
    key rotation: under 7Kx0Qw9z (previous Wn3eT1aa), 12 keys left to rewrap
    retention
    default 2y, 0 persons past it, next due 2028-01-04T09:13:00.000Z
    marketing 18mo, 3 keys past it, next due 2026-11-09T11:40:00.000Z
    invoices 6y, 0 keys past it, next due 2031-02-12T16:20:00.000Z
    dead letter retention: 30d, next purge 2026-11-20T08:00:00.000Z
    • tables: each scope of Bounda’s tables, in each database the configuration names (storage, readModels.<name> for read models in a database of their own, forgetLedger), at the version the database holds of the one this release migrates it to; one behind says (migrations pending: bounda migrate). A store without tables, as memory(), has none.
    • restores found while running: how many times a running instance found the database gone back to an earlier copy (restores), or declared by bounda restored.
    • each subscriber’s checkpoint and lag, and its last failure, by any instance: failing while it is still stuck on it, last failed, resolved … once it got past. Its class and code, the event, how many deliveries in a row failed and since when; never its text.
    • the backfill of personal data, the forget ledger’s reconciliation, the rotation of the master key, the retention and the purge of resolved dead letters, each not configured when it is off. The retention has a line for the app’s, which forgets persons as a whole, under default, and one for each category of app/personal-data.ts, which forgets its keys, with how many are past it now and when the next comes due, and the keys a forgetting kept that no legal obligation keeps any more.

    Under the rule of error text set to "include", by --error-text, BOUNDA_ERROR_TEXT=include or the configuration, it also prints, under each failure, the text it keeps, encrypted under the person the event names: it is read through app.subscriberFailures.list(), which records it in the access log with the persons and --actor. A failure whose person was forgotten says its text went with them.

    Option Meaning
    --json Print app.status() as JSON; with --error-text, subscriberFailures holds the failures with their text
    --root, --config, --registry, --error-text As for bounda rebuild
    --actor <id> Who reads the failures’ text with --error-text, named in the access log

    Says the database was restored from a copy while the app was stopped, for an app without a forget ledger, whose mark tells it on its own. It boots the project as bounda dead-letters does.

    Terminal windowShell
    bounda restored
    restore recorded: 1 found so far; the background jobs start over from the copy, without the holds it brought back

    The restore is counted, so every running instance starts its background jobs over; the holds of failures the copy brought back are cleared; and with a ledger, it is reconciled at once. Run it after bounda forget --from when restoring without a ledger (see restores). Options: --root, --config, --registry, --error-text, as for bounda rebuild. Exit codes: 0 recorded, 2 the project could not be loaded.

    Brings Bounda’s tables to this release’s version in every database the configuration names, as the first instance to start would, and prints the versions after:

    Terminal windowShell
    bounda migrate
    migrated
    storage storage 3 of 3
    storage checkpoints 1 of 1
    readModels.reports checkpoints 1 of 1

    It reads only the configuration, never the registry, and starts no app: run it as a step of the deploy, after stopping the previous version and before starting the new one (see Schema). Each database migrates under its own lock, as a start does, so running it twice, or while an instance starts, migrates once. Tables a newer release migrated are refused untouched with STORAGE_SCHEMA_TOO_NEW.

    Option Meaning
    --root <dir> The project root, the current directory by default
    --config <file> The configuration module under the root, bounda.config.ts by default

    Exit codes: 0 migrated, 1 bad arguments, 2 the configuration could not be loaded or a migration failed.

    Answer a person’s request from the command line. Both boot the project as bounda dead-letters does, and act as --actor.

    Terminal windowShell
    bounda export c-42 --actor ops-ana > c-42.json
    bounda forget c-42 --category marketing --actor ops-ana
    bounda forget c-42 --actor ops-ana

    bounda export <person> prints, as JSON, what app.exportPersonalData answers: every record that names the person, with each field’s category, and every category’s basis and retention (see Exporting a person’s data). It is recorded in the access log like any export.

    bounda forget <person> forgets the person as a whole, as app.forget does, and prints forgot c-42; with --category, repeated for several, only those categories, as when a consent is withdrawn, and prints forgot marketing of c-42. A category whose basis is legal-obligation is kept, restricted, until its retention ends, either way. A category the app does not have fails with VALIDATION_FAILED and nothing is forgotten.

    Apply again, by hand, the forgettings a restored backup undid, for an app without a forget ledger. bounda forget boots the project as bounda dead-letters does; bounda forgotten only opens the project’s storage and reads its event store, without starting the app, so a strict forget ledger that is down does not stop it.

    Terminal windowShell
    bounda forgotten --since 2026-11-01T03:00:00Z > forgotten.jsonl
    bounda forgotten --since 18250 > forgotten.jsonl
    bounda forget --from forgotten.jsonl

    bounda forgotten reads the stream and prints, one line of JSON each, the PersonalDataForgotten of every person forgotten since --since: a date, compared with each event’s time, which reads the whole stream, or a position of the stream to read after. Each line is {"personId":…,"eventId":…,"forgottenAt":…,"reason":…,"fingerprints":[…],"categories":…}, the entry a ledger would hold: categories null for a person forgotten as a whole and else the categories the forgetting asked for, and no fingerprints for a forgetting recorded before Bounda 0.4.0; the count goes to standard error, so standard output is the list alone. Run it against the database before it is restored, or regularly next to the backups.

    bounda forget --from <file> commits again each forgetting listed, of the categories it asked for or of the person as a whole, under its own id, time and reason, unless the database holds it, and only while the person holds a key in force the line covers and names, as a reconciliation would; with a forget ledger configured, it records each in the ledger first. A line may also be a bare person’s id, forgotten as a whole, as a request, whenever they hold a key; blank lines are skipped. It prints each person it forgot and how many of those listed:

    forgot again c-42
    forgot again 1 of 3 listed; the rest hold no key the line names, or this store holds their forgetting
    Option Applies to Meaning
    --since <date|position> forgotten A date such as 2026-11-01 or 2026-11-01T03:00:00Z, or a position: a whole number
    --from <file> forget What bounda forgotten printed, or one person’s id a line; not with a person
    --category <name> forget <person> Forget only this category of the person; repeat it for several
    --root, --config, --registry, --error-text all As for bounda rebuild
    --actor <id> export, forget Who acts, named in each PersonalDataForgotten, its log line and the access log

    Exit codes: 0 done, 1 bad arguments, or a person and --from both or neither, 2 the project could not be loaded, the export or the forget failed, or the file could not be read or has a line that is neither a person’s id nor an entry, in which case nobody is forgotten.

    Gives every entry of a Workers KV forget ledger the expiration keepFor sets: KV expires a key only as it was written, so the keys written before keepFor was set, or while it was longer, keep the expiration they had, none at first. Run it once after setting or shortening keepFor, from the project’s root.

    Terminal windowShell
    bounda forget-ledger expire --binding FORGET_LEDGER --keep-for 35d

    It runs the project’s own wrangler, logged in to the account: it lists every object’s keys under the ledger, writes again with their expiration those that expire later than keepFor after their forgetting or never, and removes those already past it, through wrangler kv bulk. A key that is not an entry of the ledger is never touched. It prints what it changed:

    gave 1204 entries the expiration keepFor sets; removed 37 already past it
    Option Meaning
    --binding <name> The KV namespace’s binding in wrangler.jsonc, as kvForgetLedger({ binding }) names it
    --keep-for <duration> The configuration’s keepFor, such as 35d
    --local The local state of wrangler dev instead of the deployed namespace
    --env <name>, --wrangler-config <path>, --persist-to <dir> As wrangler takes --env, --config and --persist-to
    --root <dir> The project’s root, where wrangler is installed
    --error-text Print the message of the error it fails on, as error text says

    Exit codes: 0 done, 1 bad arguments, 2 wrangler failed, is not installed, or @bounda-dev/cloudflare is not; the failure is printed as error text says.

    bounda generate is exported from @bounda-dev/cli as generate:

    TypeScript
    import { generate } from "@bounda-dev/cli";
    const report = await generate({ root: process.cwd() });
    report.written; // absolute paths written this run
    report.warnings; // layout and inference warnings, per aggregate or read model

    It throws ConventionError when the layout breaks a convention; formatConventionError and formatWarnings format the error and the warnings as the command prints them. bounda rebuild is rebuildReadModel from @bounda-dev/core over a project loaded with loadProject from @bounda-dev/core/node, with actor for --actor; an app exposes the same as app.rebuildReadModel(name), and app.as(actor).rebuildReadModel(name). bounda status is app.status(), and its failures’ text app.as(actor).subscriberFailures.list(); bounda migrate is migrateTables({ config }) from @bounda-dev/core; bounda restored is app.restored(). bounda dead-letters is app.deadLetters on a booted app, or app.as(actor).deadLetters, bounda export is app.as(actor).exportPersonalData(person), bounda forget <person> is app.as(actor).forget(person, { categories }), and bounda forget --from is app.forgetAgain(entries, { actor }). The commands boot the app with createConsoleLogger({ stream: process.stderr }) from @bounda-dev/core/node, which writes every level to the stream it is given.