Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.
Command line
One binary. With no command it runs the server; with a command it runs an operator tool.
SubactId.Server below is that binary: from a checkout,
dotnet run --project src/SubactId.Server -- <command>; in the container image, the container’s
arguments.
| Command | Reads configuration | What it does |
|---|---|---|
| (none) | all of it | Runs the control plane |
keys generate |
nothing | Writes a new signing key |
keys |
all of it, then the signing keys | Prints the public JWKS and the active key id |
keys rotate |
all of it, then the signing keys | Writes the next key and prints the three-deploy rollout |
agent init |
nothing | Writes a registration, a key pair and a public key set |
agent apply |
nothing | Reconciles registration files against a running control plane |
migrate |
all of it, then the database | Applies the schema |
doctor |
all of it | Checks everything the server would fail on, and says which |
audit-verify |
all of it, then the database | Verifies the seal over the ledger, or over an export |
audit-archive |
all of it, then the database | Exports, verifies and drops months of the ledger |
keys generate, agent init and agent apply read no server configuration, so they work before
any deployment exists. Every other command loads and validates the whole configuration first and
stops if it is invalid. doctor runs either way, to report what is wrong.
keys generate
Section titled “keys generate”SubactId.Server keys generate --out /run/secrets/subactid/active.pem [--kid <name>]Writes an unencrypted P-256 key as a PKCS#8 PEM readable only by its owner, and refuses to overwrite an existing file. It prints the key id and the setting that configures the key, and never prints the private key. The key id defaults to the RFC 7638 thumbprint.
Prints the JWKS the configured keys publish, then the active key id. Use it to confirm a rotation reached the process you expect.
keys rotate
Section titled “keys rotate”SubactId.Server keys rotate --out /run/secrets/subactid/next.pemLoads the configured keys as the server would, writes a new key file, and prints the settings for
each step of the rollout. It also takes --kid <name>. It changes nothing but the new file. The
wait before retiring the old key is the largest max_token_ttl of any registered agent, disabled
ones included; SubactId:Tokens:DefaultTokenTtl when no agent is registered; or
SubactId:Agents:MaxTokenTtl when the registry cannot be read.
Operating it walks through the steps.
agent init
Section titled “agent init”SubactId.Server agent init <agent-id> --out agents/Writes three files: the registration as YAML, its public key set as JSON, and the private key, readable only by its owner. Commit the first two; move the private key to a secret store. It refuses to overwrite any of the three and never prints the private key.
The registration has tight defaults: sponsor_required: true, max_task_ttl: PT30M,
max_token_ttl: PT5M, max_delegation_depth: 1. allowed_scopes and allowed_audiences are
empty, and apply refuses the file until both are filled in, with --dry-run too.
agent apply
Section titled “agent apply”SUBACTID_ADMIN_KEY=… SubactId.Server agent apply agents/*.yaml --server https://subactid.example.com [--dry-run]For each file, apply validates it locally with the admin API’s rules, reads the agent, and sends
POST if it is missing or PATCH with only the fields that differ. A file that already matches
sends nothing, so a second run makes only the read. Every change goes through the admin API and
is audited like any other admin request.
- The admin key comes from
SUBACTID_ADMIN_KEYor piped standard input. There is no flag for it. --dry-runvalidates locally and sends nothing. It needs no key and no--server, and reports every valid file aswould create. Run it on pull requests.applynever deletes an agent and never changesenabled. Use the admin API for both.
Output is one line per file, then a summary:
created jira-triage
1 file(s), 1 changed.migrate
Section titled “migrate”Applies the configured provider’s schema and prints what it applied. Migrations never run at
startup. With SubactId:Database:MigrationConnectionString set, it connects as that role. On Postgres
it also creates the ledger partitions for the current month and the months ahead. On SQLite it
creates the file and its directory if needed and turns on write-ahead logging.
doctor
Section titled “doctor”SubactId.Server doctor [--subject-token -]Runs the readiness checks and a few more, and prints one line per check with a likely cause and what to check for each failure. It checks the configuration, the signing key, the identity provider, the database and its schema, the ledger’s partitions, the admin API, the sponsor check mode and the SCIM and Shared Signals receivers:
ok configuration Loaded.ok signing key Active kid '9Fy-qRMxKnbHaNBF9nwF68GQVxzhgz3miPA15FHHv38', 1 key(s) published.FAIL upstream The discovery document could not be fetched. Likely cause: nothing is serving https://localhost:8443/realms/main from here, or the request timed out. Check the host is resolvable and reachable from this pod, and that SubactId__UpstreamIdp__Issuer is the realm URL.ok database Reachable (sqlite), schema up to date.ok audit partitions Not partitioned; the embedded provider keeps the ledger in one table. Records are never removed from it: detaching a partition is how a ledger sheds history without weakening the guard, and there is none to detach here.ok admin API Enabled; /admin requires the configured key.ok sponsor check Mode 'poll'; the identity provider is asked at https://localhost:8443/admin/realms/main/users, reusing an answer for at most 00:00:30. This control plane authenticates there as 'subactid' with a signed assertion, whose service account needs to be able to read users.ok SCIM receiver Not configured; /scim answers 404.ok signals receiver Not configured; /events answers 404.
9 check(s), 1 failed.To also check a real user’s access token the way an exchange would, pipe it in:
printf '%s' "$TOKEN" | SubactId.Server doctor --subject-token -. Standard input keeps the token out
of the process list. doctor never prints a token, a secret or a configured value, and exits 1
if any check fails, so it works as a Helm test or a CI step.
audit-verify
Section titled “audit-verify”SubactId.Server audit-verify [<checkpoint_id>:<root>,...] [--archive <export>]Walks every checkpoint in id order and checks its signature against the published key set, its
link to the previous checkpoint, and its root against the records it covers. It prints the last
checkpoint as checkpoint_id:root. Pass earlier ones back and it also confirms each is still
present and signs the same root, which is how a cut tail is detected. After an archive, it starts
from the checkpoint after the last archived one and checks the link to it.
With --archive, it verifies one export against the published key set and reads nothing from the
database.
audit-archive
Section titled “audit-archive”SubactId.Server audit-archive --before <YYYY-MM> --to <directory>Postgres only; on SQLite it refuses to run. For each month before --before, oldest first, it
exports the month to <directory>/audit-<YYYY-MM>.subactid-archive.gz, reads the export back and
verifies it, writes an audit.archived record with the export’s SHA-256, and then detaches and
drops the partition. A month that fails stops the run with nothing detached.
--tomust be an existing directory. An export is never overwritten.--beforemay be left out whenSubactId:Audit:Retentionis set. It cannot be after the current month.
Retention has what to plan around.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Success |
1 |
Failure: a check failed, a file could not be written, a registration was refused, there was no key to rotate, or the ledger could not be read or written |
2 |
Bad arguments, or agent apply with no admin key |
3 |
audit-verify: the seal is broken. audit-archive: a month was refused, with nothing detached |
The server exits 1 on invalid configuration, after printing every error.
© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.