Skip to main content
GUIDE · COMPLIANCE9 min read

Audit chain: enable and verify

Every API key rotation, RTBF request, destination pause, and AI Operator action is written to a tamper-evident hash-chained log. This guide shows compliance officers and platform engineers how to enable the chain, verify it on demand, and interpret what events are captured.

Phase 6.4

What the audit chain is

The audit chain is a per-merchant append-only log. Every time an auditable action occurs — a configuration change, a credential rotation, a data residency update, an AI Operator auto-action — a new row is inserted into audit_events. The row stores a hash that includes the prior row's hash, making retroactive edits detectable without consulting an external oracle.

The hash formula is: SHA-256(prev_hash || material), where material is the JSON-serialized event body (event_type, resource_id, actor, timestamp, payload). The first event in any chain uses null as prev_hash — its hash is the genesis block. Concurrent inserts from multiple workers are serialized via pg_advisory_xact_lock so the chain never has a gap or reorder.

-- audit_events · simplified schema
CREATE TABLE audit_events (
  event_id      TEXT    PRIMARY KEY,   -- ae_9a2f1
  merchant_id    TEXT    NOT NULL,
  prev_hash      TEXT,                 -- null for genesis
  hash           TEXT    NOT NULL,     -- SHA-256(prev_hash || material)
  event_type     TEXT    NOT NULL,
  resource_id    TEXT,
  actor          TEXT    NOT NULL,
  timestamp      TIMESTAMPTZ NOT NULL,
  payload        JSONB   NOT NULL,
  created_at     TIMESTAMPTZ DEFAULT now()
);

The insert trigger fires on every auditable action. Because the advisory lock ensures only one insert commits at a time per merchant_id, prev_hash always points to the real last event — no fork, no race, no gap.

Verification

How to verify the chain

Chain verification recomputes SHA-256(prev_hash || material) for every event in sequence and compares the result to the stored hash. A match means the chain is unbroken. Any divergence flags the chain as compromised.

There are two verification paths: the API and the dashboard UI. Both are read-only and do not modify state.

API: GET /v1/audit/verify

curl -H "Authorization: Bearer $TRACKLAYER_API_KEY" \
  "https://api.tracklayer.io/v1/audit/verify?merchant_id=m_abc123"

# Response
{
  "merchant_id": "m_abc123",
  "chain_status": "valid",
  "total_events": 14832,
  "break_count": 0,
  "first_event": "ae_9a2f1",
  "last_event": "ae_9b4e3",
  "last_verified_at": "2026-04-23T14:02:11Z"
}

# Chain broken — example
{
  "merchant_id": "m_abc123",
  "chain_status": "broken",
  "total_events": 14832,
  "break_count": 2,
  "first_event": "ae_9a2f1",
  "last_event": "ae_9b4e3",
  "last_verified_at": "2026-04-23T14:02:11Z",
  "broken_events": ["ae_9b1d9", "ae_9b2da"]
}

Dashboard: /security/audit-verify

Open Settings → Compliance → Audit Chain. Click “Verify chain” to run the check and display the visual chain view. The UI shows the latest 20 events, chain status badge, total event count, break count, and a timestamp of last successful verification.

The WC (WebCargo) subscription example: a compliance officer verifying the WC merchant chain sees 14,832 events spanning 11 months, zero breaks, and a last_verified_at within the last hour. That is the evidence package for a SOC2 audit on the audit trail control.

Coverage

What gets logged

The chain captures every significant configuration and identity event. Below is the full event type inventory:

api_key_created
api_key_rotated
api_key_revoked
webhook_secret_rotated
rtbf_request_initiated
rtbf_request_completed
plan_upgrade
plan_downgrade
data_residency_changed
destination_paused
destination_resumed
schema_change
consent_policy_updated
ai_operator_auto_action
advisory_lock_enabled
advisory_lock_disabled

All Phase 9.2 AI Operator auto-actions use the same event type (ai_operator_auto_action) with the full action context in payload — what was detected, what was decided, what was applied, and the operator confidence score.

Wave 9 P9.3 audit search UI lets you filter the audit log by event_type, actor, date range, and resource_id. The search is surfaced at /security/audit-search. Results are paginated and exportable as CSV for auditor submission.

R2 Export

Weekly R2 export for offline retention

When your CI token has R2 bucket permissions, TrackLayer exports all audit_events for each merchant to your private R2 bucket every Sunday at 03:00 UTC. The export is gzip-compressed JSON with full chain material — event_id, prev_hash, hash, material, timestamp — so you can re-run verification against the offline copy at any point.

# R2 export path pattern
tracklayer-audit-{account}/backup/YYYY/MM/DD/audit-events-{merchant_id}-{date}.json.gz

