POS Audit-Log Coverage Inventory
Date: 2026-04-17
Status: Discovery complete — no decision
Tracking: #1001
Charter reference: Gateway Integration Charter Section 11 ("Audit log unification") — docs/superpowers/specs/2026-04-13-gateway-integration-charter-design.md
Purpose
Charter Section 11 flagged audit-log unification as a deferred item because gateway has comprehensive audit logging but POS coverage was unverified. This document inventories what POS actually audits today, highlights gaps, and surfaces three candidate paths forward. It is a discovery artifact: no path is chosen here. A follow-up implementation ticket is filed after review.
TL;DR
Scope of scan: the three POS microservices that accept merchant-facing traffic (merchant-api, management-api, terminal-api), the terminal-onboarding service, the Android terminal app, the customer portal, and shared libraries under libs/.
Headline finding: POS has an activity_logs Spanner table with a fully-wired read path (service, repository, two controllers, IAM-gated endpoints, SDK clients in TypeScript and Kotlin) — but the write path is dead code. ActivityLogRepository.insert(...) is defined and exists as a SqlDelight query (insertMerchant in ActivityLog.sq), but no production code ever calls it. Only test fixtures instantiate ActivityLog objects. Merchants can query the UI for an activity log that is always empty.
Count across the inventory below:
| Coverage state | Actions |
|---|---|
| Logged to a durable, structured sink | 0 |
| Logged partially (stdout / INFO / ledger-only) | 13 |
| Not logged at all | 24 |
| Total in-scope actions audited | 37 |
Section 11 of the charter was written assuming POS had some audit coverage that needed mapping. The reality is substantially less coverage than implied: beyond the deny-side IAM decision logs and the per-domain side-effect tables (inventory ledger, point transactions, cash movements, invoice_event) that exist for operational reasons, POS has no audit surface. The activity_logs table is scaffolding, not a working system.
Full detail in Section 3 and Section 5.
1. Scan methodology
- Code search: grep for
audit,Audit,AUDIT_LOG,ActivityLog,@RequiresPermission,@RequireRole,@PreAuthorizeacrossapps/microservices/**,apps/android/**,apps/websites/**,libs/**. - Schema scan: every
.sqfile underapps/specifications/schema/src/main/sqldelight/com/myriad/schema/(60 tables) read for audit-shaped columns (actor_id,user_id,created_by,before,after,old_*,new_*). - Infra scan:
infra/terraform/*.tfforgoogle_logging_project_sink,google_logging_bucket_config, audit-log retention, access-log sinks. - Call-site check: every occurrence of insert-capable repositories and services to verify write paths are actually exercised.
- Charter cross-reference: charter Section 3 source-of-truth table used to scope "mutates merchant-facing state or elevates privileges" actions.
2. Infrastructure state
2.1 Spanner tables that could be audit sinks
| Table | Purpose | Used as audit today? |
|---|---|---|
activity_logs | Generic action log (actor, org, store, entity, details JSONB, IP) | No — insert path is dead code |
inventory_ledger | Per-product quantity deltas with user_id and notes | Yes (operational, side-effect of inventory mutations) |
point_transactions | Loyalty point deltas with idempotency_key and type | Partial — no actor/cashier id captured |
cash_movements | Cash drawer movements with user_id + override_user_id (T9) | Yes (designed with audit in mind — per T9 spec) |
cash_drawer_snapshots | Opening/closing counts with counted_by_user_id + approved_by_user_id (T9) | Yes (T9) |
invoice_event | Current invoice lifecycle/event log: event_type, status transitions, correlation id, payload | Partial — no actor id; scope is invoice lifecycle only |
revoked_terminal_certs | Terminal mTLS revocations, revoked_by VARCHAR | Partial (free-text actor, no UUID FK) |
terminal_certificate_renewals | Cert renewal audit trail with status + error_message | Yes (operational) |
webhook_events | Inbound webhook delivery record | Yes (but inbound-only, not POS action audit) |
onboarding_pipelines | Multi-step onboarding with per-step completed_by | Yes (implicit, per-step) |
waste_log | Product waste with store + org scope | Partial — operational, not security |
All tables are append-mostly, not append-only. None have DB-level REVOKE UPDATE, DELETE grants (charter flags Spanner access control via DDL GRANTs but none are configured for these tables). No row-signing, no hash-chaining, no tamper-evidence beyond the default Spanner write-timestamp columns.
2.2 Structured log sinks
- Logback JSON encoder wired into every Cloud Run microservice via
libs/microservices/security/src/main/resources/com/myriad/shared/logging/cloud-run-json-console.xml. Every log line becomes a JSON object withseverity,logger,message,trace.id, plus MDC fields forfirebase.uid,firebase.email,firebase.tenant,http.request.method,url.path. - Cloud Logging is the default sink (stdout → Cloud Logging ingest via Cloud Run).
- Exclusion filters in
infra/terraform/logging.tf: health checks and staging logs excluded from the default sink. No include-only sink for audit-tagged logs. - No dedicated audit log bucket. No
google_logging_project_sinkdirected at a long-term retention bucket. No BigQuery export, no Pub/Sub fanout. Everything relies on Cloud Logging's default 30-day retention (Cloud Run application logs). GCS bucket-access logs are sunk intopinpointpos-access-logs-<project>with a 90/365-day lifecycle (Nearline/Coldline), but these are object-access logs, not application audit. - Spanner backup retention: 7 days (
infra/terraform/spanner.tf:28). Point-in-time recovery window is Spanner default (1h–7d configurable, not customized).
2.3 IAM decision log (the one genuine structured audit surface)
libs/microservices/iam-engine/src/main/java/com/myriad/shared/iam/IamService.kt emits an INFO line on every checkAccess call with fields {decision, principal(email+orgId), resource, action, reason, context}. This is the only structured log line that meets a standard audit-log schema today.
Caveat: decisions from @RequiresPermission-annotated controllers are logged, but the audit document specified for IAM (docs/private-docs/docs/api/iam-spec.md Section 12) is written as a design intent, not a verified coverage claim. The log message format exists; retention and tamper-evidence claims do not.
2.4 Cloud Audit Logs (GCP-native)
Not configured. No google_project_iam_audit_config resource in Terraform. GCP Data Access audit logs are at the default level (admin-activity only, 400-day retention, free tier). This covers IAM role changes on the GCP project, NOT application-level actions inside our services.
3. Summary inventory table
Grouping is by domain (identity, catalog, payments, retail ops, platform). "Sink" column uses: ACT = activity_logs (dead), IAM = IAM decision log, LG = domain side-effect table (ledger/event log), LOG = free-text INFO log, NONE = no audit.
| # | Action | Surface | Logged? | Sink | Fields captured | Tamper-evident | Gap vs. gateway standard |
|---|---|---|---|---|---|---|---|
| Identity / membership | |||||||
| 1 | Create organization | management-api OrgController.create via OrgService | No | LOG (warn only on failure) | none | No | No actor/before/after; not captured at all on success |
| 2 | Update organization | management-api OrgController.update | No | NONE | none | No | No actor/before/after |
| 3 | Delete organization | management-api OrgController.delete; demo via DemoOrgController | No | NONE | none | No | Irreversible; no record of who deleted or when beyond deleted_at-ish signals |
| 4 | Create store | management-api StoreController; merchant-api StoreController | No | NONE | none | No | No actor |
| 5 | Update store | merchant-api StoreController.update | No | NONE | none | No | No actor/before/after |
| 6 | Delete store | management-api StoreController | No | NONE | none | No | No actor |
| 7 | Invite org member | merchant-api OrgInviteController → InviteService | Partial | LG (invites.created_by) + LOG (warn on email failure) | invite row has created_by FK | No | No record on revoke/resend; no details JSON |
| 8 | Accept invite (identity promotion) | merchant-api InviteService.acceptInvite | Partial | LG (invites.accepted_at) | invite row mutation | No | No record of IP / user-agent / elevation resulting policy |
| 9 | Change org member role | merchant-api OrgMemberController | No | NONE | none | No | Elevates privileges; nothing logged |
| 10 | Remove org member | merchant-api OrgMemberController | No | NONE | none | No | No actor |
| 11 | Invite store member | merchant-api StoreInviteController | Partial | LG (invites.created_by) + LOG | invite row | No | Same as #7 |
| 12 | Change store member role | merchant-api StoreMemberController | No | NONE | none | No | Elevates privileges; nothing logged |
| 13 | Remove store member | merchant-api StoreMemberController | No | NONE | none | No | No actor |
| 14 | IAM role assignment (user-to-role) | merchant-api IamRoleController -> iam_user_roles | No | NONE | none | No | Row is the only evidence; no change history |
| 15 | IAM policy / group / role mutation | merchant-api IamRoleController / IamGroupController / IamCatalogController | Partial | LG (tables themselves; IAM DENY/PERMIT lines on check) | IAM decision log on read | No | No mutation log — only access-decision log on reads |
| 16 | Staff invite / deactivation (support tenant) | management-api SupportController, SupportUserInviteService | Partial | LOG (staff invite, welcome email failures) | free-text email | No | No actor id, no structured record |
| 17 | Update PIN (cashier PIN reset) | merchant-api UserPinController | No | NONE | none | No | Security-sensitive change, no record |
| 18 | Firebase user create / reset | merchant-api FirebaseAdminService, LocalFirebaseAdminService | No | LOG (fire-and-forget debug) | none | No | No actor on reset; Firebase's own audit is separate |
| Customer PII | |||||||
| 19 | Create customer (portal) | merchant-api CustomerController → CustomerService.create | No | NONE | none | No | No record of who entered PII |
| 20 | Update customer (portal, PII edit) | merchant-api CustomerController.update → customers.update | No | NONE | none | No | No before/after; no actor |
| 21 | Update customer license / DOB (age verification) | terminal-api CustomerController (terminal) + compliance flow | No | NONE | none | No | High-compliance PII; nothing logged |
| 22 | Delete customer | merchant-api CustomerController.delete | No | NONE | none | No | Irreversible; no record |
| 23 | Loyalty points adjustment (cashier override) | merchant-api LoyaltyController / terminal-api LoyaltyRedemptionService | Partial | LG (point_transactions) | delta, customer_id, type, idempotency_key | No | No actor id on point_transactions (only org_id) |
| 24 | Consent change (SMS / marketing) | merchant-api ConsentController | Partial | LG (sms_consents row) | consent row has state | No | No actor id |
| Catalog / pricing | |||||||
| 25 | Create / update / delete product | merchant-api ProductController → ProductService | No | NONE | none | No | No actor/before/after |
| 26 | Bulk import products | merchant-api BulkImportController → BulkImportService | No | NONE | none | No | High-impact mass change; nothing logged |
| 27 | Inventory mutation | merchant-api InventoryController → ledger insert | Yes (ops) | LG (inventory_ledger) | store, org, product, Δqty, user_id, notes | No | Closest thing to a working audit; per-product only |
| 28 | Create/update supplier | merchant-api SupplierController | No | NONE | none | No | No actor |
| 29 | Create/update promotion / discount | merchant-api PromotionController / DiscountController | No | NONE | none | No | No actor; charter T3 will move this to gateway |
| 30 | Tax config mutation | merchant-api TaxConfigController / TaxConfigService | No | NONE | none | No | No actor; charter T2/T2b moves this to gateway |
| Payments / retail ops | |||||||
| 31 | Refund issuance (POS terminal origin) | terminal-api RefundController → RefundService | Partial | LOG (gateway card refund log line) | originalTxnId, gatewayTxnId, amountCents | No | Missing: cashier id, manager-override id, reason, before/after of original row |
| 32 | Return / credit note (portal origin) | merchant-api ReturnController → ReturnService | Partial | LG (returns row) + LOG | returns.user_id, notes, restocked | No | No activity_logs entry; no manager-override capture |
| 33 | Shift close / variance acknowledgment | merchant-api ShiftController / ShiftService | Partial | LG (shifts_actual rows) | user_id, clock_in/out, break_minutes | No | Variance acknowledgment depends on T9 cash_drawer_snapshots.approved_by_user_id |
| 34 | Cash movement (deposit / drop / manager override) | T9 — cash_movements table | Yes (ops) | LG (cash_movements) | drawer, shift, movement_type, amount, reason, user_id, override_user_id | No | Best coverage of any action; still not append-only |
| Platform / gateway binding / terminal | |||||||
| 35 | Gateway store auth binding (create/update/delete) | management-api GatewayStoreAuthController + GatewayWebhookController | Partial | LOG (INFO on StoreAuthorized/StoreRevoked) | gatewayMerchantId, oauthClientId, scopes | No | No actor on portal-originated binding change; webhook path logs only |
| 36 | Terminal provisioning / mTLS cert issuance | terminal-onboarding OnboardingService | Partial | LG (terminals, terminal_onboarding_tokens, terminal_certificate_renewals) + LOG | cert metadata | No | No actor on renewal trigger |
| 37 | mTLS cert revocation | management-api RevokedCertController → revoked_terminal_certs | Partial | LG (revoked_terminal_certs.revoked_by VARCHAR) + LOG | revoked_by free-text | No | revoked_by is a string, not a FK to merchant_users; nothing verifies it |
3.1 Actions considered but not counted
- Gateway transaction sync (POS → gateway) — charter-side concept. Sync itself isn't a merchant-facing mutation; the underlying in-store sale is captured in
transactionsbuttransactionshave no audit-shaped columns (the cashier id lives on the childtransaction_items.user_idfor line-level granularity, not on the transaction). - Webhook delivery (gateway → POS) —
webhook_eventsis captured but it's ingest, not POS mutation. - Reads — the ActivityLog list/read endpoints, report endpoints, and
@RequiresPermission(action="read")paths are out of scope per the ticket ("mutates merchant-facing state or elevates privileges").
4. By-domain commentary
4.1 Identity / membership
Covered: invite rows carry created_by and accepted_at. The IAM decision log emits an INFO line per access check (PERMIT + DENY), which at least tells you who tried to do what even when the action itself isn't audited.
Gaps: role assignments (iam_user_roles), role/policy/group mutations, org and store creation, and member role changes have no audit trail whatsoever — the row is the only evidence the change happened, and the row has no actor. Deletes are irreversible and leave nothing behind.
This is the highest-severity cluster. Actions #9, #10, #12, #13, and #14 all elevate privileges and none produce any audit record.
4.2 Customer PII
Covered: nothing usefully. customers.updated_at is touched on each mutation but no actor is captured.
Gaps: create, update, and delete of customer records — including license number, date of birth, phone, email — are completely unaudited. For compliance purposes (age verification / tobacco / alcohol) this is a material hole: a changed license_number or date_of_birth cannot be attributed to a specific cashier or admin.
Loyalty points have the best coverage of the customer-domain actions via point_transactions, but point_transactions captures org_id — not the cashier who performed the adjustment. Manual cashier overrides of loyalty balances are indistinguishable from routine accruals at the audit layer.
4.3 Catalog / pricing
Covered: inventory mutations via inventory_ledger (user_id + notes).
Gaps: product create / update / delete, price changes, promotion / discount create, and tax config mutation have no audit. Bulk import is the most alarming — it is the single biggest-blast-radius mutation in the system and produces no record of who ran it or what changed.
Note that charter Section 3 targets much of this moving to the gateway (T2 / T2b tax, T3 discounts, T6 products). Once those migrations land, the audit question for those concepts transfers to the gateway. The POS-owned residue is inventory (ledger exists, adequate) and suppliers (no audit).
4.4 Payments / retail ops
Covered: refunds log an INFO line with amounts and gateway txn ids; cash movements (T9) capture actor + override actor; shift actuals capture user_id.
Gaps: refunds miss the cashier and any manager-override identity — the INFO log line is stdout, not queryable, and not tamper-evident. Shift close variance acknowledgment is captured iff T9 fully lands (cash_drawer_snapshots.approved_by_user_id). Manager override on any flow other than cash is not captured at all — there is no generic managerOverrideUserId or overrideReason column on transactions, returns, or any loyalty path.
4.5 Platform / gateway binding / terminal
Covered: gateway store auth transitions log INFO lines on webhook receipt (StoreAuthorized, StoreRevoked). Cert renewals live in terminal_certificate_renewals with status + error_message. Cert revocations live in revoked_terminal_certs.
Gaps: revoked_terminal_certs.revoked_by is VARCHAR(255) — free-text with no FK to merchant_users or support_users, so "who revoked this cert" is whatever string the caller chose to pass. No actor is captured on the portal-originated gateway-binding mutation path (only the webhook-side is logged, and even then not as a structured audit event).
5. Gap summary
Ranked by severity (compliance + forensics + operational impact):
| Rank | Gap | Severity | Why |
|---|---|---|---|
| 1 | Role / permission changes (IAM user-role, group, policy mutations) have no audit trail | Critical | Privilege escalation is the single attack that most matters; charter Principle 2 makes "POS employees never get gateway access" a load-bearing claim that cannot be enforced post-incident without this |
| 2 | Customer PII edits (license, DOB, phone) unaudited | High | Age-verification compliance (TTB / state tobacco boards) requires attribution to a specific employee for ID-check disputes |
| 3 | Refund issuance lacks structured actor + manager-override + reason | High | Refund fraud is the standard loss-prevention case at POS; INFO-line in Cloud Logging is not queryable by merchant auditors |
| 4 | activity_logs table is dead code — write path exists only in tests | High | Existing merchants can query for their activity log and see it empty; false sense of coverage; shipped SDK methods return nothing |
| 5 | Bulk product import unaudited | High | Highest-blast-radius mutation; a single call can change thousands of prices |
| 6 | Gateway store auth mutations on portal side (non-webhook) unaudited | Medium | Gateway-binding changes redirect revenue; merchant needs an auditable record of who authorized it |
| 7 | revoked_terminal_certs.revoked_by is free-text with no referential integrity | Medium | "Who revoked this terminal cert" is whatever string a caller passed; cannot be trusted |
| 8 | Delete actions across the board (org, store, customer, member) produce no record | Medium | Irreversible, no attribution; any delete is an instant evidence-wipe |
| 9 | No tamper-evidence — nothing is append-only, no row-signing, no hash-chain | Medium | Even where a row is written with actor id, an org admin or rogue operator can edit it directly via Spanner once IAM permits them |
| 10 | Cloud Logging retention is 30 days (default) for all stdout audit-like logs | Medium | Forensic window too short for typical investigation timeline; no long-term archive |
| 11 | Product / price / promotion / tax / supplier changes unaudited | Medium | Most of these migrate to gateway (T2/T2b/T3/T6); exposure is transitional, not permanent |
| 12 | point_transactions captures no actor | Medium | Cashier override on loyalty cannot be distinguished from routine accrual |
| 13 | No dedicated audit-log Cloud Logging sink → BigQuery or GCS for long-term retention | Low | Easy to add infrastructure-wise; not load-bearing until #4 above is fixed |
6. Three candidate paths
The ticket asks for paths, not a decision. All three are presented; trade-offs are surfaced; nothing is chosen.
Path A — Fill POS gaps locally
Shape. Wire the existing activity_logs table into a mandatory write path at every mutation. Add a small AuditLogger component in libs/microservices/audit/ (new library) with a fluent API:
auditLogger.actor(authUser).action("customer.update").entity("customer", customerId)
.before(oldRow).after(newRow).context(ctx).record();
Harden the table: add prev_hash VARCHAR(64) for per-org hash-chaining, signature VARCHAR(512) using an ES256 key from KMS, enforce DDL-level REVOKE UPDATE, DELETE on activity_logs for the microservice roles (Spanner DDL GRANT/REVOKE pattern — already called out in the project CLAUDE.md). Add a dedicated Cloud Logging sink to GCS Coldline with 7-year retention and lifecycle → archive.
Rewire the write side by either (a) ArchUnit-style "every mutating service method must call AuditLogger" enforcement plus an AOP aspect that wraps @RequiresPermission(action="create"|"update"|"delete"), or (b) explicit call-sites per action. Option (a) is lower burden at write time, higher magic; option (b) is verbose but reviewable.
Pros.
- Self-contained — no coordination with gateway team.
- Table and SDK clients already ship; only the write path and hardening are new.
- Schema is flexible (JSONB
details), accommodates every action type without per-action tables. - Tamper-evidence and retention are under POS control.
Cons.
- Duplicates capability gateway already has. Charter Principle 1 (gateway-first directionality) argues against it: a gateway-only merchant will ask about audit logs and get them from gateway; now we have two systems.
- Retroactively reliable — every new mutation in POS has to remember to call the logger, ArchUnit or otherwise. Gateway's audit is instrumented at the controller/request layer, which is more uniform.
- Row-signing + per-org hash-chain is non-trivial to operate correctly (key rotation, verification tooling). Risk of shipping tamper-evidence theater.
- Does nothing for actions that cross the boundary (refunds that originate at POS but settle on gateway — which side owns the audit? Currently the charter says split-by-origin, which means two partial records instead of one whole one).
Path B — Gateway-proxy
Shape. Every POS mutation emits an audit event to the gateway's existing audit surface via the gateway SDK. A per-service gateway/audit/ package wraps com.myriad.gateway.audit.* (following Charter Rule A). The audit event carries {origin: "pos", pos_service, action, actor_user_id, actor_tenant, entity_type, entity_id, before, after, context}.
Gateway is the retention root; gateway's existing tamper-evidence (whatever it is) applies. POS retains nothing locally — the activity_logs table is deprecated, and the existing read-side controllers become proxies to gatewayAuditApi.listForOrg(...).
For POS actions on entities that have no gateway counterpart (shifts, cash movements, loyalty), we either (a) still emit to gateway with a POS-only entity type, or (b) fall back to Path A for those specific entities.
Pros.
- Aligns with Charter Principle 1 — gateway owns mainstream capabilities.
- Single source of truth — merchants and internal staff look in one place.
- Gateway's existing tamper-evidence and retention investment is reused.
- Uniform story for cross-origin events like refunds: one record, on the authoritative side.
Cons.
- Requires a gateway-side audit API that accepts externally-sourced events and preserves them with attribution — tracking: TBD (no gateway issue filed). Coordination overhead with gateway team, unknown API shape today.
- Latency / availability coupling: a gateway outage means POS mutations run without audit, or fail-closed. Fail-closed breaks the POS (cashiers can't sell); fail-open defeats the audit purpose. Needs a local durable queue.
- POS-only entities (shifts, cash drawers, loyalty) look awkward in a gateway-centric audit model.
- Per Charter Rule C,
terminal-onboardingandtx-bundlerare not authorized to consume gateway SDK — but terminal-onboarding is the only place cert operations happen. Either we carve an exception (needs charter update) or we relay viamanagement-api.
Path C — Unified surface
Shape. Stand up a new service (or augment management-api) as the audit-log aggregator. Both POS and gateway write to it; neither reads their own local audit table as the source of truth. The aggregator fronts a single query API for merchant-portal, support-portal, and regulators.
Storage is gateway-owned (leveraging gateway's existing infrastructure) OR a new Spanner DB under POS ownership OR an external log analytics platform (BigQuery, Datadog, Splunk). All three sub-options are possible; the audit service abstracts them.
Pros.
- Future-proof — appointments (Issue #866), storefront (Issue #869), and any new cross-product feature has one audit surface from day one.
- Clean separation: audit writing is a cross-cutting concern, not a gateway feature or a POS feature.
- Can host tamper-evidence (hash chain / signed rows / WORM bucket) in one hardened place.
Cons.
- Largest scope by far; takes the audit question from "ship in a quarter" to "ship in a year".
- New service to operate; another piece of infra to run 24×7.
- Does not solve the POS-today problem — POS still has nothing while the unified surface is being built.
- Violates Charter Principle 1 (gateway-first) in a subtle way: if gateway already has audit and we're building a separate aggregator, we're splitting a capability gateway owns.
- Risk of becoming yet another half-built table: the
activity_logslesson is that scaffolding is cheap and write-paths are expensive.
Comparison at a glance
| Factor | A — Local fill | B — Gateway-proxy | C — Unified surface |
|---|---|---|---|
| Time to first value | Weeks | Quarters (gateway API needed) | Year+ |
| Charter alignment | Against (duplicates gateway) | With (Principle 1) | Neutral |
| Coordination with gateway team | None | Heavy | Heavy |
| Handles cross-origin events (refunds) | Poorly | Naturally | Naturally |
| Handles POS-only events (shifts, cash, loyalty) | Naturally | Awkwardly | Naturally |
| Tamper-evidence surface | POS builds | Gateway reuses | Unified builds once |
| Retention cost | POS budgets | Gateway budgets | Shared budget, new line item |
| Risk of scaffolding-without-writes (the current failure mode) | Low — explicit call-sites | Low — SDK typed | Medium — more moving parts |
7. Recommended decision inputs
Not a decision — the ticket explicitly defers it. But the information the decision needs:
- What does gateway's audit surface actually look like today? The charter says "comprehensive" — we need the actual API shape, tamper-evidence implementation, retention policy, and query interface. Path B and C both depend on this.
- How much of the POS mutation surface is migrating to gateway anyway? Per charter T2/T2b/T3/T5/T6/T12, a large fraction of the "not logged" actions in Section 3 above transfers to gateway regardless. The POS residue after charter Part 2 lands is smaller than it looks today.
- Regulatory baseline. PCI, age-verification (TTB / state ABC), and any state-level POS transparency rules — do any of them set a hard retention floor or tamper-evidence requirement? This dictates whether Path A's GCS Coldline 7-year approach is overkill or insufficient.
- Tolerable outage semantics. Fail-open vs fail-closed for audit in the hot path — only relevant for Path B.
8. Out of scope for this ticket
- Gateway's own audit implementation review — out of repo.
- Retroactive audit of historical data — this inventory is current-state.
- Non-merchant-facing mutations (internal ops, batch jobs, scheduled cleanups) — not in scope per ticket.
- Android app cashier-override UI audit — no evidence any manager-override UI exists today beyond cash-drawer and T9.
- Redaction / right-to-be-forgotten on the audit log — separate concern.
9. References
- Gateway Integration Charter, Section 11 —
docs/superpowers/specs/2026-04-13-gateway-integration-charter-design.md - IAM Spec, Section 12 (Audit Logging)
- Security Audit — Phases 1 & 2
- Security Remediation Report
- Schema:
apps/specifications/schema/src/main/sqldelight/com/myriad/schema/ActivityLog.sq - Service:
apps/microservices/merchant-api/src/main/java/com/myriad/merchant_api/service/ActivityLogService.kt - Repository:
apps/microservices/merchant-api/src/main/java/com/myriad/merchant_api/repository/ActivityLogRepository.kt - Logging config:
libs/microservices/security/src/main/resources/com/myriad/shared/logging/cloud-run-json-console.xml - Terraform (what's missing):
infra/terraform/logging.tf