Skip to content

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.

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

Terminal window
SubactId.Server keys rotate --out /run/secrets/subactid/next.pem

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

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

Terminal window
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_KEY or piped standard input. There is no flag for it.
  • --dry-run validates locally and sends nothing. It needs no key and no --server, and reports every valid file as would create. Run it on pull requests.
  • apply never deletes an agent and never changes enabled. Use the admin API for both.

Output is one line per file, then a summary:

Terminal window
created jira-triage
1 file(s), 1 changed.

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.

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

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

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

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

  • --to must be an existing directory. An export is never overwritten.
  • --before may be left out when SubactId:Audit:Retention is set. It cannot be after the current month.

Retention has what to plan around.

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.

Subact ID Pre-release. v0.1 is not out yet.

© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.

LegalTermsPrivacy