Skip to main content
The thesis made the argument; this page is the implementation. A customer is one, by email. A CustomerAccount is a person — keyed by their email address, living in the core schema, deliberately not scoped to any tenant. One person, one account, no matter how many merchants they pay. That account owns a wallet and saved cards that work everywhere.

Why email, and why global

A person subscribes to several Duro merchants over time — a newsletter here, a SaaS tool there, a gym membership. Without a shared identity they’d be a stranger at each one: re-entering a card, re-topping-up, with no single place to see everything they pay for. Keying the account to email — the one identifier a customer reliably re-uses across merchants — collapses all of that into a single person: The account is stored lower-cased and unique on email, and it’s created on first touch — the first time a person pays at any merchant with a card, or verifies into the portal, or tops up. From then on the same email always resolves to the same account, and race conditions on first-creation converge on one row.

What the account holds

An account is a small, deliberate surface — a person and the money that belongs to them:

Wallet

One global balance in kobo, topped up in advance, spent wallet-first on renewals.

Cards

Cards saved automatically on any checkout, deduped by token, working at every merchant.

Virtual account

An optional dedicated Nomba bank account, BVN encrypted at rest, that funds the wallet by transfer.

BVN (encrypted)

Held only as ciphertext + a unique hash, never returned to the browser — collected solely to issue the virtual account.

Signing in: email OTP, no password

Access to the customer portal is by one-time code to the email — there is no password and no magic link to click: Security properties, all real:
  • The code is never stored in clear — only sha256(code) in Redis. Verification is a timing-safe hash compare (Hash.verifyHashedSecret).
  • Codes expire and are single-use — a 10-minute TTL, deleted on successful verify, with a 30-second resend cooldown.
  • The ?t= link is a convenience, not a credential. A portal link carries an encrypted ?t= token that only pre-fills the email on the sign-in screen; it does not log anyone in. The one-time code, delivered separately to the inbox, is what grants a session.
  • The session token is scoped to portal_account and carries the email as its subject — it can’t act as a merchant credential, and every portal action re-derives the email from it.

Re-login returns the same person

Because the key is the email, a second sign-in for the same address resolves to the same account — the same wallet balance, the same cards, the same virtual account. That’s what makes “one login, every merchant” possible: there is exactly one account per email, ever, and it’s the wallet’s owner. Next: the customer portal built on top of this.