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.
Configuration
The control plane reads its configuration from environment variables. Every setting has a key and
a variable: SubactId:RateLimit:Burst is the key and SubactId__RateLimit__Burst is the variable.
Replace each : with __.
The server does not start on invalid configuration. It reports every error at once, names both the key and the variable, and never repeats a configured value.
How a value is read
Section titled “How a value is read”- Empty means unset. A blank value takes the default.
SubactId__Admin__ApiKey=disables the admin API. - Durations are ISO 8601 (
PT5M) or .NET time spans (00:05:00). Every duration must be greater than zero. Years and months are refused, because they have no fixed length. Durations in an agent registration are ISO 8601 only. - Whole numbers are plain digits, with no sign or separators.
- Booleans are
trueorfalse. - URLs must be absolute
httporhttps. - Lists are comma-separated.
SubactId:RateLimit:TrustedProxiesis the only list. Signing keys are numbered instead:SubactId__Signing__Keys__0__Path. - Command-line arguments override the environment, for example
--SubactId:RateLimit:Burst=5. Use this to try things out, not in a deployment.
Identity provider
Section titled “Identity provider”| Setting | Default | Notes |
|---|---|---|
SubactId:Issuer |
— | Required. The URL the control plane issues tokens as: the iss of every token. A trailing slash is dropped |
SubactId:UpstreamIdp:Issuer |
— | The identity provider’s realm URL. The discovery URL is derived from it |
SubactId:UpstreamIdp:MetadataUrl |
— | The full discovery URL instead of Issuer. Set exactly one of the two |
SubactId:UpstreamIdp:Audience |
— | Required. The aud a subject token must carry |
SubactId:UpstreamIdp:SponsorCheck:Mode |
poll |
poll or signals |
SubactId:UpstreamIdp:SponsorCheck:UsersUrl |
— | The provider’s admin users collection. Required under poll, refused under signals |
SubactId:UpstreamIdp:SponsorCheck:TokenUrl |
— | The provider’s token endpoint. Required under poll, refused under signals |
SubactId:UpstreamIdp:SponsorCheck:ClientId |
— | The client the control plane is registered as at the provider. Required under poll, refused under signals |
SubactId:UpstreamIdp:SponsorCheck:CacheTtl |
PT30S |
How long a person’s status is reused. At most SubactId:Tokens:DefaultTokenTtl. Refused under signals |
SubactId:UpstreamIdp:SponsorKeyClaim |
sub |
The claim each task records the human under. At most 64 characters, no whitespace |
SubactId:UpstreamIdp:BackchannelLogout:Audience |
— | Turns on POST /backchannel-logout. The human-facing client the logout URI is registered on, not UpstreamIdp:Audience |
What the two modes do, and when to change SponsorKeyClaim, is in
connecting your identity provider.
SCIM and Shared Signals
Section titled “SCIM and Shared Signals”Each receiver is off until its first setting is set. Unset, its routes do not exist and answer
404. The other settings of each group are refused without it.
| Setting | Default | Notes |
|---|---|---|
SubactId:Scim:BearerToken |
— | Turns on the SCIM 2.0 receiver at /scim/v2. At least 32 characters. A secret |
SubactId:Scim:PreviousBearerToken |
— | A second accepted credential, for rotation. At least 32 characters, different from BearerToken. A secret |
SubactId:Scim:SponsorKeyAttribute |
externalId |
externalId or userName: the attribute that names the person. Must match SponsorKeyClaim |
SubactId:Scim:MaxUsers |
50000 |
Most user records held |
SubactId:Ssf:Issuer |
— | Turns on the Shared Signals receiver at POST /events. The transmitter’s issuer URL |
SubactId:Ssf:Audience |
— | Required with Issuer. The audience every event must carry |
SubactId:Ssf:BearerToken |
— | Required with Issuer. At least 32 characters. A secret |
SubactId:Ssf:PreviousBearerToken |
— | A second accepted credential, for rotation. At least 32 characters, different from BearerToken. A secret |
Section 6 of the spec describes what each receiver accepts.
Database
Section titled “Database”| Setting | Default | Notes |
|---|---|---|
SubactId:Database:Provider |
postgres |
postgres or sqlite |
SubactId:Database:ConnectionString |
— | Required for postgres, refused for sqlite. A secret |
SubactId:Database:MigrationConnectionString |
the connection string | Refused for sqlite. A role that may change the schema, used only by migrate. A secret |
SubactId:Database:Path |
— | The SQLite file. Required for sqlite, refused for postgres |
Under postgres, readiness uses its own pool of at most four connections. Count those per
instance when you size max_connections. The request pool’s size is Maximum Pool Size in the
connection string (100 by default).
Signing keys
Section titled “Signing keys”| Setting | Default | Notes |
|---|---|---|
SubactId:Signing:Keys:N:Path |
— | Path to a PEM file, typically a mounted secret |
SubactId:Signing:Keys:N:Pem |
— | The PEM inline. Set exactly one of Path or Pem. A secret |
SubactId:Signing:Keys:N:Kid |
the RFC 7638 thumbprint | An explicit key id |
SubactId:Signing:ActiveKid |
— | The key that signs. Required when more than one key is set |
N is a number from 0, and gaps are allowed. Every configured key is published in the JWKS;
only the active one signs. With no key, a Development host generates an ephemeral key and logs
that it did. Any other environment refuses to start. Prefer Path over Pem, so the key is not in
the process environment. Operating it covers rotation.
Lifetimes and the sweeper
Section titled “Lifetimes and the sweeper”| Setting | Default | Notes |
|---|---|---|
SubactId:Tokens:DefaultTaskTtl |
PT30M |
max_task_ttl for a registration that sets none |
SubactId:Tokens:DefaultTokenTtl |
PT5M |
max_token_ttl for a registration that sets none. At most DefaultTaskTtl |
SubactId:Agents:MinTaskTtl |
PT1M |
Shortest max_task_ttl a registration may set |
SubactId:Agents:MaxTaskTtl |
P1D |
Longest max_task_ttl a registration may set |
SubactId:Agents:MinTokenTtl |
PT30S |
Shortest max_token_ttl a registration may set |
SubactId:Agents:MaxTokenTtl |
PT1H |
Longest max_token_ttl a registration may set |
SubactId:Tasks:SweepInterval |
PT1M |
Time between passes that mark expired tasks terminal. PT1S to P1D |
SubactId:Tasks:SweepBatchSize |
20 |
Tasks expired per transaction |
SubactId:Tasks:Retention |
P7D |
How long an ended task is kept before the sweeper deletes it and its grants |
A registration’s own max_task_ttl and max_token_ttl apply to its tasks. The SubactId:Tokens:*
settings apply only to a registration that sets none. To hold every agent to shorter lifetimes,
lower the SubactId:Agents:* bounds.
A token’s lifetime is always cut to what remains of its task, and a task with less than five seconds left is refused a token.
Admin API
Section titled “Admin API”| Setting | Default | Notes |
|---|---|---|
SubactId:Admin:ApiKey |
— | At least 32 characters. Unset, /admin and /audit answer 503. A secret |
Audit delivery
Section titled “Audit delivery”Records are always written to the ledger. With a sink set, they are also queued and posted to it in the background, at least once.
| Setting | Default | Notes |
|---|---|---|
SubactId:Audit:Sink:Url |
— | Where records are posted. Must not contain credentials |
SubactId:Audit:Sink:BearerToken |
— | Sent to the sink. Requires Sink:Url, and that it is https. A secret |
SubactId:Audit:DrainInterval |
PT5S |
Time between delivery passes. PT1S to PT1H |
SubactId:Audit:DrainBatchSize |
100 |
Records posted per request |
Audit checkpoints, retention and aggregation
Section titled “Audit checkpoints, retention and aggregation”| Setting | Default | Notes |
|---|---|---|
SubactId:Audit:Checkpoint:Interval |
PT1M |
Time between sealing passes, and the longest a record stays unsealed. PT1S to PT1H |
SubactId:Audit:Retention |
— | The default cutoff for audit-archive. At least P31D. Postgres only |
SubactId:Audit:Partitions:MonthsAhead |
2 |
Ledger partitions created ahead of the current month. 1 to 12. Postgres only |
SubactId:Audit:Aggregation:Enabled |
true |
Collapse denials that name nobody, and a task’s renewals, into summary records |
SubactId:Audit:Aggregation:Window |
PT1M |
PT1S to PT15M |
SubactId:Audit:Retention deletes nothing by itself; see
retention. Unset, the ledger grows without bound. Turning
aggregation off logs a warning at startup. SubactId:Audit:Chains is not a setting, and the server
refuses to start if it is set.
Rate limiting
Section titled “Rate limiting”A token bucket per source, per instance. A refused request gets 429, Retry-After and
slow_down, and is audited with reason rate_limited.
A request spends one of three buckets, chosen by its path:
| Path | Bucket |
|---|---|
POST /oauth2/introspect |
Introspection, per source |
/backchannel-logout, /scim, /events |
Signals, per receiver per source |
/healthz, /readyz |
none |
Everything else, /oauth2/token included |
the main bucket, per source |
| Setting | Default | Notes |
|---|---|---|
SubactId:RateLimit:Enabled |
true |
true or false |
SubactId:RateLimit:PermitsPerMinute |
600 |
Sustained requests per source |
SubactId:RateLimit:Burst |
120 |
Bucket size |
SubactId:RateLimit:Signals:PermitsPerMinute |
6000 |
Sustained signals per source per receiver |
SubactId:RateLimit:Signals:Burst |
5000 |
Signal bucket size: the most sessions a provider can end in one burst |
SubactId:RateLimit:Introspection:PermitsPerMinute |
6000 |
Sustained introspection calls per source, about one per high-risk tool call |
SubactId:RateLimit:Introspection:Burst |
1200 |
Introspection bucket size |
SubactId:RateLimit:TrustedProxies |
— | CIDR networks whose X-Forwarded-For is trusted. 0.0.0.0/0 is refused |
- Each bucket refills every second by
PermitsPerMinute / 60, rounded up. EachBurstmust be at least one second’s worth of its rate. nreplicas admit up tontimes the configured rate.- Agents behind one egress address are one source. Behind a proxy, set
TrustedProxies, or every caller shares the proxy’s bucket. List every hop.
Overload
Section titled “Overload”Caps the work one instance takes on at once, across all callers. Each kind of work has its own
limit and queue: the token endpoint, introspection, the paths that revoke (/oauth2/revoke,
/backchannel-logout, /scim, /events, /admin), and everything else. A request that finds no
turn gets 503, Retry-After: 1 and temporarily_unavailable, and is audited with reason
overloaded. /healthz and /readyz are never limited.
| Setting | Default | Notes |
|---|---|---|
SubactId:Overload:Enabled |
true |
true or false |
SubactId:Overload:ConcurrencyLimit |
16 per core | 1 to 100000. Requests run at once, per kind. A pod with no CPU limit sees every core of its node |
SubactId:Overload:QueueLimit |
4 × ConcurrencyLimit |
0 to 1000000. Requests that may wait, per kind |
SubactId:Overload:QueueTimeout |
PT1S |
How long a request may wait. At most PT30S |
When the database cannot answer, requests get 503, temporarily_unavailable and
Retry-After: 5. No token is issued without its audit record.
Rules checked across settings
Section titled “Rules checked across settings”Each is a startup error.
SubactId:Tokens:DefaultTokenTtlis at mostSubactId:Tokens:DefaultTaskTtl.- Each
SubactId:Agents:Min*is at most itsMax*, andMinTokenTtlis at mostMinTaskTtl. - Each
SubactId:Tokens:Default*is within itsSubactId:Agents:*bounds. - Under
poll,SponsorCheck:CacheTtlis at mostSubactId:Tokens:DefaultTokenTtl, so a disabled user’s tasks end within one token lifetime. - Each rate-limit
Burstis at least one second of itsPermitsPerMinute. SubactId:Audit:Sink:BearerTokenrequires anhttpsSink:Url.SubactId:UpstreamIdp:IssuerandMetadataUrlare not both set.- No
SponsorCheckURL, client id orCacheTtlundersignals. - No
SubactId:Scim:*withoutSubactId:Scim:BearerToken, and noSubactId:Ssf:*withoutSubactId:Ssf:Issuer. - Each
PreviousBearerTokendiffers from itsBearerToken. - Each database provider refuses the other provider’s settings.
- Exactly one of
PemorPathper signing key.
Secrets
Section titled “Secrets”These settings hold secrets. None is logged, repeated in an error or returned by any endpoint. Pass them as mounted files or secret references, never in a committed manifest.
SubactId:Database:ConnectionStringandSubactId:Database:MigrationConnectionStringSubactId:Signing:Keys:N:PemSubactId:Admin:ApiKeySubactId:Audit:Sink:BearerTokenSubactId:Scim:BearerTokenandSubactId:Scim:PreviousBearerTokenSubactId:Ssf:BearerTokenandSubactId:Ssf:PreviousBearerToken
Checking it
Section titled “Checking it”SubactId.Server doctor loads this configuration as the server would and reports what is wrong; see
the command line.
Other environment variables
Section titled “Other environment variables”| Variable | What it does |
|---|---|
ASPNETCORE_HTTP_PORTS |
The port the server listens on. The container image sets 5100 |
ASPNETCORE_ENVIRONMENT |
Development makes the server generate a signing key when none is set. Not for deployment |
SUBACTID_ADMIN_KEY |
Read by agent apply, not by the server |
© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.