# Example
tracklayer-audit-abc123/backup/2026/04/19/audit-events-m_abc123-2026-04-19.json.gz

# Export metadata in the JSON envelope
{
  "merchant_id": "m_abc123",
  "exported_at": "2026-04-19T03:00:11Z",
  "export_id": "exp_7f3b2a",
  "events": [
    { "event_id": "ae_9a2f1", "prev_hash": null,   "hash": "h8f3...", ... },
    { "event_id": "ae_9b3e2", "prev_hash": "h8f3...", "hash": "ha1c...", ... }
  ]
}

Retention is 90 days by default. You can configure longer retention via your R2 lifecycle rules. The Phase 7 follow-up will add configurable export frequency and custom prefixes.

To enable, go to Settings → Compliance → Audit Export. Paste your R2 bucket path and save. TrackLayer validates write access before activating the schedule.

Backfill

Pre-trigger rows and the Phase 7.6 backfill

Audit_events rows that were created before the Phase 6.4 insert trigger was deployed do not have hash chain entries. To give compliance auditors a complete chain, the Phase 7.6 backfill Durable Object iterates all existing audit_events and computes retroactive hash entries for them.

The backfill writes a synthetic genesis event and then computes prev_hash chains for all pre-existing rows. A backfill_completed event is appended after the backfill finishes, marking the import boundary. This event has event_type backfill_completed and payload.import_id for auditability.

# Backfill completion event — written after Phase 7.6 DO finishes
{
  "event_type": "backfill_completed",
  "resource_id": "audit_events",
  "actor": "system/backfill_do",
  "timestamp": "2026-04-01T04:12:08Z",
  "payload": {
    "import_id": "bf_4e7c1",
    "rows_imported": 8834,
    "chain_from": "ae_9a2f1",
    "chain_to": "ae_9f0ab",
    "backfill_triggered_by": "Phase 7.6 DO"
  }
}

You can verify the backfill boundary by checking for the backfill_completed event with the matching import_id in the audit log.

SOC2

Tying the audit chain to SOC2 evidence collection

SOC2 Type II requires documented evidence of operating effectiveness. The audit chain contributes to the following trust service criteria:

  • CC7.2 — System operations:audit_events proves which configuration changes occurred, who initiated them, and when — without relying on a team member's memory or a Slack thread.
  • CC7.4 — Change management: every schema change, destination pause, and credential rotation is an auditable event in the chain. SOC2 auditors can query the chain for a date range and see the full change log.
  • CC6.1 — Logical access: api_key_created, api_key_rotated, and api_key_revoked events document identity lifecycle. If an access review is needed, the chain provides the authoritative record.
  • A1.2 — Availability: destination_pause and destination_resumed events document uptime-affecting actions. Cross-reference with your incident log to prove SLA continuity.

To prepare for a SOC2 audit: run GET /v1/audit/verify for the full observation window, download the weekly R2 export for the period, and share the verify response (chain_status: valid) with your auditor as the audit trail control evidence.

FAQ

FAQ

When does the audit chain start recording?

The insert trigger is deployed with Phase 6.4. All new events from that point are chained. Rows created before the trigger are backfilled by the Phase 7.6 Durable Object — you will see a backfill event in the chain marking the import timestamp.

Can I modify or delete an audit event?

No. The audit_events table is write-once. The trigger fires on INSERT only. UPDATE and DELETE operations are rejected at the database level. If advisory_lock_enabled is true on the merchant row, even system_worker cannot append without the advisory key.

What happens if the verify endpoint returns break_count > 0?

A break_count > 0 means stored_hash !== SHA-256(prev_hash || material) for at least one event. This indicates either a database integrity issue or a tampering attempt. Contact TrackLayer support immediately and do not clear the events. The platform will investigate the chain segment.

How does the weekly R2 export help SOC2?

SOC2 Type II requires evidence of operating effectiveness over a period. The weekly export gives your auditor an immutable offline copy of the chain. You can re-run verification against the exported JSON at any point, proving the chain was unbroken during the audit window.

Does the AI Operator auto-action use the same chain?

Yes. All Phase 9.2 AI Operator actions — auto-destination pausing, suggested schema fixes, anomaly resolutions — are written to audit_events with event_type ai_operator_auto_action and the full action context in the payload.

How do I give my auditor access?

Create a read-only team member with the Viewer role and scope it to the audit log only. The auditor can use the GET /v1/audit/verify endpoint or the dashboard verify UI. They cannot append, modify, or delete events.

Continue

Next reads

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.

We use essential cookies to keep the site secure and functional. Analytics and third-party tags run only with your consent. See our Cookie Policy.