Security & operations

What leaves your systems

The library is zero-egress: the engine decides and records, but you perform the mutation in your own database, so personal data never leaves your infrastructure. The only thing you may optionally send out is evidence — opaque subject refs, event types, and hashes (plus any sealed ciphertext) — never PII. See "Push evidence to the vault" below.

Tamper-evidence, in two layers

  1. Hash chain. Each audit entry's hash covers the previous entry's, so any change to content or ordering breaks verification. This makes tampering detectable anywhere — even by an auditor reading the rows directly (verify offline).
  2. Database-enforced append-only (hosted vault, PostgreSQL). A trigger blocks UPDATE/DELETE on the audit, evidence, and checkpoint tables, so the rows can't be altered in place at all. Retention pruning is the one allowed delete — gated behind a covering checkpoint — so a pruned chain still verifies.

Multi-tenant isolation

Every query is scoped to the caller's organization at the application layer. The hosted vault adds Row-Level Security on the tamper-evident tables as defense-in-depth: a PostgreSQL policy scopes rows to the request's tenant, so a handler that forgets the explicit filter still can't read another tenant's data.

Deployment note. RLS only takes effect when the application connects with a DB role that is not a superuser and does not have BYPASSRLS — those bypass all policies (some managed-Postgres admin users have BYPASSRLS, so provision a dedicated app role). The hash chain protects integrity regardless of the role.

Crypto-shredding for erasure

When evidence must contain PII, seal it: the PII is encrypted into a token the hash covers. Destroying the key (right-to-erasure) renders it unreadable while the chain still verifies — and the vault only ever stores the ciphertext, never the key.

Push evidence to the vault

Keep everything local, or push your tamper-evident chain to the hosted vault for an independent, server-timestamped, counter-signed copy. It's idempotent (re-pushing is a no-op) and the fire-and-forget variant never blocks or raises in your request path.

from dpdpstack import EvidenceClient

vault = EvidenceClient("https://getdpdp.net/api/v1", api_key="dpdp_sk_…", source="api")

vault.push(log)            # synchronous: -> {"stored": N, "chain_verified": True, …}
vault.push_background(log) # fire-and-forget: returns immediately, errors swallowed

The vault verifies the pushed chain with the SDK's own hashing (so the two never drift), server-timestamps receipt, and can counter-sign a Certificate from it. See the hosted platform.