Skip to main content
The phrase you’ll see in every Guild conversation is “the ledger.” It’s not a metaphor. It’s two specific Postgres tables — channel_audit and payments_audit — with structural guarantees that make them legitimate as a source of truth for credit underwriting. This page is what those tables actually contain, how data gets into them, and how the ML layer and the Partner API read out.

The substrate

The amber tiles are the substrate — the only places we accept “this happened.” The green tiles are the products that read from the substrate.

The append-only contract

Two tables, two triggers, one promise.
Once a row is inserted, it cannot be UPDATEd or DELETEd. Not by application code. Not by an engineer in a psql session. Not by a Postgres superuser short of dropping the trigger entirely (an action that itself is auditable and reviewed). This is the structural promise behind every claim Guild makes about the ledger. “We can’t edit your history” is provable from the schema definition.

channel_audit — every interaction

Every interaction across every channel writes a row. Schema (abbreviated):
The event_kind taxonomy is intentionally rich. Examples: Every row carries the request_id from the originating request. Tracing across services for a single user action is straightforward.

payments_audit — every kobo motion

Every motion of money writes a row. Schema (abbreviated):
Every credit-bearing entry has a verified_at timestamp from the reconcile guard. If it’s NULL on a row that should have it, something is wrong — and we’d never insert that row in the first place because the reconcile guard runs before the insert. Examples of motion_kind: A user’s full transaction history is a query over payments_audit filtered by user_id, sorted by occurred_at. The dashboard pages all read from this view.

How a single event ripples through

Walking through one event end to end. Adekunle posts a job for ₦60,000. What’s notable in the sequence:
  • The payments_audit row is written before the OPay response is confirmed. Status is “held” via the absence of verified_at. When the webhook confirms, we don’t UPDATE — we insert a fresh row with the verified_at populated, linked by the original ULID’s id. Append-only.
  • The channel_audit row is written immediately. The user did the action. We record it regardless of whether OPay later confirms.
  • The derived state in jobs is updated with UPDATE — but derived state isn’t audit. Derived state can change. The audit can’t.

Schema ownership

Six application-owned schemas in one Postgres database. Plus the audit tables (channel_audit, payments_audit) which sit in a shared audit schema and are writable by every service. Cross-schema queries are cheap inside one cluster. Each service has its own Prisma schema file declaring the schemas it owns.

How the standing model reads the ledger

The standing model’s feature pipeline is a SQL query that joins across the schemas. Roughly:
This is the featurisation step. The result feeds the standing model, which produces a tier classification. The tier writes back to a derived users.standing_tier and users.standing_score column, with standing_recomputed_at timestamp. A subsequent dashboard read or Partner API snapshot fetch reads from the derived column unless an invalidating event fired (a new completed job, a new review, a repayment). Lazy recomputation; nightly batch recomputation backstop.

How the matching model reads the ledger

Similar query, different shape. When a job is posted, the matching service:
  1. Loads the job row from jobs.job_postings.
  2. Queries identity.users filtered by actor_role = 'JOB_SEEKER' and within geographic range of the job’s location.
  3. Joins each candidate with their derived standing tier, their last activity timestamp, their language preferences, their skill tags.
  4. Builds a feature vector per candidate.
  5. Runs the lambdarank model. Returns the top N ranked.
Feature construction time: a few milliseconds. Model inference: under 100ms for a candidate pool of a few thousand.

How the Partner API reads the ledger

The Partner API surfaces aggregates, never raw rows.
Notice: no transaction-level data. The partner sees the aggregates. They get sums, counts, distinct-counts — never the names of the counterparties or the timestamps of individual transactions. This is a deliberate privacy boundary. The snapshot JSON is then signed with our Ed25519 key and returned to the partner.

Data retention

A user who deletes their account triggers a “right to erasure” workflow under NDPR. Personal identifiers in identity.users are anonymised; the user’s channel_audit and payments_audit rows persist with the anonymised user ID. This preserves the audit integrity (the rows are still there, still immutable) while removing the personal link.

Cross-schema reads at the application layer

Most queries cross at least two schemas (identity + payments, jobs + identity, etc.). We do not maintain foreign keys across schemas — application-layer validation handles referential integrity. The reason: foreign keys add operational coupling (a DDL migration in one schema can lock another). Service ownership of schemas means each service migrates its schema independently. FKs would break that contract. The trade-off is that orphaned data is possible in theory (an audit row referencing a deleted user). In practice, the right-to-erasure flow anonymises rather than deletes, so orphans don’t occur for real-world reasons.

A note on the partition path

channel_audit and payments_audit are partitioned by month using PostgreSQL declarative partitioning. As the platform scales, individual partition tables stay manageable in size; queries scoped to a recent time window (90 days) touch only a few partitions. The partition path scales us comfortably through the first million users without restructuring. Beyond that, sharding by user-ID range is the next move — a known operation that doesn’t require an architecture change.

Resilience & scale

How the ledger stays correct under load.

ML layer

The models that read this substrate.

Standing & the Ledger

The user-facing surface that the ledger powers.

Security & trust

The compliance framing of the append-only contract.