Maintainer (Bernd) accepted 2026-07-29 with no changes requested. T03-T08 may now build against PostgreSQL + per-Licensor API tokens + railiance-cluster hosting + Python/src/target_revenue reuse.
211 lines
12 KiB
Markdown
211 lines
12 KiB
Markdown
---
|
||
id: ADR-0002
|
||
title: "Hosted Trust Service stack: storage, auth, and deployment target"
|
||
status: accepted
|
||
date: 2026-07-29
|
||
decided_by: Bernd
|
||
accepted_at: "2026-07-29"
|
||
workstream: TREV-WP-0006 (trust-service-implementation)
|
||
alternatives_considered: [Go service, file/git-based ledger store, per-repo-hosted SQLite]
|
||
---
|
||
|
||
# ADR-0002 — Hosted Trust Service Stack (Storage, Auth, Deployment)
|
||
|
||
## Status
|
||
|
||
**Accepted 2026-07-29** by the maintainer (Bernd). Per
|
||
`docs/adr/ADR-0001-stage0-library-stack.md`'s own Revisit trigger, this was
|
||
that separate, later decision — hosting, multi-tenant storage, and
|
||
authentication were explicitly out of ADR-0001's scope and are not
|
||
inherited from it. T03–T08 (`workplans/TREV-WP-0006-trust-service-implementation.md`)
|
||
may now build against this decision.
|
||
|
||
## Scope
|
||
|
||
Three decisions, per T02's task description:
|
||
|
||
1. Storage engine for the append-only Target Ledger at multi-repo scale.
|
||
2. Authentication/authorization model for Phase Registry and Ledger writes.
|
||
3. Hosting/deployment target.
|
||
|
||
This ADR does **not** decide API framework internals beyond what's needed
|
||
to justify the storage/auth/hosting choices, does not draft the actual API
|
||
routes (`specs/TrustServiceProductRequirementsDocument.md` §6 previews
|
||
those, non-binding), and does not resolve the Breach/Compliance Record
|
||
component's ownership gap flagged in that same PRD §4 — that remains a
|
||
separate open item.
|
||
|
||
## Context
|
||
|
||
`specs/TrustServiceProductRequirementsDocument.md` (WP-0006-T01) specifies
|
||
what the hosted service must do: serve multiple Phases/Licensors
|
||
concurrently (TS-FR-8), enforce phase-scoped authenticated writes (TS-FR-3),
|
||
preserve WP-0002's append-only/hash-chained/deterministic-fold guarantees
|
||
under hosting (§3 of that PRD), and publish public facts alongside
|
||
gated confidential evidence (TS-FR-4). The immediate driver is
|
||
`workplans/TREV-WP-0008-governance-and-pilot-rollout.md`'s four target
|
||
product lines (`coulomb-loop`, `net-kingdom`, `helix-forge`, `railiance-*`),
|
||
all within the same Forgejo org — this is an internal-scale multi-tenant
|
||
problem (single-digit to low-double-digit Licensors at pilot stage), not an
|
||
internet-scale one.
|
||
|
||
Existing infrastructure in this org, per the state hub's repo-goal records:
|
||
|
||
- `railiance-infra` provisions and hardens a HostEurope server.
|
||
- `railiance-cluster` installs k3s, Helm, GitOps tooling, monitoring, and
|
||
"essential platform services" on that server, explicitly as reusable
|
||
baseline infrastructure for `coulomb`-org projects — not a repo-specific
|
||
deployment target only `railiance-cluster` itself may use.
|
||
|
||
This means a hosting decision does not start from zero: a Kubernetes
|
||
baseline with GitOps deployment already exists and is the natural fit
|
||
target-revenue's own PRD (T02's task description) named as the first
|
||
candidate to investigate, not assume.
|
||
|
||
## Decision
|
||
|
||
**Storage: PostgreSQL**, one `target_ledger_entries` table partitioned
|
||
logically by `phase_id`, with database-level enforcement — not just
|
||
application-level convention — that settled entries cannot be updated or
|
||
deleted:
|
||
|
||
- the application's database role has `INSERT`/`SELECT` only on this table,
|
||
no `UPDATE`/`DELETE` grant at all;
|
||
- a unique constraint on `(phase_id, entry_id)` and a foreign-key-shaped
|
||
check that `previous_entry_hash` matches the prior entry for that
|
||
`phase_id`, enforced by a trigger, not solely by application code;
|
||
- `phase_manifests` and `extensions` as separate tables, `JSONB` columns for
|
||
the less rigidly-typed fields (`target_basis`, extension contract
|
||
metadata) so schema evolution doesn't require a migration for every new
|
||
optional field.
|
||
|
||
**Authentication: per-Licensor API token for write access, independent from
|
||
per-entry Ed25519 signing.** Two distinct guarantees, not one mechanism
|
||
doing both jobs:
|
||
|
||
- each registered Licensor (one per product line at pilot stage, per
|
||
WP-0008-T01's Licensor-identity question) is issued a bearer token scoped
|
||
to write only within its own `phase_id` namespace — this is service-level
|
||
access control, revocable and rotatable without touching the ledger's
|
||
cryptographic history;
|
||
- every accepted Ledger entry is still signed with the Trust Service
|
||
instance's Ed25519 key over the canonical bytes (TSD §3.2, ADR-0001's
|
||
established crypto), independent of which token authenticated the write —
|
||
this is the integrity guarantee an external verifier checks, and it does
|
||
not depend on trusting the token layer;
|
||
- confidential-evidence read access (TS-FR-4) uses the same token
|
||
infrastructure with a separate, narrower read-grant scope, so a Licensor
|
||
can delegate temporary read access (e.g., to an Enforcement Partner
|
||
investigating an Alleged Violation, `specs/EnforcementNetworkConcept.md`)
|
||
without issuing a write-capable credential.
|
||
|
||
**Hosting: `railiance-cluster`'s existing k3s deployment on the
|
||
`railiance-infra`-provisioned HostEurope server**, packaged as a
|
||
containerized service with a Helm chart following that cluster's existing
|
||
GitOps pattern, rather than provisioning separate infrastructure. PostgreSQL
|
||
runs as a dedicated StatefulSet in the same cluster (or against a shared
|
||
cluster Postgres instance, if `railiance-cluster` already operates one for
|
||
another service — an open item for T03 to confirm at implementation time,
|
||
not assumed here).
|
||
|
||
**Application language: Python**, wrapping `src/target_revenue/` directly
|
||
rather than reimplementing the fold/hashing/validation logic in a second
|
||
language. A thin HTTP layer (a lightweight ASGI framework — the specific
|
||
package is an implementation detail for T03/T04, not fixed by this ADR) adds
|
||
the network boundary, auth, and persistence TS-FR-1–9 require; the pure fold
|
||
and canonical-serialization code that WP-0002 already tested (36 passing
|
||
tests) is called, not rewritten.
|
||
|
||
## Rationale
|
||
|
||
| Requirement | Choice | Why |
|
||
|---|---|---|
|
||
| Append-only enforcement must survive a hosting layer that could accidentally add an update path (TrustServicePRD §3 point 4) | Postgres with revoked `UPDATE`/`DELETE` grants + a trigger | Makes "no mutation of settled entries" a database-enforced fact, not just an API design intention that a future endpoint could quietly violate |
|
||
| Deterministic fold must match between hosted and offline computation (TrustServicePRD TS-NFR-1, WP-0006-T04's stated highest-risk property) | Reuse `src/target_revenue/fold.py` unchanged inside the hosted service | Eliminates the entire class of bug where a second, hand-written fold implementation drifts from the tested one |
|
||
| Multi-tenant isolation (TS-FR-8) must be a security property, not a convention | Per-Licensor token scoped at the database-query level (row-level filtering by `phase_id` ownership), not just at the API-route level | An API-layer-only check can be bypassed by a bug in one route; a query-level scope is harder to forget |
|
||
| Existing infra should be reused, not duplicated, for an internal-scale service | `railiance-cluster`'s k3s + GitOps baseline | That repo's own stated goal is to be reusable "coulomb infrastructure," and this avoids a second hosting decision this org would have to maintain |
|
||
| Ledger integrity must not depend on trusting the hosting/access-control layer | Ed25519 signature over canonical bytes, independent of the API token | Matches TrustServicePRD §3 point 2 (offline verifiability) — a party with an exported ledger and the public key can verify without trusting the token system at all |
|
||
|
||
### Why not a Go service
|
||
|
||
`docs/adr/ADR-0001-stage0-library-stack.md` names Go as fitting "a
|
||
long-lived, standalone, performance-sensitive service." This is not (yet)
|
||
that: pilot-stage throughput is a handful of Phases across four internal
|
||
product lines, not a high-throughput public API. Reusing the
|
||
already-tested Python fold avoids a second implementation of the exact
|
||
logic WP-0002 spent its test suite validating. Revisit if throughput,
|
||
latency, or team composition changes materially post-pilot.
|
||
|
||
### Why not a file/git-based ledger store
|
||
|
||
`workplans/TREV-WP-0008-governance-and-pilot-rollout.md` T05 already
|
||
countenances "an explicit interim manual/git-based ledger process" as an
|
||
acceptable **fallback** if the hosted service isn't ready in time for the
|
||
first pilot Phase. That is a reasonable stopgap precisely because it avoids
|
||
concurrent-writer problems by having only one thing (a human, or a very
|
||
small number of them) append at a time. It is a poor default for the actual
|
||
hosted service once multiple product lines write concurrently: a flat file
|
||
or git-commit-based ledger has no native transactional guarantee against a
|
||
lost-update race between two near-simultaneous append requests, which a
|
||
relational database's row-level locking handles as a solved problem. Keep
|
||
the git-based path available as WP-0008-T05's named fallback; do not build
|
||
the hosted service's primary storage on it.
|
||
|
||
### Why not per-repo-hosted SQLite
|
||
|
||
Would avoid running a shared database service, but reintroduces exactly the
|
||
isolation and concurrent-access problems a single shared Postgres instance
|
||
with per-tenant row scoping already solves, while adding an operational
|
||
question (where does each repo's SQLite file actually live, who backs it
|
||
up) that `railiance-cluster`'s existing platform-services remit is better
|
||
positioned to answer once, centrally.
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
|
||
- No new infrastructure to provision — reuses `railiance-infra`/
|
||
`railiance-cluster`'s already-stated purpose.
|
||
- Ledger integrity (signature + hash chain) is decoupled from access
|
||
control (tokens), so revoking or rotating a Licensor's credentials never
|
||
touches the cryptographic history a third party would verify.
|
||
- Reusing `src/target_revenue/` means WP-0002's 36 tests continue to cover
|
||
the core fold/hash logic inside the hosted service, not just a
|
||
now-unused offline library.
|
||
|
||
### Negative / risks
|
||
|
||
- Single HostEurope server is a single point of failure. Acceptable for
|
||
alpha/beta pilot scale (small, internal, `coulomb`-org-only usage per
|
||
`SCOPE.md` §1's alpha/beta risk-acceptance framing) but must be revisited
|
||
before any external or higher-availability commitment.
|
||
- Database-level append-only enforcement (revoked grants + trigger) must
|
||
actually be tested as part of T04's conformance work, not assumed correct
|
||
because it sounds right on paper — a misconfigured role grant would
|
||
silently reintroduce the exact risk this ADR is designed to close.
|
||
- Whether `railiance-cluster` already runs a shared Postgres instance
|
||
suitable for reuse, or whether this service needs its own, is not
|
||
resolved here and is explicitly left to T03 to confirm before
|
||
implementation, not assumed by this ADR.
|
||
|
||
### Compensating guardrails
|
||
|
||
1. `src/target_revenue/fold.py` and `hashing.py` are imported, not
|
||
reimplemented, inside the hosted service's request handlers.
|
||
2. Database migrations must never grant the application role `UPDATE` or
|
||
`DELETE` on `target_ledger_entries`; any migration that does is a
|
||
defect, not a convenience fix, and should be caught in review.
|
||
3. T04's conformance testing must include an explicit test that attempts
|
||
to update or delete a settled entry through every available access path
|
||
(API, direct DB role) and asserts each is rejected.
|
||
4. T08's hosted conformance suite must assert hosted-computed and
|
||
offline-computed folds agree on the same exported ledger, per
|
||
TrustServicePRD TS-NFR-1.
|
||
|
||
## Revisit trigger
|
||
|
||
Reconsider this decision if: pilot usage moves beyond the four named
|
||
`coulomb`-org product lines to external, higher-throughput, or
|
||
higher-availability requirements; `railiance-cluster`'s k3s baseline is
|
||
retired or materially changed; or T03's implementation discovers the
|
||
single-Postgres-instance assumption doesn't hold operationally.
|