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.