Skip to main content

The event chain

Every event QAudit stores is linked to the one before it, forming a hash chain per tenant. The chain is the cryptographic backbone of the entire platform: every proof QAudit can make — that no event was altered, silently dropped, or inserted after the fact — ultimately rests on it.

When the Audit Gateway stores a new event, it computes:

chain_link = SHA-256(previous_signature ‖ previous_event_id ‖ new_event_id)

This hash is included in the event's signature input alongside the canonical payload and receipt timestamp — tying each event's signature to the one before it.

Integrity guarantees

Why the chain matters

The chain alone — without any signing — already makes structural tampering detectable. Consider an attacker who can write to the event store but does not have the signing key:

  • Delete an event — the next event's chain link would reference a predecessor that no longer exists. The chain breaks at that point.
  • Insert a fabricated event — the inserted event cannot carry a valid signature (the signing key is never in the store), so any inspection reveals the forgery.
  • Reorder events — the chain link encodes the identity of the predecessor; swapping two events breaks both links.

The chain alone is not enough. A more careful attacker could delete events and patch the following event's chain pointer to skip over the gap — recomputing a new chain link hash is cheap and requires no secret. A chain walk would find no broken pointer and raise no alert.

This is why the chain link hash is included inside the signature input. Patching a chain pointer now forces recomputation of all downstream signatures, which requires the signing key. Without it, the tampered chain fails to verify.

In short: the chain prevents silent gaps. The signature inside the chain prevents silent relinking.

What the chain does not guarantee

Completeness is the emitter's responsibility. The chain covers only what the Gateway received. Whether the emitter sent everything it should have, and whether the payload truthfully reflects the real world, is outside QAudit's scope — QAudit records faithfully what was sent, nothing more.

Timestamps require trusting the platform's clock. The chain proves the relative ordering of events cryptographically — no trust in the platform is needed to know that event A was chained before event B. However, the absolute point in time recorded for each event is assigned by the Gateway's own clock. There is no cryptographic proof that this timestamp is accurate — it must be taken on trust from QAudit. The mitigation is the evidence pack: packs include a timestamp from an independent Timestamp Authority (TSA), which provides an externally verifiable upper bound on when the events in the pack were received.

One chain per tenant

Chains are per tenant, not system-wide. Every tenant starts its own chain when its first event is stored. Chains from different tenants are independent; an event on one tenant's chain has no relationship to any event on another tenant's chain.

Data isolation. Each tenant's chain contains only that tenant's events — no other tenant's data is ever part of it. This has two practical consequences: the chain is independently verifiable using only data the tenant has access to, with no hidden state or server-side secret required; and an external auditor or regulator can verify the chain in full without ever touching another tenant's events.

Performance. Because each link in the chain must reference the previous one, events on the same tenant's chain must be written sequentially — the Gateway cannot parallelise ingestion within a single tenant. This is an inherent consequence of the chain's integrity guarantee. Per-tenant chains neutralise this constraint at scale: chains from different tenants are fully independent and written concurrently, so the sequential bottleneck is per tenant, not system-wide. In practice, a single tenant can sustain up to ~10 M events per month, with peaks of ~1 M events in a single day — and every other tenant in the system runs at the same throughput simultaneously.

Work in progress

The performance figures above are architectural expectations derived from the design. They have not yet been confirmed by load testing and are subject to revision.

Chain genesis

Every tenant's chain begins with qaudit.tenant.created, a platform event emitted when the tenant is provisioned. It is the anchor of the entire chain — no business event can be stored on a tenant's chain before this event exists. Its payload includes the initial signing public key, making the chain self-contained: a verifier who receives a full export of the chain has everything needed to check signatures without consulting an external registry.

A complete chain walk starts here. See Platform events for the full catalogue of platform-emitted events that appear in the chain.

Verifying the chain

To verify a tenant's chain, start from the genesis event and walk forward:

  1. Take an event's chain_link_hash and check it equals SHA-256(prev_signature ‖ prev_event_id ‖ event_id).
  2. Verify the event's signature (see Signing and integrity).
  3. Move to the next event and repeat.

A gap (missing event), a recomputed chain link that doesn't match, or a signature that fails to verify — any of these indicates tampering or data loss. A complete verification requires both steps: a chain walk alone proves structural integrity, but only signature verification proves that the content of each event was not altered.

The dashboard's integrity view makes it possible to walk the chain visually without performing the cryptographic steps manually. For independent verification, the public key and the signed bytes for each event are available from the single-event view. The Verification SDK provides a reference implementation of the full verification procedure.

Coming soon

The dashboard integrity view is not yet available.