The three tiers
A user can stop at any tier. Most Job Seekers move to Tier 2 in the first week (the day they want to withdraw earnings). Tier 3 is typically reached when a user opts into the credit pathway with a partner lender.
Tier 1 — phone + OTP
The minimum.
The phone number is the identity anchor. We don’t encrypt it because we need it for routing every inbound (voice, WhatsApp, SMS, web login). Encryption of the identity anchor would force decryption on every login lookup; the cost would be enormous for marginal security gain when the attacker who has database access already has the phone number.
The first name is unencrypted because it shows up in every greeting (“Welcome back, Musa”) and would need to be decrypted on every read.
Tier 2 — profile + payout
The unlock for receiving real money to your own bank.
Account numbers are stored with the last four visible (
••••3911) so we can display them in user-facing surfaces without exposing the full number on every read. The full number is needed only when initiating a payout; we look it up by user ID at that moment, decrypt if needed, fire the payout, and don’t surface it elsewhere.
Tier 3 — BVN + NIN + address
The unlock for lender consent and the full transactional surface.
Verification timestamps and verification flags live as separate columns so we can query “has this user been BVN-verified” without decrypting. The actual BVN, NIN, and personal identifiers live inside an encrypted JSON blob.
The envelope encryption
Provided by@guild/crypto. Uses XChaCha20-Poly1305 through sodium-native (Node’s bindings for libsodium).
Why XChaCha20-Poly1305
The AAD binding
Theaad (additional authenticated data) parameter binds the ciphertext to the user’s ULID. If an attacker swaps user A’s encrypted blob into user B’s record, decryption with user B’s ULID as AAD will fail with an authentication error.
This is a defence against a scenario where someone with database write access tries to “move” a KYC record from one account to another. The cryptography rejects the move.
Key version field
The encrypted blob includes akeyVersion field. When we rotate the master key, we re-encrypt KYC payloads with the new key over a rolling window. Old blobs with keyVersion: v1 are decryptable until the rotation completes; new blobs use v2. The rotation is transparent to application code.
How a Tier 3 verification flows
The BVN never sits in our database in plaintext. The verification partner sees it momentarily (over TLS). We never log it. The verification result and the encrypted record persist; the raw BVN ages out of our memory the instant the request handler returns.When KYC data is decrypted
Three legitimate triggers:- A user request to view their own KYC data. They authenticated; they’re entitled to see what we hold. The decryption happens in the request handler and the data is sent over TLS to the user; not logged.
- A re-verification (e.g., periodic NIBSS or partner refresh). The decryption happens, the partner call fires with the data, we update the verification timestamp.
- A regulatory request for the data, with proper authority. The decryption happens manually under audit. Logged in
channel_auditwith a system-actor entry.
When KYC data is anonymised
When a user invokes their right to erasure (NDPR-compliant data deletion):- The encrypted KYC payload is deleted from
identity.kyc_records. - The user’s
identity.usersrow is anonymised — phone becomes a placeholder, first name becomes a placeholder, all personally-identifying columns are replaced. - The user’s audit rows (
channel_audit,payments_audit) persist with the anonymised user ID. - The user receives a confirmation.
The master key
A 32-byte key. Lives in environment variables on the VPS, never in the database. Backed up to a sealed file on the operations team’s password manager. Rotated annually under a documented procedure. The day-to-day risk model: a database leak alone yields ciphertext blobs that cannot be decrypted. To decrypt, an attacker would need both the database AND the host’s environment (or the password manager). The “and” is the critical security property.What we don’t ask for
A few things you might expect, that we deliberately skip:- Selfie / facial verification at v1. The BVN + NIN match against the partner database is the live-person check. Adding selfie liveness is on the roadmap for higher-tier verification but isn’t in scope at v1.
- Document uploads (ID card photos) for Tier 1 or Tier 2. Only for Tier 3, where we accept utility bills for address verification.
- Social network OAuth. Guild doesn’t depend on Google / Facebook / Apple identity. The phone number is the anchor.
- Biometrics on-device. Out of scope.
What to read next
Audit ledger
The permanent record alongside the encrypted-PII story.
NDPR posture
The compliance framing in detail.
Lender consent
What Tier 3 unlocks on the lender side.