Skip to content

Command line

Every command of the nexus binary, every flag, and what each one defaults to.

Terminal window
$ nexus --help
$ nexus --version

State 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
Terminal window
$ nexus validate forward-order.flow.md
ok forward-order.flow.md
flow: forward-order
tenant: acme
steps: 3

validate 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:

Terminal window
$ nexus validate forward-order.flow.md
forward-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
Terminal window
$ 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:

Terminal window
$ nexus compile forward-order.flow.md --output forward-order.ir.json
ok forward-order.flow.md → forward-order.ir.json
hash: 9f2b7c4d1ae05836b1c0d4f7a83e2916d5b0c7314ea9f6d8b2c5107e43ab9d02

nexus 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
Terminal window
$ nexus deploy forward-order.flow.md --version 1.2.0
deployed forward-order.flow.md
flow: forward-order
tenant: acme
version: 1.2.0
hash: 9f2b7c4d1ae05836b1c0d4f7a83e2916d5b0c7314ea9f6d8b2c5107e43ab9d02
db: /home/you/.nexus/registry.db

The 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
Terminal window
$ nexus undeploy --tenant acme --flow forward-order
undeployed acme/forward-order
version: 1.3.0
routes: 2 removed

A 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:

Terminal window
$ nexus undeploy --tenant acme --flow orders
error: refusing to undeploy acme/orders: acme/intake (queue in step 'hand-off') names it

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

Terminal window
$ nexus serve --port 9090
NexusFabric 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: enabled

Started 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:

Terminal window
$ nexus serve
error: 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 picture

The 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 as nexus serve refuses it, before anything else is judged, when any other account of the host can reach it; the remedy is chmod o-rwx on 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 old ctx.X form.
  • The registry settings descendants_max_extra_bytes, fanout_max_parallel, queue_segment_max_bytes, queue_retention_hours and oidc_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 declares dedup_key:, correlation_open: or correlation_close:. When it is set and well formed, flow_state_retention_hours, flow_lease_ttl_secs, dedup_max_response_bytes and correlation_state_max_bytes are 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 sentence nexus serve stops 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: enabled when set, disabled when not, refused when it is set but empty or not valid UTF-8. Neither the value nor its length is printed.
Terminal window
$ nexus config check
descendants copy ceiling: 33554432 bytes (default)
split fan-out width: 10 branches (default)
queue segment size: 134217728 bytes
queue retention term: none
flow state (dedup + correlation): disabled — no NEXUS_SECRET_NEXUS_STATE_KEY set
webhook signature verification: enabled
oidc 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) — MISSING
error: 1 declared key(s) have no value: PARTNER_ENDPOINT (NEXUS_CONFIG_PARTNER_ENDPOINT). `nexus serve` will refuse to start

Each 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 for nexus serve. Run as another user, or from another directory, it judges another environment.
  • Set-but-empty is reported as set, empty, distinct from MISSING. 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
Terminal window
$ nexus audit --tenant acme --limit 3 --verify
seq event_type created_at correlation_id
------------------------------------------------------------------------------------------
1187 queue.message.processed 2026-08-06T09:14:22Z 7f3a1c02-9b6e-4f11-8a2d-0d55c3b41e90
1186 queue.message.failed 2026-08-06T09:14:19Z 7f3a1c02-9b6e-4f11-8a2d-0d55c3b41e90
1185 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/
Terminal window
$ nexus dlq list --tenant acme
DLQ: 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 out

correlation_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/
Terminal window
$ nexus dlq replay --tenant acme --flow order-processor --id 17
replayed id=17 tenant=acme flow=order-processor

nexus 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/
Terminal window
$ nexus dlq discard --tenant acme --flow order-processor --id 17
discarded 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 listings

nexus 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
Terminal window
$ 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: 2

The 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 declare dedup_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 than dedup_max_response_bytes. A duplicate is refused rather than handed a truncated body: 409 with an explicit message on JSON, a Server (SOAP 1.1) or Receiver (SOAP 1.2) fault on 500 on SOAP.
  • empty: memoisation was requested and the answer had no body (a flow whose output is null). A duplicate gets what the first caller read: the recorded status, no body, no Content-Type; the same answer as not_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:

claim — new key, or the previous row's window/lease has expired the flow succeeds the flow errors or faults once dedup_window_hours has passed (no row) in_flight done failed

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 on dedup_memoize_response:.
  • failed — replayed under dedup_on_failure: absorb; runs again, as a fresh attempt, under the default retry.

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
Terminal window
$ 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.

Terminal window
$ nexus tenant create --id acme --display-name "Acme Ltd" --auth-methods api_key,oidc

The 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

Terminal window
$ nexus tenant list
id display name status auth methods created_at
----------------------------------------------------------------------------------------------------
acme Acme Ltd active api_key 2026-08-13T09:14:02Z
globex Globex disabled api_key 2026-07-02T11:40:55Z

nexus 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
Terminal window
$ nexus tenant disable --id globex
disabled 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.

Terminal window
$ nexus tenant enable --id globex
enabled 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
Terminal window
$ 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 20
declared 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 tenant

With --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
Terminal window
$ 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 20
registered 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
Terminal window
$ 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_4f19c0b27ad3489e93f5c1e6a70d2b84

Only 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
Terminal window
$ nexus keys list --tenant acme
id 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:11Z
3 legacy integration revoked — * * 2026-05-14T08:33:57Z

status 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:

Terminal window
$ nexus keys list --tenant acme --expiring-in 30
Nothing 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
Terminal window
$ nexus keys revoke --id 4
revoked API key id=4

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

Terminal window
$ protoc --include_imports -o orders.binpb orders.proto
$ nexus proto deploy orders.binpb --tenant acme --name orders --version 2.1.0 --latest
deployed acme/orders@2.1.0
hash: 4c8a1e07b93d25f6
size: 8412 bytes
latest -> 2.1.0

Publishing 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
Terminal window
$ nexus proto list --tenant acme
NAME VERSION HASH LATEST DEPLOYED
orders 2.1.0 4c8a1e07b93d25f6 yes 2026-08-06T09:20:44Z
orders 2.0.0 b70f3d92c1a84e56 2026-06-19T14:07:02Z

nexus 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
Terminal window
$ nexus proto show --tenant acme --name orders
acme/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.BatchAck

Read 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

Terminal window
$ 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:

Terminal window
$ nexus cert add /etc/nexus/pki/partner-client.key --tenant acme --name partner
error: the first PEM block is `PRIVATE KEY`, not `CERTIFICATE` — point at the certificate, not at the key or the chain

An expired certificate is refused, and nothing is stored:

Terminal window
$ nexus cert add /etc/nexus/pki/old-partner.crt --tenant acme --name old-partner
error: this certificate expired at 2021-01-01T00:00:00Z — nothing was registered; pass --accept-expired to register it anyway

With --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:

Terminal window
status pending, valid from 2027-01-01T00:00:00Z

A name that already exists is refused. Rotating is replace.

nexus cert list

Terminal window
$ nexus cert list --tenant acme
NAME KIND EXPIRES STATUS
partner-client client_cert 2027-01-14T00:00:00Z expires in 22 days
registry-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.

Terminal window
$ nexus cert list --tenant acme --expiring-in 30

That is the line to put in a scheduled job.

nexus cert replace

Terminal window
$ 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.

Terminal window
$ nexus cert reconcile --tenant acme
Scanned 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 latest version 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/pki is not.

nexus cert show

Terminal window
$ nexus cert show --tenant acme --name partner-client

The 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
Terminal window
$ nexus seed --no-run
NexusFabric 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.

Terminal window
$ nexus mcp

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