Command line
Every command of the nexus binary, every flag, and what each one defaults to.
$ nexus --help$ nexus --versionState lives under ~/.nexus/. Each command opens the file it needs and creates the directory if
it is missing:
| Command | Default database |
|---|---|
validate, compile |
none — they never touch storage |
deploy, keys, proto, seed |
~/.nexus/registry.db |
serve |
~/.nexus/registry.db, plus nexus-audit.db, nexus-log.db, nexus-ui.db and queues/ beside it |
audit |
~/.nexus/nexus-audit.db |
dlq |
~/.nexus/queues/ — derived from the registry path, not opened as a database |
state, tenant, cert |
~/.nexus/registry.db |
Every command that reads a database accepts --db <path> to point elsewhere. For dlq, --db
is only used to locate the queue directory: the queues live in a queues/ folder next to the
file you name.
Before anything else runs, nexus reads a .env file from the working directory if one exists,
and stops if that file is refused (see .env);
nexus mcp reads none.
Diagnostics go to stderr, results to stdout, so nexus compile flow.md > artifact.json is safe.
Set RUST_LOG to change the verbosity; the default is info.
nexus validate
Parses a flow file and resolves names. Exits 0 when clean, 1 with the errors on stderr otherwise.
| Flag | Default | Meaning |
|---|---|---|
<path> |
— | The .flow.md file. Positional, required |
--libs <dir> |
none | Directory of shared .flow.md files that this flow may include or call_flow |
$ nexus validate forward-order.flow.mdok forward-order.flow.md flow: forward-order tenant: acme steps: 3validate runs the same checks as compile and deploy, and the three commands print the same
error lines for the same file and libraries. A compile error that names a step of the file is
printed at the line of that step’s header, as <path>:<line>:<column>: error: <message>, the form
editors and CI logs read. <path> is the path as you gave it; the column is that of the header’s
first character, 1 for an unindented header. A step of a ## Fault: section is named
fault:<name>, as in step 'fault:notify': …, and its errors are printed at the line of its
## Fault: header, also when a main step has the same name. An error that names no step of the file, such as a
front-matter error or an error inside a step of a library flow, is printed error: <message>,
without a position. After the error lines comes one closing line, and the exit code is 1:
$ nexus validate forward-order.flow.mdforward-order.flow.md:21:1: error: step 'forward': unknown key `endpont` — did you mean `endpoint`?error: 1 validation error(s)nexus compile
Compiles a flow to its intermediate representation and prints it. Nothing is stored.
| Flag | Default | Meaning |
|---|---|---|
<path> |
— | The .flow.md file. Positional, required |
-o, --output <path> |
stdout | Write the IR to a file instead |
-p, --pretty |
off | Indent the JSON |
--libs <dir> |
none | Directory of shared .flow.md files |
$ nexus compile forward-order.flow.md --pretty | head -20{ "meta": { "name": "forward-order", "tenant": "acme", "version": { "major": 0, "minor": 1 }, "effects": [ "http_egress" ], ...With --output, the hash is printed instead of the document:
$ nexus compile forward-order.flow.md --output forward-order.ir.jsonok forward-order.flow.md → forward-order.ir.json hash: 9f2b7c4d1ae05836b1c0d4f7a83e2916d5b0c7314ea9f6d8b2c5107e43ab9d02nexus deploy
Compiles a flow and publishes it to the registry under a version, then points latest at it.
| Flag | Default | Meaning |
|---|---|---|
<path> |
— | The .flow.md file. Positional, required |
-v, --version <ver> |
— | Version string, required |
--db <path> |
~/.nexus/registry.db |
Registry database |
--libs <dir> |
none | Directory of shared .flow.md files, resolved at compile time |
$ nexus deploy forward-order.flow.md --version 1.2.0deployed forward-order.flow.md flow: forward-order tenant: acme version: 1.2.0 hash: 9f2b7c4d1ae05836b1c0d4f7a83e2916d5b0c7314ea9f6d8b2c5107e43ab9d02 db: /home/you/.nexus/registry.dbThe tenant and the flow name come from the file’s front matter, not from a flag. Deploying a version that already exists is an error — a published version is fixed, so publish a new one.
The tenant must already be registered with nexus tenant create. Deploying into
an unknown tenant fails, naming the command that fixes it; the platform will not create one for
you, because then a typo in tenant: would become a real tenant that nothing distinguishes from a
new customer. A disabled tenant is refused for the same reason, with a different message.
Every flow published is recorded in the tenant’s audit chain
as flow.deployed. When the chain cannot record it, the command refuses and nothing is published.
nexus undeploy
Withdraws a flow from service: its latest pointer and its declared routes are removed, so every
door answers as for a flow that was never deployed. The versions themselves stay in the registry.
See Undeploying a flow for what stops and what
stays.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | The tenant that owns the flow, required |
--flow <f> |
— | The flow to withdraw, required |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus undeploy --tenant acme --flow forward-orderundeployed acme/forward-order version: 1.3.0 routes: 2 removedA line trace: turned off follows when the flow’s payload trace was on.
Three cases are not an act and change nothing. A tenant that is not registered and a flow with no
version registered for it are errors. A flow that has versions but is not deployed prints
acme/forward-order is not deployed and exits successfully. A disabled tenant’s flow can be
undeployed.
The command refuses while another deployed flow of the same tenant names this one in queue: or
timeout_flow:, and names each of them. Undeploy those first:
$ nexus undeploy --tenant acme --flow orderserror: refusing to undeploy acme/orders: acme/intake (queue in step 'hand-off') names itIt also refuses when an artifact of the tenant cannot be read, since that flow cannot be shown not to name this one.
The act is recorded in the tenant’s audit chain
as flow.undeployed. When the chain cannot record it, the command refuses and the flow stays
deployed. --accept-unrecorded is not accepted here; to stop a tenant’s traffic while the chain
cannot be written, use nexus tenant disable --accept-unrecorded.
nexus serve
Starts the HTTP server. Flows must be deployed first.
| Flag | Default | Meaning |
|---|---|---|
--host <h> |
0.0.0.0 |
Bind address |
-p, --port <p> |
9090 |
Bind port |
--db <path> |
~/.nexus/registry.db |
Registry database; the audit, log, UI databases and the queue directory are created beside it |
--grpc-port <p> |
off | Open the gRPC door on a separate port, h2c (plaintext). Without it nothing listens; a flow deployed with inbound: leaves a WARN naming it. 0 asks the system for a port and announces it: NexusFabric gRPC ingress listening on <addr> |
--metrics-port <p> |
off | Serve metrics on a separate port that answers only GET /metrics, with no authentication. 0 asks the system for a port and announces it: NexusFabric metrics listening on <addr>. See Metrics |
--no-auth |
off | Serve without API key authentication. An unregistered or disabled tenant still answers 404, as with the gate closed. Local development only; the server warns on every boot |
--trusted-proxy <ip-or-cidr> |
none | A peer allowed to assert a client certificate in Client-Cert: an IPv4 or IPv6 address or CIDR range, repeatable. A value that does not parse stops the start. Without it every Client-Cert is answered 401, and a tenant that accepts client_cert leaves a WARN naming it. See Client certificates |
--accept-new-state-key |
off | Start over flow state written under another NEXUS_SECRET_NEXUS_STATE_KEY and record the new key in its place. The stored deduplication rows and correlations stop matching, so every redelivered message runs again for one retention window; the start writes state.key_replaced to the audit chain first. Refused on any start that has nothing to accept |
X-Nexus-Signature verification is turned on by NEXUS_WEBHOOK_SECRET, the HMAC-SHA256 key, read
from the environment; nexus config check reports webhook signature verification: enabled or
disabled. --secret <key> is refused: the server exits before it starts, since a command-line value is readable by every user of the host in the process list.
API key authentication is on unless you pass --no-auth: POST .../run, POST .../enqueue,
the declared routes /t/{tenant}/…, POST /connector/events/... and GET /flows/{tenant}/{name}
answer 401 without a valid
Authorization: Bearer <nxk_...>. Issue one with nexus keys create before the
first call. GET /health and GET .../wsdl stay public.
$ nexus serve --port 9090NexusFabric server starting on 0.0.0.0:9090 db: /home/you/.nexus/registry.db ui db: /home/you/.nexus/nexus-ui.db audit db: /home/you/.nexus/nexus-audit.db log db: /home/you/.nexus/nexus-log.db queue dir: /home/you/.nexus/queues webhook signature verification: disabled API key authentication: enabledStarted with --no-auth, the last line reads DISABLED (--no-auth) and a warning is written to the
log — an open server should never be a quiet one.
The same process serves the HTTP API, the management UI at /ui, the cron scheduler
for flows with a schedule: key, and the queue workers. See Server and
configuration.
A flow that declares config: needs its
values in the environment before the server will start. If one is missing, the server refuses to
boot and names the tenant, the flow, the key and the variable:
$ nexus serveerror: acme/submit-reading declares config key `BILLING_URL`, but NEXUS_CONFIG_BILLING_URL is not set. Set the variables and start again, run `nexus config check` to see the whole pictureThe values are captured once, at start-up, so changing configuration means restarting.
nexus config check
The verdict nexus serve reaches at boot on the deployed flows, the registry settings and the
flow-state key, without starting anything; it does not judge what only a start can find out (a
directory, database or port the machine cannot provide), nor the start of the management UI (its
asset files and NEXUS_UI_ADMIN_PASS). It lists what every deployed flow requires — not only what
is missing, because the question an operator asks when preparing an installation is “what does
this platform need from me”.
| Flag | Default | Meaning |
|---|---|---|
--db <path> |
~/.nexus/registry.db |
Registry database |
What it judges, each in the words nexus serve refuses to start with:
- The state directory, the parent of
--db: refused asnexus serverefuses it, before anything else is judged, when any other account of the host can reach it; the remedy ischmod o-rwxon that directory (see The state directory). - The deployed flows: every
config:key the environment does not offer, and every context reference still written in the oldctx.Xform. - The registry settings
descendants_max_extra_bytes,fanout_max_parallel,queue_segment_max_bytes,queue_retention_hoursandoidc_jwks_ttl_secs(see Capacity settings). - The flow-state key
NEXUS_SECRET_NEXUS_STATE_KEY: refused when it is set but is not 64 hex characters, and when it is absent while a deployed flow declaresdedup_key:,correlation_open:orcorrelation_close:. When it is set and well formed,flow_state_retention_hours,flow_lease_ttl_secs,dedup_max_response_bytesandcorrelation_state_max_bytesare judged too, and the key is compared with the one the stored flow state was written under: a different key while deduplication rows or correlations are stored is refused with the sentencenexus servestops with, both counts included. The command writes nothing, so it neither records a key nor replaces one. Without a key the server does not read those four, and neither does this command. - The webhook signature secret
NEXUS_WEBHOOK_SECRET:enabledwhen set,disabledwhen not, refused when it is set but empty or not valid UTF-8. Neither the value nor its length is printed.
$ nexus config checkdescendants copy ceiling: 33554432 bytes (default)split fan-out width: 10 branches (default)queue segment size: 134217728 bytesqueue retention term: noneflow state (dedup + correlation): disabled — no NEXUS_SECRET_NEXUS_STATE_KEY setwebhook signature verification: enabledoidc key-set ttl: 300 seconds (default)Configuration required by deployed flows: acme/submit-reading BILLING_URL (NEXUS_CONFIG_BILLING_URL) — set acme/submit-reading PARTNER_ENDPOINT (NEXUS_CONFIG_PARTNER_ENDPOINT) — MISSINGerror: 1 declared key(s) have no value: PARTNER_ENDPOINT (NEXUS_CONFIG_PARTNER_ENDPOINT). `nexus serve` will refuse to startEach setting is printed with the value the server would use. A setting the server would refuse is
printed instead as ! followed by the server’s sentence, the rest of the report is still printed,
and every refusal found is reported in the same run.
Exits non-zero when anything it judges would stop the boot, so it gates a deployment script the same way it gates a boot. Details worth knowing:
- The value is never printed, only whether it is present. Configuration is not masked in the log — an endpoint is not a secret — but this command answers a question about presence, and printing values would put them into terminal scrollback and CI output. The flow-state key is not printed either, well formed or not: its line carries the four numbers, and a refusal says only what is wrong with the key (its length, or that it is not hexadecimal).
- Run it with the service’s environment. The
config:values and the flow-state key are read from the environment of the command and from.env, as fornexus serve. Run as another user, or from another directory, it judges another environment. - Set-but-empty is reported as
set, empty, distinct fromMISSING. An empty prefix is a legitimate value, so it starts the server; an absent one does not. - An artifact that cannot be read is reported and the exit stays non-zero. The command cannot say what such a flow requires, and a clean exit would claim more than was checked.
- A registry that cannot be listed is a refusal, not an empty answer. If the command cannot read the list of deployed flows at all, it says so and exits non-zero. It used to report that nothing required configuration — the opposite answer, said with confidence, to the one question this command exists to answer.
nexus audit
Prints the most recent entries of the tamper-evident audit trail for one tenant.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Tenant to query, required |
-l, --limit <n> |
20 |
How many of the most recent entries to show |
--db <path> |
~/.nexus/nexus-audit.db |
Audit database |
--verify |
off | Also check the hash chain and fail if it is broken |
$ nexus audit --tenant acme --limit 3 --verifyseq event_type created_at correlation_id------------------------------------------------------------------------------------------1187 queue.message.processed 2026-08-06T09:14:22Z 7f3a1c02-9b6e-4f11-8a2d-0d55c3b41e901186 queue.message.failed 2026-08-06T09:14:19Z 7f3a1c02-9b6e-4f11-8a2d-0d55c3b41e901185 queue.message.enqueued 2026-08-06T09:14:18Z 7f3a1c02-9b6e-4f11-8a2d-0d55c3b41e90
chain integrity: OK (1187 entries checked)With --verify, a broken chain prints the offending entries and exits 1, which makes it usable
as a scheduled check.
The list shows the entry’s format_version in a fmt column, and the verdict always says whether
anything vouches for the tail of the chain (tail NOT anchored when nothing does). Three
subcommands take the chain out of the installation and hold it to what was recorded elsewhere. The
format of the file and the verdicts are on the audit trail.
nexus audit export
Writes one tenant’s chain as JSON Lines: a header, the entries as stored, and a checkpoint trailer.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Tenant to export, required |
--from-seq <n> |
0 |
First sequence |
--to-seq <n> |
last entry | Last sequence |
--out <file> |
standard output | Destination; - is standard output |
--db <path> |
~/.nexus/nexus-audit.db |
Audit database; never created by this command |
nexus audit verify
Verifies a chain from an export file, with no database, or from the database.
| Flag | Default | Meaning |
|---|---|---|
--file <path> |
— | Export file to verify. One of --file and --tenant is required |
--tenant <t> |
— | Verify this tenant’s chain in the database instead |
--db <path> |
~/.nexus/nexus-audit.db |
Audit database, with --tenant |
--anchors <path> |
none | Checkpoints to hold the chain to: L1 nexus-log-*.jsonl files or export trailers. Read strictly |
--continues-from <path> |
none | With --file, a range export: the previous export it must continue |
--prev-tail <seq>:<hash> |
none | With --file, a range export: the entry before it |
--require-anchor |
off | Fail when no checkpoint vouches for the tail |
For a range export, pass --anchors only up to the range’s last entry: an anchor beyond it is
reported as tail truncated. An intent without an outcome is reported only within the exported
range. The anchors file must come from outside the installation; see
anchoring the tail.
Exit 1 when the chain does not verify, when the tail was truncated, when a checkpoint does not match
the chain, or when --require-anchor is given and the tail is not anchored.
nexus audit checkpoint
Takes a checkpoint of a tenant’s chain now: prints it and sends it to the structured log as an
audit.checkpoint event.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Tenant, required |
--db <path> |
~/.nexus/nexus-audit.db |
Audit database; the log is written beside it |
nexus dlq
Manages the dead-letter queue: messages that used up every retry. All three subcommands read the
queue directory derived from --db.
nexus dlq list
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
all tenants | Show only this tenant |
--flow <f> |
all flows | Show only this flow |
--db <path> |
~/.nexus/registry.db |
Used to locate queues/ |
$ nexus dlq list --tenant acmeDLQ: 1 pending message(s)
id=17 tenant=acme flow=order-processor enqueued_at: 2026-08-06T09:14:18Z failed_at: 2026-08-06T09:15:04Z attempts: 3 reason: failed correlation_id: "c-2f81" last_error: effect failed at step 'forward': connect timed outcorrelation_id is the ID the message had when it was parked, with control characters escaped; it is
"" for an entry written before the platform stored it, and replay keeps it. reason is failed, missing_flow, integrity or expired; see
What reaches the dead-letter queue.
With no filters it scans every tenant and flow and prints DLQ is empty. when there is nothing.
A DLQ that cannot be read is not skipped. The command lists what it could read, prints one line per
unreadable DLQ to stderr, starting with ! and naming the tenant and flow, the file, the byte offset
and the kind of fault, and exits non-zero. It never prints DLQ is empty. in that case. A newer
version’s entry, for instance an expired entry read by a version that does not know the reason, is
reported this way.
nexus dlq replay
Copies the message back into the main queue and marks the dead-letter entry handled. The flow
runs again from the beginning, with a fresh attempt counter, against whatever version is
currently latest. On a flow with dedup_key: the copy is claimed like any delivery while the
key is remembered: it is acknowledged without running if a delivery of the same key has finished
without failing since, or if the key’s last execution failed and that version sets
dedup_on_failure: absorb; when that version keeps the default retry, a failure lets it run
again (Deduplication).
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--flow <f> |
— | Required |
--id <n> |
— | Message ID from dlq list, required |
--db <path> |
~/.nexus/registry.db |
Used to locate queues/ |
$ nexus dlq replay --tenant acme --flow order-processor --id 17replayed id=17 tenant=acme flow=order-processornexus dlq discard
Marks the entry handled without re-enqueuing it. The entry leaves dlq list immediately; its body
leaves the disk at the flow’s next queue drain, when the worker rewrites dlq.log without the
marked rows. There is no command to bring it back.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--flow <f> |
— | Required |
--id <n> |
— | Message ID from dlq list, required |
--db <path> |
~/.nexus/registry.db |
Used to locate queues/ |
$ nexus dlq discard --tenant acme --flow order-processor --id 17discarded id=17 tenant=acme flow=order-processor the body leaves the disk at this flow's next queue drain; until then it is only hidden from listingsnexus state
Shows what the platform is still holding on behalf of a message, and — when a process died holding a lease — the one way out of it. Both belong to flow state: the deduplication table and the open correlations.
nexus state list
Two sections, one command. An operator asking “what is this installation still carrying?” has one question, not two.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
all tenants | Show only this tenant |
--flow <f> |
all flows | Show only this flow |
--status <s> |
in_flight for deduplication, open + timed_out for correlations |
Deduplication: in_flight, done, failed. Correlations: open, timed_out, closed, closed_late. Or all, for both tables unfiltered |
--limit <n> |
50 | Maximum rows printed |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus state list --tenant acme== deduplication (flow_inbox) ==
nothing in flow_inbox for this scope
== correlations (flow_correlations) ==acme/submit-claim (step ask-assessor) status: timed_out opened: 2026-09-09T15:03:53Z deadline: 2026-09-09T15:18:53Z on timeout: escalate-claim corr-id: bfcc484b-761b-43c8-8fd2-a56e5e6b0f69 state: present key-hash: 6b09b1aa127867866185426a05f22592f65f5863f8f66a7c10e1fa3ef6fbbc0b
acme/submit-claim (step ask-assessor) status: open opened: 2026-09-09T15:31:10Z deadline: 2026-09-09T15:46:10Z past due (not yet reconciled) on timeout: escalate-claim corr-id: f235209b-838a-4aa1-b89a-b456d56ab8e1 state: present key-hash: 47944a0b00571c60a949dfbbd144431f98519b8dd2ead062c1a55a1566e38d6b
in this scope — open: 1, timed_out: 2The first line of each block is the flow, not the fingerprint: key-hash is the handle
nexus state release takes, not what you scan the page for, so it comes
last. deadline carries a verdict only when it adds one — past due (not yet reconciled) means
the once-a-minute reconcile pass has not reached the row yet, which is not timed_out.
state is present or none: the row says whether a correlation_state was declared, never
what it holds.
A deduplication row that has finished carries a response: line. It shows the recorded status and,
for a kept body, its size and content type, never its bytes:
response: 200 body 29 bytes application/json. When no body was stored, the line names the
reason, response: <status> body not stored (<reason>):
not_requested: the flow does not declarededup_memoize_response: true, or there was nothing to offer (the run ended in an error the platform answered for, a gRPC refusal, or a delivery from the queue or a schedule, which have no caller waiting). A duplicate gets the recorded status and no body; on SOAP, a recorded error status answers a fault.over_limit: the answer was larger thandedup_max_response_bytes. A duplicate is refused rather than handed a truncated body: 409 with an explicit message on JSON, aServer(SOAP 1.1) orReceiver(SOAP 1.2) fault on 500 on SOAP.empty: memoisation was requested and the answer had no body (a flow whose output isnull). A duplicate gets what the first caller read: the recorded status, no body, noContent-Type; the same answer asnot_requested, SOAP fault included.
--status is checked against the union of the two vocabularies, which have no value in common,
so one flag filters both sections without ambiguity.
This command needs no NEXUS_SECRET_NEXUS_STATE_KEY, deliberately: a server whose state key is
missing refuses to start, and that is exactly the moment you most need to see the table. It also
means the business keys are not shown — the table stores fingerprints, and without the key
nothing here could reverse one.
dedup_key state diagram
The three values --status accepts on the deduplication side are also the row’s life cycle:
A row only ever holds one of these three. What a duplicate delivery of the same key gets while a row is in each of them:
in_flight— held back, not run, while the lease is held.done— replayed, not run. With or without the recorded body, depending ondedup_memoize_response:.failed— replayed underdedup_on_failure: absorb; runs again, as a fresh attempt, under the defaultretry.
in_flight is the only state nexus state release can act on — done and failed are already
holding an answer, so releasing them has nothing to do. And it only has anything to do while the
lease is still held: if the lease has already expired, the next delivery reclaims the row on its
own, the same way it would after release, without needing the command at all.
nexus state release
Clears the lease on one row so the next delivery of that key may run.
This authorises a second execution of the message, effects included. The next delivery of the
key runs it from its first step. If the process that holds the lease is still alive, it stops
before its next step and its caller gets an error, but the step it is running goes to its end: nothing
cancels a step, and the platform cannot tell you whether the process is alive. Hence the flag:
without --accept-double-execution the command refuses and prints the row it would have
released.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--flow <f> |
— | Required |
--key-hash <h> |
— | The fingerprint, as state list prints it. Mutually exclusive with --key |
--key <k> |
— | The business key as the flow author wrote it. Needs the state key in the environment |
--accept-double-execution |
off | Required to actually release |
--db <path> |
~/.nexus/registry.db |
Registry database |
Prefer --key-hash when the key is personal data: a command-line argument is visible in ps and
lands in shell history, which is the exposure the fingerprinting exists to close.
nexus tenant
Registers and manages tenants. A tenant is a real row, not just a path segment: nexus deploy and
nexus keys create both refuse one that was never registered, and api_keys.tenant_id carries a
foreign key to it.
nexus tenant create
| Flag | Default | Meaning |
|---|---|---|
--id <t> |
— | The identifier that appears in route paths. Required |
--display-name <n> |
— | Human-readable name. Required |
--auth-methods <list> |
api_key |
Ordered, comma-separated. See below |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus tenant create --id acme --display-name "Acme Ltd"registered tenant acme display name: Acme Ltd auth methods: api_key
Next: nexus keys create --tenant acme --label <label>--auth-methods is the ordered list of ways a caller may authenticate as this tenant, and the
first method that produces a result decides — there is no retry after a failure. Three values are
legal: api_key, oidc and client_cert. mtls and anything else are refused at registration rather than
stored for later: a tenant carrying a method nobody implements is a tenant nobody can enter, and
the failure would surface as a 401 that no configuration explains. The list is fixed at
registration — this version has no command to change it.
$ nexus tenant create --id acme --display-name "Acme Ltd" --auth-methods api_key,oidcThe output ends with one Next: line per method, in the tenant’s order: nexus keys create for
api_key, nexus tenant oidc set for oidc. A tenant with oidc admits no token
until its issuer is declared with nexus tenant oidc set. How a request
picks its method is in
Authentication.
nexus tenant list
$ nexus tenant listid display name status auth methods created_at----------------------------------------------------------------------------------------------------acme Acme Ltd active api_key 2026-08-13T09:14:02Zglobex Globex disabled api_key 2026-07-02T11:40:55Znexus tenant disable
Marks a tenant as disabled. It does not delete anything — the row, its keys and its artifacts stay, and the listing keeps showing it, marked. An operator has to be able to see what they turned off.
| Flag | Default | Meaning |
|---|---|---|
--id <id> |
— | Tenant id, required |
--accept-unrecorded |
off | Perform the act even when the audit chain cannot record it. See the audit trail |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus tenant disable --id globexdisabled tenant globex its API keys stop working immediately; nothing was deleted.Callers of a disabled tenant get 404, byte-for-byte the same answer as a flow that does not exist — a disabled tenant and one that never existed must be indistinguishable from outside.
nexus tenant enable
Puts a disabled tenant back into service.
$ nexus tenant enable --id globexenabled tenant globex its existing API keys authenticate again; none were reissued.Read that second line carefully. disable never touched the keys, so re-enabling makes every
key of the tenant authenticate again, at once, including any you had forgotten about. If disabling
was a response to a leaked credential, revoke the key before you enable the tenant — re-enabling is
not a review.
Both acts are written to the tenant’s audit chain (tenant.disabled, tenant.enabled), which is the
only place either is legible: from outside, a tenant going down and coming back looks like nothing
at all. Running the command on a tenant that is already active succeeds and records nothing — there
was no act to record.
nexus tenant oidc set
Declares the OIDC issuer whose tokens a tenant accepts. The tenant must have been registered with
oidc among its --auth-methods; otherwise set is refused. Until set runs, such a tenant admits
no token.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | The tenant, required. It must have oidc among its methods |
--issuer <url> |
— | Required. Compared exactly with the token’s iss — a trailing slash is a different issuer |
--audience <s> |
— | Required. The value the token’s aud must contain |
--jwks-uri <url> |
— | Required. Where the issuer publishes its signing keys (JWKS). Never discovered from the issuer; a redirect from it is not followed |
--claim <name> |
— | Required. The one claim of the token read for rights: roles, groups, scope, or any other name |
--map <file> |
— | Required. JSON file mapping claim values to platform scopes, see below |
--rate-limit <rps> |
— | Requests per second per subject (per sub); burst is twice that. 0 is refused |
--unlimited |
— | The issuer’s subjects are not rate-limited. Exactly one of --rate-limit / --unlimited is required; neither or both is refused |
--flows <list> |
all flows | Comma-separated flow names the issuer’s subjects may call |
--tls-ca-cert <path> |
default roots | PEM file of the CA certificate(s) the --jwks-uri server’s chain must end at; when set, the only CA trusted for the key fetch. Read again at each key fetch, so a replaced file needs no restart. See below |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus tenant oidc set --tenant acme \ --issuer https://login.example.com/realms/acme \ --audience nexus-acme \ --jwks-uri https://login.example.com/realms/acme/protocol/openid-connect/certs \ --claim roles --map acme-roles.json --rate-limit 20declared OIDC issuer of tenant acme issuer: https://login.example.com/realms/acme audience: nexus-acme jwks uri: https://login.example.com/realms/acme/protocol/openid-connect/certs jwks ca: default roots claim: roles (2 value(s) mapped, 0 default scope(s)) rate limit: 20 req/s per subject flows: all flows of the tenantWith --unlimited the line reads rate limit: unlimited; with --flows a,b it reads
flows: a, b; with --tls-ca-cert the jwks ca: line shows the path as given.
An issuer behind a private CA. --tls-ca-cert names the CA that signed the --jwks-uri
server’s certificate. The file is checked before anything is written: it must hold at least one PEM
CERTIFICATE block and nothing else, and --jwks-uri must start with https://. A file that cannot
be read, holds no certificate or holds anything else (a private key, for one) is refused, and the
declaration is left as it was. An expired CA certificate is accepted with a warning that names its
notAfter, so a declaration can be made during a renewal. The path is stored as given and the file
is read again at each key fetch: replace it and the next fetch uses it. A fetch that fails on the CA
answers 503, as any failed key fetch does, and the server log names the tenant, the issuer and the
cause. Running set again without --tls-ca-cert clears it, like every other field: set states
the whole declaration.
The mapping file, acme-roles.json above:
{"map": {"meter-reader": ["run:meters"], "auditor": ["run:reports"]}, "default_scopes": []}map gives the scopes each claim value grants; a value absent from it grants nothing.
default_scopes go to every valid subject of the issuer, and are usually empty. The file may also
contain "claim", which must then equal --claim. Unknown keys are refused — a typo like
default_scope is an error — and so is a scope that is empty or has spaces around it.
One issuer per tenant. Running set again with the same --issuer updates the declaration, and
the first word of the output becomes updated; a different issuer is refused, not substituted. An
issuer and audience already declared by another tenant are refused, and the refusal names that
tenant. A change applies to the next request, without a restart. Each set writes a
tenant.oidc_configured entry to the tenant’s audit chain.
There is no list, show or unset in this version. What a token must pass, how the issuer’s keys
are cached and what an issuer outage does are in
Authentication.
nexus tenant client-cert
Registers, lists and revokes the client-certificate subjects a tenant admits. A subject is the pair of the certificate’s issuer DN and subject DN, in the RFC 4514 string form; how to take them from a certificate is in Registering a subject. What a request with a certificate gets is in Client certificates.
add
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | The tenant, required. It must be registered |
--issuer-dn <dn> |
— | Required. The issuer DN of the certificate |
--subject-dn <dn> |
— | Required. The subject DN of the certificate |
--flows <list> |
all flows | Comma-separated flow names the subject may call |
--scopes <list> |
* |
Comma-separated scopes of the subject |
--rate-limit <rps> |
unlimited | Requests per second; burst is twice that. 0 is refused |
--expires-at <rfc3339> |
never | When the registration stops admitting. A time in the past is refused |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus tenant client-cert add --tenant acme \ --issuer-dn "CN=Acme Issuing CA,O=Acme SRL,C=RO" \ --subject-dn "cn=orders-gateway, o=Acme SRL, c=RO" --scopes run --rate-limit 20registered client certificate subject id=1 tenant: acme issuer dn: CN=Acme Issuing CA,O=Acme SRL,C=RO subject dn: CN=orders-gateway,O=Acme SRL,C=RO scopes: run flows: all flows of the tenant rate limit: 20 req/s (burst 40)The DNs are printed in the canonical form stored. A DN that does not parse, an unknown tenant, and a
pair that already has a registration not revoked, expired included, are refused. Each add writes a
client_cert.registered entry to the tenant’s audit chain.
list
nexus tenant client-cert list --tenant <t> prints one tab-separated row per registration, with the
columns id, state (active, revoked or expired), issuer dn, subject dn, flows (* for
all), scopes, rate and expires.
revoke
nexus tenant client-cert revoke --tenant <t> --id <n> withdraws a registration and writes a
client_cert.revoked entry to the tenant’s audit chain; it prints
revoked client certificate subject id=<n>. An id that is unknown or already revoked is refused.
It does not revoke the certificate: the proxy refuses that through its CRL or OCSP.
nexus keys
Creates and manages API keys for a tenant. The tenant must exist first
(nexus tenant create).
nexus keys create
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Tenant the key belongs to, required |
--label <l> |
— | Human-readable label, required |
--scopes <list> |
* |
Comma-separated scopes; * grants all |
--expires-at <ts> |
never | 2027-01-01 (the whole of that day, UTC) or 2027-01-01T00:00:00Z (an instant). A timezone offset is refused — convert to UTC first |
--rate-limit <rps> |
unlimited | Sustained requests per second, at least 1 (0 is refused); burst is twice that |
--flows <list> |
all flows | Comma-separated flow names this key may call |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus keys create --tenant acme --label 'CI pipeline key' \ --scopes 'run:orders,run:invoices' --rate-limit 20 \ --flows 'order-processor,invoice-generator'
created API key id=4 tenant: acme label: CI pipeline key scopes: run:orders, run:invoices flows: order-processor, invoice-generator rate limit: 20 req/s (burst 40)
key (copy now — not stored): nxk_4f19c0b27ad3489e93f5c1e6a70d2b84
Usage: Authorization: Bearer nxk_4f19c0b27ad3489e93f5c1e6a70d2b84Only a hash of the key is stored. The line above is the one and only time the key is printed — lose it and you create a new one.
nexus keys list
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--expiring-in <days> |
— | Only the keys that need renewing: expiring within this many days, already expired, or with an expiry date that cannot be read. Revoked keys and keys with no expiry are left out |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus keys list --tenant acmeid label status rate flows scopes created_at--------------------------------------------------------------------------------------------------------------------------------------4 CI pipeline key active 20/s order-processor,inv run:orders,run:invoices 2026-08-06T09:02:11Z3 legacy integration revoked — * * 2026-05-14T08:33:57Zstatus is active, revoked, or expired. rate is — for a key with no limit and N/s
otherwise. A key stored with a rate of 0, made by an earlier version, shows 0/s (refuses every call): it refuses every request, and nexus keys revoke removes it.
With --expiring-in, the same columns, soonest expiry first. When nothing is due:
$ nexus keys list --tenant acme --expiring-in 30Nothing expires within 30 days.A negative number of days is refused.
nexus keys revoke
| Flag | Default | Meaning |
|---|---|---|
--id <n> |
— | Row ID from keys list, required |
--accept-unrecorded |
off | Perform the act even when the audit chain cannot record it. See the audit trail |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus keys revoke --id 4revoked API key id=4Revoking a key that does not exist, or is already revoked, exits 1.
Every command that changes a tenant, a key, a dead letter or a lease first records its intent in the
audit chain; when the chain cannot take it the command exits 1 and nothing changes. Only
keys revoke and tenant disable accept --accept-unrecorded.
nexus proto
Publishes and inspects the protobuf descriptors that gRPC steps compile against. See gRPC and protobuf descriptors.
nexus proto deploy
| Flag | Default | Meaning |
|---|---|---|
<path> |
— | A FileDescriptorSet file. Positional, required |
--tenant <t> |
— | Tenant the descriptor belongs to, required |
--name <n> |
— | The name flows refer to, required |
--version <v> |
— | Contract version, required |
--latest |
off | Also point latest at this version |
--db <path> |
~/.nexus/registry.db |
Registry database |
Produce the descriptor with your own protoc. --include_imports is required: without it the
type references cannot be linked and the descriptor is rejected here rather than failing on
every call.
$ protoc --include_imports -o orders.binpb orders.proto
$ nexus proto deploy orders.binpb --tenant acme --name orders --version 2.1.0 --latestdeployed acme/orders@2.1.0 hash: 4c8a1e07b93d25f6 size: 8412 bytes latest -> 2.1.0Publishing a version twice is an error. Moving latest is a separate act on purpose: it changes
what every flow of that tenant puts on the wire, all at once. Without --latest the command says
latest unchanged (pass --latest to move it).
The publication is recorded in the tenant’s audit chain
as proto.deployed, with the --latest move in the same record. When the chain cannot record it,
the command refuses and nothing is stored.
The tenant must already be registered with nexus tenant create and enabled.
An unregistered or disabled tenant is refused with the same message nexus deploy gives, and
nothing is stored or recorded.
nexus proto list
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus proto list --tenant acmeNAME VERSION HASH LATEST DEPLOYEDorders 2.1.0 4c8a1e07b93d25f6 yes 2026-08-06T09:20:44Zorders 2.0.0 b70f3d92c1a84e56 2026-06-19T14:07:02Znexus proto show
Lists the services and methods a descriptor declares, with the streaming kind of each method.
| Flag | Default | Meaning |
|---|---|---|
--tenant <t> |
— | Required |
--name <n> |
— | Required |
--version <v> |
latest |
Version to inspect |
--db <path> |
~/.nexus/registry.db |
Registry database |
$ nexus proto show --tenant acme --name ordersacme/orders@2.1.0 hash: 4c8a1e07b93d25f6a2b0417de95c38f0b1ad672e94c53b8017fe2a6d4c9081bb deployed: 2026-08-06T09:20:44Z
service acme.orders.v2.OrderService GetOrder (unary) — acme.orders.v2.GetOrderRequest -> acme.orders.v2.Order ListOrders (server streaming) — acme.orders.v2.ListOrdersRequest -> acme.orders.v2.Order UploadBatch (client streaming) — acme.orders.v2.OrderBatch -> acme.orders.v2.BatchAckRead the kind before you write the flow: only unary and server streaming methods can be
called.
nexus cert
Registers certificates and tells you when they expire. The registry holds the public half:
a private key is never stored — a flow points at it by path with tls_client_key:, and rotating
it means replacing that file.
Registering is separate from using. A flow reads its certificate from the path you give it; the registry exists so that you find out about an expiry with enough notice to renew, which at an external authority takes weeks.
nexus cert add
$ nexus cert add /etc/nexus/pki/partner-client.crt \ --tenant acme --name partner-client --kind client_cert
registered id=1 acme/partner-client (client_cert) subject CN=acme-integration,O=Acme Ltd issuer CN=Partner Issuing CA serial 4A040B7020DAC022AA759496C3C712CD1605DC1E valid 2026-03-01T00:00:00Z .. 2028-02-29T23:59:59Z sha256 13889615aee5ceb5ea3ee8af4bc0282f767f028015c6cae3457091f9eb125b8e| Flag | Required | Default | Meaning |
|---|---|---|---|
<path> |
yes | PEM certificate | |
--tenant |
yes | ||
--name |
yes | What you will refer to it by | |
--kind |
no | client_cert |
client_cert, server_cert or ca_cert |
--accept-expired |
no | off | Register a certificate that has already expired |
--db |
no | ~/.nexus/registry.db |
Everything shown is read out of the certificate; nothing is supplied alongside it. A registry fed by hand would eventually report an expiry date that is not the certificate’s.
The file is parsed at registration, so pointing at the private key by mistake fails here:
$ nexus cert add /etc/nexus/pki/partner-client.key --tenant acme --name partnererror: the first PEM block is `PRIVATE KEY`, not `CERTIFICATE` — point at the certificate, not at the key or the chainAn expired certificate is refused, and nothing is stored:
$ nexus cert add /etc/nexus/pki/old-partner.crt --tenant acme --name old-partnererror: this certificate expired at 2021-01-01T00:00:00Z — nothing was registered; pass --accept-expired to register it anywayWith --accept-expired it is registered, with a warning. The check comes after the file is
parsed, so the flag does not let through a file that is not a certificate.
A certificate that is not valid yet is registered, and its status says from when:
status pending, valid from 2027-01-01T00:00:00ZA name that already exists is refused. Rotating is replace.
nexus cert list
$ nexus cert list --tenant acmeNAME KIND EXPIRES STATUSpartner-client client_cert 2027-01-14T00:00:00Z expires in 22 daysregistry-ca ca_cert 2028-02-29T23:59:59Z valid, 690 days left| Flag | Required | Meaning |
|---|---|---|
--tenant |
yes | |
--expiring-in <days> |
no | Only what expires within this many days |
--db |
no |
STATUS is one of valid, N days left, pending, valid from <date> (not valid yet),
expires in N days, EXPIRED or EXPIRED N days ago, and UNREADABLE EXPIRY DATE — not monitored.
Soonest expiry first. Already-expired entries are always shown, including under
--expiring-in: they are the urgent case, and a window that only looked forward would go quiet
exactly when the integration breaks.
$ nexus cert list --tenant acme --expiring-in 30That is the line to put in a scheduled job.
nexus cert replace
$ nexus cert replace /etc/nexus/pki/partner-client-2028.crt \ --tenant acme --name partner-client
replaced id=7 acme/partner-client (client_cert) …The previous entry is kept, marked replaced.Same flags as add, --accept-expired included: an expired replacement is refused unless the
flag is given, and a refusal leaves the current entry as it was. The old entry is retired rather than deleted — it is still the answer to
what was being presented on a given date.
Note that this updates the registry, not the flow. The flow reads whatever file
tls_client_key: and tls_client_cert: point at, so the rotation itself is replacing those
files.
nexus cert reconcile
Compares what the deployed flows ask for against what the registry knows. Read-only — it never registers anything.
$ nexus cert reconcile --tenant acmeScanned the `latest` version of each deployed flow.Paths resolve on THIS node's filesystem — another node may hold different files.
UNREADABLE — these flows will fail at their next call: acme/partner-query step 'call' tls_ca_cert: /etc/nexus/pki/partner-ca.pem No such file or directory (os error 2)
NOT REGISTERED — in use, but the registry does not track their expiry: acme/order-push step 'send' tls_client_cert: /etc/nexus/pki/our-client.crt sha256 1388…5b8e register with: nexus cert add /etc/nexus/pki/our-client.crt --tenant acme --name <name>
3 reference(s) in use and registered.
REGISTERED BUT NOT REFERENCED — spare material, or a leftover: acme/old-client (client_cert) expires 2027-01-14T00:00:00Z| Flag | Required | Meaning |
|---|---|---|
--tenant |
no | Narrow to one tenant. Omit to cover all |
--db |
no |
Four verdicts, in the order they matter:
Unreadable is the one worth running this for. The flow is deployed and its TLS material is not there, so its next call fails — with an error that deliberately does not name the path. Finding that out before the call is worth more than any expiry warning.
Not registered means the certificate is in use but nothing tracks when it expires. The
fingerprint and the exact nexus cert add line are printed so registering is a copy and a paste.
Registered but not referenced is not an error. It may be spare material staged for a rotation, or it may be left over from one — the difference is yours to make.
Could not be scanned appears when a flow’s artifact cannot be read at all. It is listed separately rather than skipped: a shorter list would look exactly like a clean result.
Two limits, both stated in the output rather than assumed:
- Only the
latestversion of each flow is scanned. Older versions may legitimately reference paths that no longer exist. - Paths resolve on the filesystem of the node you run this on. The registry is shared between
nodes;
/etc/nexus/pkiis not.
nexus cert show
$ nexus cert show --tenant acme --name partner-clientThe current entry followed by every retired one, newest first, each with its facts, when it was registered, and its status.
nexus seed
Deploys a directory of demo flows, creates demo API keys and UI users, and runs each flow once. Useful for getting a populated instance to look at.
| Flag | Default | Meaning |
|---|---|---|
--db <path> |
~/.nexus/registry.db |
Registry database; the UI database is created beside it |
--demo-dir <dir> |
./examples/demo |
Directory of .flow.md files to deploy |
--no-run |
off | Deploy and create keys and users, but do not execute the flows |
$ nexus seed --no-runNexusFabric seed registry db: /home/you/.nexus/registry.db ui db: /home/you/.nexus/nexus-ui.db demo dir: /home/you/examples/demo
── Deploying flows ───────────────────────────────── deployed acme/order-processor v1.0.0 ...Flows are deployed at version 1.0.0 and latest is moved, so running it twice is harmless.
Every flow published is recorded in its tenant’s audit chain as flow.deployed; when the chain
cannot record it, the command refuses and that flow is not published.
Each UI user the command creates (admin, dev, support) gets a fresh random password of 24
characters, printed once in the output and not recoverable afterwards. A UI user whose username
already exists is skipped, and no password is printed for it. API keys are created fresh every
time, and each one is printed once.
The command fails if the demo directory does not exist.
nexus mcp
Starts a Model Context Protocol server on stdin and stdout, for an AI assistant writing flows. It is started by the assistant’s client, not by hand, and takes no flags.
$ nexus mcpIt offers three tools, validate_flow, compile_flow and describe_language, answered by the
same compiler as nexus validate and nexus compile. It reads no file (not even .env) and
no installation, resolves no secret or environment variable for a flow, and opens no socket.
The one variable it reads is RUST_LOG, for its own log, which goes to stderr. It exits with
status 0 when its stdin closes.