target-revenue/docs/adr/ADR-0002-hosted-trust-service-stack.md
tegwick d35820365b Accept ADR-0002: hosted Trust Service stack (WP-0006-T02)
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.
2026-07-29 20:57:38 +02:00

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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. T03T08 (`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-19 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.