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_accountand carries the email as its subject — it can’t act as a merchant credential, and every portal action re-derives the email from it.