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

12 KiB
Raw Blame History

id title status date decided_by accepted_at workstream alternatives_considered
ADR-0002 Hosted Trust Service stack: storage, auth, and deployment target accepted 2026-07-29 Bernd 2026-07-29 TREV-WP-0006 (trust-service-implementation)
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.