Architecture decisions¶
One record per decision that shapes this repository from the outside —
what a relying party must do, what an installation must accept, what a
release removes. The pages under concepts/sluis/ say
how the shipped thing works; a record here says why it is shaped that
way, what was weighed against it, and what follows from choosing it. When
the two disagree, the explanation pages describe what actually shipped and a
record here is read as the reasoning that got there.
A record is never edited to reverse a decision. A changed mind gets a new record that supersedes the old one, so the index below stays a true timeline and nothing is silently rewritten under an old date. The old record's Status line (and the Status column below) is changed to say what overtook it, so a reader of one record does not need to know another exists; its body is not edited.
Index¶
| ADR | Decision | Status |
|---|---|---|
| 0001 | Sessions and an absolute limit | Accepted; amended by 0033 and 0040 |
| 0002 | Mission boundary: tokens and memberships | Accepted; partly superseded by 0008 and, for sluisctl credential db/client specifically, by 0013; its "next candidate, a chat workspace's channel membership" was built as the Slack reconciler, see 0017 |
| 0003 | Deprecate and remove the access-proxy chart | Accepted; carried out in v1.32.0 (the chart is removed; the recipe for running upstream oauth2-proxy on another gateway lives in how-to/connect/oauth2-proxy.md) |
| 0004 | SSH: opkssh for people, the secret store's SSH CA for hosts | Accepted; partly superseded by 0011 (machines and hosts move to the secret store); refined by 0015 |
| 0005 | ES384 is the signing algorithm | Accepted; partly superseded by 0009 (ES384 stays the default; it is no longer the only algorithm an audience may have) |
| 0006 | The groups claim is scoped per audience, by default | Accepted; the scoping rule is refined by 0010 |
| 0007 | Breaking changes inside 1.x | Accepted; extended by 0036 |
| 0008 | Credentials only where we govern membership | Accepted; supersedes 0002 in part; refined by 0014; applied to Slack by 0025 |
| 0009 | A default signing algorithm, and per-audience exceptions | Accepted; partly supersedes 0005 |
| 0010 | A declared vocabulary: things, scopes, roles, inheritance and mapping wildcards | Accepted; extended by 0012 |
| 0011 | SSH: people on opkssh, machines and hosts on the secret store's OpenBAO | Accepted; amended by 0013 (the machine path is now sluisctl bao ssh -mode=ca / sluisctl bao write ... sign/<role>, not a dedicated sluisctl credential ssh); refined by 0015, 0016 |
| 0012 | Per-role scopes in the vocabulary | Accepted; extends 0010 |
| 0013 | OpenBAO access through the bao CLI |
Accepted; refines 0002, amends 0011 |
| 0014 | Minting third-party credentials: only where membership is governed, brokers elsewhere | Accepted; refines 0008 |
| 0015 | A per-audience groups delimiter, for opkssh's colon-splitting bug | Accepted; refines 0004 and 0011 |
| 0016 | A managed known_hosts file for SSH host CAs, distinct from sluisctl bao |
Accepted; refines 0011, distinguishes from 0013 |
| 0017 | The Slack reconciler keeps channel membership and nothing else | Accepted; applies 0002 |
| 0018 | Do not configure what the product already knows | Accepted; applies 0007 |
| 0019 | Two kinds of Slack channel, never mixed | Accepted; refines 0017 |
| 0020 | A channel defined twice is held, not taken over | Accepted; refines 0019 |
| 0021 | Slack Connect channels are console-managed, audited records | Accepted; refines 0019 |
| 0022 | The console archives only ordinary channels, only when asked | Accepted; refines 0017 |
| 0023 | The guest-side probe asks only about managed Slack Connect channels | Accepted; refines 0021 |
| 0024 | Reconciler rails are shared pieces, not a framework | Accepted |
| 0025 | The Slack Apps catalogue keeps credentials but mints none | Accepted; applies 0008 and 0014 |
| 0026 | Two platforms, permanently: Kubernetes and AWS Lambda | Accepted |
| 0027 | The State port: NATS JetStream on Kubernetes, DynamoDB on AWS | Accepted; partly superseded (2026-10-04): the NATS JetStream half was removed, the DynamoDB half stands. It supersedes the store statement in the store ("plain Kubernetes objects … no cloud parameter store, no cache") once the migration in 0031 has run |
| 0028 | Nothing writes ConfigMaps or Secrets; written secrets are sealed | Accepted; partly superseded (2026-10-04): sealing is retired, the rule that nothing writes ConfigMaps or Secrets stands; extended by 0034; amended by 0036 |
| 0029 | Ticks per target, under a lease | Accepted; applies 0024 |
| 0030 | Workload identity: both mechanisms on both platforms | Accepted; applies 0002 |
| 0031 | A generic migration tool, and the order of the move | Accepted; amended by 0036 |
| 0032 | One configuration file, one binary, one chart | Accepted; applies 0007, 0018; refined by 0036 |
| 0033 | A longer absolute limit for read-only resources | Accepted; amends 0001; amended by 0040 (the lengthening for read-only resources is deprecated) |
| 0034 | Exports: the service copies its secrets into OpenBao itself | Accepted; amended by 0041 (proposed): the export copies, their schedule and the recovery bundles are retired; extends 0028 and supersedes the part of an estate's GitOps ADR-034 §9 that says the issuer never calls OpenBao and a PushSecret copies |
| 0035 | Renamed to sluis | Accepted; its NATS-bucket and sluis:binding rows are moot since 0027 and 0028 removed NATS and sealing (2026-10-04); the kept contract names still stand |
| 0036 | Configuration and policy are immutable per instance; credentials and State are read live | Accepted; amended by 0037; amended by 0071 (immutable per execution environment; AppConfig on AWS); amended by 0041 (proposed): layout v3 becomes v4; refined by 0038; refined by 0075 (a runtime decision kept in State that configuration overrides by naming a generation); follows up 0007 (the deferred deprecation window), refines 0032, amends 0028 and 0031 only where they name SSM paths or configuration documents |
| 0037 | One process everywhere: one Lambda function, one Deployment, one service document | Superseded by 0071 (2026-10-09); was: Accepted; refined by 0038; amends 0036 (three documents and three Deployments → one unified process) |
| 0038 | Estates render their documents through sluis | Accepted; refines 0036 (who writes the documents an instance is started with) and 0037 (one service document, one policy document) |
| 0039 | The issuer generates confidential client secrets | Accepted; amended by 0041 (proposed): the secret and its previous value live at external/oidc/<client>; extends 0038 (what an estate declares for a client) and refines the client table of the policy document |
| 0040 | Agent-class sessions: a longer chain by client class, not by resource | Accepted; amends 0001 (agent-class chains are not held to the installation's absolute limit) and 0033 (its lengthening half is deprecated) |
| 0041 | The secret contract: internal and external, one storage module, keys by purpose | Accepted (2026-10-08). Once carried out it amends 0034 (the export copies, their schedule and the recovery bundles are retired), 0036 (layout v3 becomes v4) and 0039 (where a generated secret and its previous value live). Its companion 0042 moves audit into this repository and decides how audit is installed. |
| 0042 | One repository, one release train: audit moves into sluis and is installed by preset to destinations | Accepted (2026-10-08). Companion of 0041, which decides secrets, storage, keys and deployment shapes for both products. |
| 0043 | Record schema in Protocol Buffers with JSON Schema extension slots | accepted |
| 0044 | Profiles composed from framework profiles, one copy per profile | accepted; the compliance bundle this record first called a preset is a framework profile, and the profiles/ directory, the frameworks: key and the profile package carry that name (a preset is a named bundle of adapter or deployment choices, per truvity/policy decision 0012) |
| 0045 | S3 Object Lock in compliance mode is the record; everything else is a projection | accepted; the object layout is superseded by 0060, and retention is extended by 0065 |
| 0046 | One sink interface at every hop; the queue is invisible | accepted; the delivery modes are superseded by 0054, itself superseded by 0059, which extends this |
| 0047 | Identity tiers and per-purpose pseudonymisation in the split writer | accepted |
| 0048 | Search contract: DNF typed predicates, cursor page object, tail cursor | accepted |
| 0049 | Pluggable authentication and authorization with declarative defaults | accepted |
| 0050 | Hourly signed digest chain and an auditor-run verify command | superseded by 0061 |
| 0051 | Versioning: package per major, major.minor on the record, decoders forever | accepted; refined by 0067 for configuration |
| 0052 | Key providers: local, OpenBAO transit and AWS KMS envelope, behind one interface | accepted; the default is none per 0055 |
| 0053 | One installation per service or product, in that application's namespace | accepted; refined by 0058 |
| 0054 | Two deliveries, and the receiver's acknowledgement means durable | superseded by 0059 |
| 0055 | No pseudonymisation keys by default | accepted; supersedes the default of 0052 |
| 0056 | Lock modes and store tiers: the lock is demanded where a framework demands it | accepted; refined by 0068 (the lock is a property of the install preset's bucket, not a setting of the process) |
| 0057 | Schema identifiers on GitHub Pages, the old ones kept as aliases | accepted; the base moved to the sluis site by 0069 |
| 0058 | Three parts, installed independently; the bucket layout is the contract | accepted; refines 0053 |
| 0059 | Sink durability: the acknowledgement says how durable, and the start-up refuses less | accepted; supersedes 0054 and extends 0046 |
| 0060 | The v1 bucket layout, and v0 is dropped | accepted; supersedes the object layout of 0045 |
| 0061 | Seals: JOSE ES384, chained, per profile, tenant and hour | accepted; supersedes 0050 |
| 0062 | Observe follows the bucket by cursor; notifications only wake it | accepted |
| 0063 | One configuration file, validated against a schema | accepted; refined by 0067; the secret fields it names (<field>Env) are <field>Secret through secrets.source in configuration version 2 (truvity/policy decision 0012; upgrade) |
| 0064 | Contracts are proto and Connect; Lambda RPCs are unary | accepted |
| 0065 | Archive retention: Object Lock compliance as the target, governance first | accepted; extends 0045 and 0056 |
| 0066 | The indexer and the query service are separate processes, under separate database roles | accepted |
| 0067 | Configuration is immutable per instance; credentials and State are read live | accepted; refines 0063 and 0051 for configuration |
| 0068 | Storage is configured per install preset | accepted; refines 0056 and 0065 |
| 0069 | Schema identifiers move to the sluis site, the audit Pages base kept as an alias | Accepted (2026-10-08). A consequence of 0042: audit's repository and its Pages site are retired. Amends 0057. |
| 0070 | Cloudflare tokens and R2 credentials: sluis clones a disabled prototype token | Accepted (2026-10-08). Follows 0014 (a credential is minted only where membership is governed), stores its output under the contract of 0041 and grants it to people and agents as 0040 sets out. |
| 0071 | Least-privilege modules: one function per credential holder, the signer inside the issuer and a binary per module | Accepted (2026-10-09); amended 2026-10-09 by 0072 (points 1, 3, 5, 8, 9 and 11: the signer and the console stay inside sluis-issuer; six binaries and seven functions; restore is a second deployment of the backup binary). Amended 2026-10-12 by 0073 (point 9: a backup is encrypted with age to public recipients, not under a KMS or OpenBao key). Supersedes 0037 (one process everywhere); amends 0036 (immutability moves from the instance to the execution environment, and AWS configuration is delivered through AppConfig); builds on 0026, 0030, 0041 and 0042. The Cloudflare module is the one ADR 0070 (in review) decides. |
| 0072 | Storage layout v5: module first, a table per module, and a one-time migration | Accepted (2026-10-09). Refines point 8 of 0071 (state, secrets, keys and blobs are per module) and amends its points 1, 5, 9 and 11; builds on 0041 (the secret contract) and 0031 (a generic migration tool). |
| 0073 | Backup format 2: one age-encrypted tar.gz per backup | Accepted (2026-10-12). Amends point 9 of 0071 (a backup is encrypted to age recipients, not under a KMS or OpenBao key) and replaces backup format 1 of 0072. |
| 0074 | GoAlert identity reconciler and a Telegram module | Accepted (2026-10-15). Builds on 0071 (one function per credential holder), 0017 (membership only) and 0008. |
| 0075 | Recovery is turned off from the console and turned on by configuration | Accepted (2026-10-18). Refines 0036 (configuration is immutable and State is read live: this is a decision kept in State that the configuration overrides only by naming a new generation). Applies 0027 (the State port keeps the record). |
Audit's records¶
The records numbered 0043 to 0067 are audit's, which had its own series (0001 to 0025) before audit moved into this repository (0042). They were renumbered after sluis's, in order, and are otherwise unchanged apart from the Status and Date lines, which now use the shape the rest of this directory has. A reference to "audit ADR NNNN" in an older changelog entry, issue or comment means the record in this table.
| formerly audit ADR | now | decision |
|---|---|---|
| 0001 | 0043 | Record schema in Protocol Buffers with JSON Schema extension slots |
| 0002 | 0044 | Profiles composed from framework profiles, one copy per profile |
| 0003 | 0045 | S3 Object Lock in compliance mode is the record; everything else is a projection |
| 0004 | 0046 | One sink interface at every hop; the queue is invisible |
| 0005 | 0047 | Identity tiers and per-purpose pseudonymisation in the split writer |
| 0006 | 0048 | Search contract: DNF typed predicates, cursor page object, tail cursor |
| 0007 | 0049 | Pluggable authentication and authorization with declarative defaults |
| 0008 | 0050 | Hourly signed digest chain and an auditor-run verify command |
| 0009 | 0051 | Versioning: package per major, major.minor on the record, decoders forever |
| 0010 | 0052 | Key providers: local, OpenBAO transit and AWS KMS envelope, behind one interface |
| 0011 | 0053 | One installation per service or product, in that application's namespace |
| 0012 | 0054 | Two deliveries, and the receiver's acknowledgement means durable |
| 0013 | 0055 | No pseudonymisation keys by default |
| 0014 | 0056 | Lock modes and store tiers: the lock is demanded where a framework demands it |
| 0015 | 0057 | Schema identifiers on GitHub Pages, the old ones kept as aliases |
| 0016 | 0058 | Three parts, installed independently; the bucket layout is the contract |
| 0017 | 0059 | Sink durability: the acknowledgement says how durable, and the start-up refuses less |
| 0018 | 0060 | The v1 bucket layout, and v0 is dropped |
| 0019 | 0061 | Seals: JOSE ES384, chained, per profile, tenant and hour |
| 0020 | 0062 | Observe follows the bucket by cursor; notifications only wake it |
| 0021 | 0063 | One configuration file, validated against a schema |
| 0022 | 0064 | Contracts are proto and Connect; Lambda RPCs are unary |
| 0023 | 0065 | Archive retention: Object Lock compliance as the target, governance first |
| 0024 | 0066 | The indexer and the query service are separate processes, under separate database roles |
| 0025 | 0067 | Configuration is immutable per instance; credentials and State are read live |
Template¶
Start a new record from this shape. Keep it tight — long enough to make the reasoning checkable, short enough that the next reader finishes it.
# NNNN — <a decision, stated as a decision>
**Status:** Proposed | Accepted | Accepted; amended by [NNNN](NNNN-slug.md) | Accepted; partly superseded by [NNNN](NNNN-slug.md) | Superseded by [NNNN](NNNN-slug.md)
**Date:** YYYY-MM-DD
## Context
The situation that made a decision necessary, and the constraint that
ruled some answers out before the rest were compared.
## Decision
What was decided, stated so a reader could act on it without reading
anything else. Include the shape of the mechanism, not just its name.
## Consequences
What this costs, what it forecloses, and what a relying party or an
operator must now do differently. Say the honest boundary out loud —
the case this decision does not cover — rather than leaving it to be
discovered.
## Alternatives considered
Each one named, with the specific reason it was not chosen. "We didn't
think of it" is a fine thing to be able to write here later; do not
retrofit reasons no one had at the time.
Use refines, extends or amends on the newer record, and add the matching
amended by, refined by or extended by to the older one's Status line only.
The older record's text is never edited.