# ACDL Tiered Audit Ledger Design (REQ-20) > **Status:** design authored in Phase 07 (milestone v1.1); the > hash-chain + DynamoDB-outbox path is **shipped + production since > v1.8**. The S3 Object Lock + JWS + async worker + DLQ + daily > checkpoints build-out is **deferred to a future milestone (D-083)** — > it requires non-offline-testable AWS infrastructure (Object Lock > bucket, KMS signing key, SQS DLQ, Lambda worker) and is not in v1.9. The audit stream is the platform's tamper-evident record of every delivery action. The vision's "Audit truth lives outside the repository" bet [1] and "Not a mutable audit log" anti-goal [1] are the binding constraints. Version-control history does not satisfy regulatory evidence; the ledger is the source of truth. ## Three tiers - **Cold tier (source of truth):** S3 with **Object Lock in compliance mode**, **7-year retention** (ARCHITECTURE.md §9). No one — including root — can delete or overwrite until retention expires. The regulatory record. **Deferred to a future milestone (D-083).** - **Hot tier (query index):** the `acdl-evidence` audit repo (unchanged from the v1.0 demo). Not part of the chain; a queryable mirror the evidence UI (`evidence-ui/index.html`) reads. Lightweight attestation linkage lives in the repo; the regulatory event body lives in S3. - **Outbox (write path):** DynamoDB, **RPO = 0** (synchronous write before contract submission ack). Single-region in v1 (`us-east-1`). **Shipped + production since v1.8.** ## Shipped scope (D-041) — production since v1.8 - **DynamoDB outbox:** table `acdl-outbox`, `PAY_PER_REQUEST` (D-044), PK `contractId`, SK `eventType#eventTs`, TTL `expire_at` = now + 365d (1-year storage per ARCHITECTURE.md §8). - **`prev_event_hash` chain:** SHA-256 over canonical JSON (`json.dumps(event, sort_keys=True, separators=(",", ":"))`), lifted from the v1.0 demo's `evidence_writer.py`. Auto-genesis: first event has `prev_hash="GENESIS"`. - **Synchronous write** via boto3 `put_item` (strong-consistent by default). No separate async worker / DLQ in v1.9 (RTO = workflow re-run). - **Mirror to `acdl-evidence`:** unchanged from v1.0 — the finalize step commits `audit.json` to the evidence repo (the hot tier). - **Evidence event shape:** `{seq, ts, stage, event, prev_hash, hash, contractId, environment, stack, score, band}`. ## Deferred to a future milestone (D-083) The following build-out was authored as design in Phase 07 and is **not in v1.9**. It requires AWS infrastructure that cannot be exercised offline (Object Lock bucket, KMS signing key, SQS DLQ, Lambda worker) and is deferred to a future milestone. The hash-chain + DynamoDB-outbox path above remains the v1.9 production audit record. - **S3 Object Lock:** bucket `acdl-evidence-lock-`, Object Lock enabled at creation, compliance mode, 7-yr retention (`RetainUntilDate` = now + 7y). The outbox→S3 path is an async worker that reads from the outbox and writes to Object Lock. - **JWS detached signature (RFC 7515):** the event payload is canonical-JSON-serialized, SHA-256 hashed, signed with a private key; the signature is stored *detached* alongside the payload. Signing key = **platform-level KMS key** (not per-contract — a per-contract key would explode the key-management surface), rotated **quarterly**. The `jws` field is added to the event shape when this ships. - **Async worker + DLQ:** a Lambda (or a forge Actions scheduled workflow) reads the outbox, writes to S3 Object Lock, signs with KMS. DLQ = an SQS dead-letter queue for failed writes. RTO = DLQ replay. - **Daily checkpoints (§9):** a daily job reads the last event hash and writes a "checkpoint" event to the ledger (+ optionally to a public notarization service). ## JWS vs chain — orthogonality note The `prev_event_hash` chain gives ordering/tamper-evidence *within* the log (a deleted event breaks the chain visibly); JWS gives authenticity *per event* (a forged event is detectable without re-reading the whole chain). The chain is shipped (v1.8+); JWS is deferred (D-083). Together they cover both integrity properties the vision's "Not a mutable audit log" anti-goal requires. ## Outbox item shape (shipped + deferred fields marked) - PK `contractId` (UUID). - SK `eventType#eventTs` (e.g. `POLICY_CHECKED#2026-07-21T12:00:00Z`). - `payload` (the event body — hash-chained in v1.8+; JWS-signed when D-083 ships). - `prev_event_hash` (chain link; `GENESIS` for the first event). - `hash` (this event's SHA-256 over canonical JSON). - `approver_qa` (CI username of the QA approver; populated on qa-promotion by v1.9's `hitl_gates.attest` — D-042). - `approver_prod` (SRE username; populated on prod-promotion by v1.9's `hitl_gates.attest`). - `approver_dr` (SRE username; populated on dr-promotion by v1.9's `hitl_gates.attest`). - `environment`, `stack`, `score`, `band`. - `expire_at` (TTL = now + 365d). - **Deferred (D-083):** `jws` (detached signature), `checkpoint_ref`. ## RPO / RTO table | Phase | RPO | RTO | |-------|-----|-----| | v1.8+ (production, shipped) | 0 (sync outbox write) | workflow re-run | | Future milestone (D-083) | 0 (sync outbox) | async worker DLQ replay | ## Decision trail - **D-041** — shipped scope = hash chain + outbox write; Object Lock + JWS + worker + DLQ are deferred (D-083). - **D-044** — outbox mode `PAY_PER_REQUEST`; PK/SK; TTL `expire_at` = now + 365d; no separate async worker in v1.9. - **D-042** — approver identities (`approver_qa`, `approver_prod`, `approver_dr`) live in the outbox; the separation-of-duties check (`core/separation_of_duties.py`) reads `approver_qa` and compares to the prod-dispatch CI actor. v1.9's `hitl_gates.attest` populates these attributes. - **D-083** (v1.9) — S3 Object Lock + JWS + async worker + DLQ + daily checkpoints deferred to a future milestone. Requires non-offline- testable AWS infra.