Skip to content

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.