The two tables
Both live in a shared
audit schema. Both have the same immutability trigger.
The trigger
BEFORE means the trigger fires before the operation. RAISE EXCEPTION rejects the operation entirely. The exception propagates back to the caller as an error. The row is not modified. The transaction either aborts or proceeds without the mutation.
This is not “the application is careful.” This is “the database is incapable of allowing it.”
What this means in practice
INSERT works normally. UPDATE and DELETE both raise an exception. The application code doesn’t need to “remember” to avoid mutation. Even if a developer accidentally writes anUPDATE against an audit table — through Prisma, through a hand-written query, through a migration script — the trigger catches it at execution time. The bug is loud, not silent.
How we represent “the status changed”
If a row’s status changes (e.g., a payment that was PENDING becomes SUCCESS), we don’t update the original row. We insert a fresh row representing the state transition.references_id (a self-referential column). Reading the history is a query, not a stateful update.
This pattern is fundamental to event sourcing. It’s slightly more verbose at write time and significantly more reliable at read time, audit time, and dispute time.
How we derive current state
Application code doesn’t read directly frompayments_audit to compute a user’s current wallet balance. That would be expensive and error-prone.
Instead, derived state lives in the payments schema (payments.wallets, payments.wallet_transactions, etc.) and is updated transactionally alongside the audit insert.
The “we cannot edit history” claim, in depth
The phrase is precise. We make it because the database guarantees it. Could a sufficiently determined operator drop the trigger? Yes — but:- The trigger drop itself is an audit-loggable event at the Postgres level (the
pg_auditextension is enabled). - The deploy that drops the trigger would be reviewed in code.
- The before-and-after database fingerprints would differ in a detectable way.
- The lenders who rely on Guild’s audit have contractual access to spot-check the database fingerprints, and they would.
What this enables for partners
A bank reads a Guild snapshot for a borrower. The snapshot says: “Trusted tier. 38 completed jobs in the last 90 days. ₦458K in inflows. 12 distinct counterparties.” The bank’s underwriting model can take that signal at face value because:- The aggregates come from a database with structural immutability.
- The aggregates are signed by Guild with our Ed25519 key.
- The signature can be verified against our published public key.
- The user explicitly granted consent for this read.
- The snapshot includes a timestamp; the bank can detect a stale or replayed snapshot.
What this enables for disputes
A Job Seeker disputes a payment: “I worked four days, he only paid for three.” The audit ledger shows:- Four
attendance_markedrows inchannel_audit, each with a request ID. - Three
WALLET_CREDITrows inpayments_audit, each with a OPay transaction reference.
Retention
The audit tables are retained indefinitely. The financial regulators we expect to interact with (CBN, SEC) have 7-10 year retention requirements; indefinite retention satisfies that. Lender contracts may impose longer requirements; indefinite still satisfies. Storage cost is linear with platform volume. At 10M monthly active users and average per-user activity, the audit tables grow at a predictable rate that fits comfortably in modern Postgres deployments. Partitioning by month gives operational flexibility.What’s NOT in the audit ledger
Things you might expect:- Application logs (Pino lines). These are operational diagnostics, retained 90 days. Not authoritative.
- Voice call audio (R2). Retained 30 days, then auto-deleted. Transcripts may be longer.
- WhatsApp message contents. Stored separately in
whatsapp.messages; subject to a 365-day retention; PII-redacted. - Ephemeral state (OTPs, session tokens). In Redis with short TTLs.
What to read next
Data flow
How the ledger fits in the broader data model.
Lender snapshots
What lenders read from the ledger aggregates.
KYC tiers
The PII story that sits alongside the ledger.