Skip to main content
Duro serves four very different kinds of caller, each with its own credential. The middleware chain’s whole job is to turn any of them into the same thing the controllers expect: a request with a known tenant, a known mode, and a RepositoryContext already wired to the right schema.

Four ways to authenticate

CredentialCarriesResolves toNotes
SessionhttpOnly cookiea Member (tenant + role)Dashboard only. 10-minute idle timeout, sliding.
Secret keyAuthorization: Bearer sk_…a tenant + mode + full scopeServer-to-server. Prefix picks the mode. Hashed at rest, lastUsedAt written fire-and-forget.
OAuth tokenAuthorization: Bearer … (JWT)a tenant + mode + scopeclient_credentials grant from a cid_/csec_ pair, 60-minute expiry.
Identity tokenAuthorization: Bearer … (JWT)a CustomerIdentityCustomer-facing. Issued after WhatsApp OTP. Marked keyId: identity so it can never pass tenant auth.

The middleware chain on /v1

The public merchant API is the most interesting chain because it does the most work before the controller sees anything: By the time SubscriptionController.create runs, it calls this.repos(req).subscriptions.create(...) and that repository is already bound to (tenantId, mode). The controller never sees a raw database client, never sees the other tenants’ data, and never constructs a query that could.

Tenant scoping is the load-bearing wall

RepositoryContext is the object that makes cross-tenant leakage structurally hard: Each repository is constructed with the tenantId baked in and every query it issues includes tenantId in the where clause. There is no repository method that omits it. A new resource added to the system gets a repository, gets added to RepositoryContext, and inherits the scoping for free.

Idempotency, scoped to the tenant

Mutating endpoints accept an Idempotency-Key. The record is keyed (tenantId, key) — a compound unique. This matters: a global key namespace would let one merchant’s key collide with another’s, and an attacker could probe for replays. Per-tenant scoping closes that.
A real bug this design caught early: the idempotency table originally had a global-unique key. That’s a cross-tenant replay vector — fixed to @@unique([tenantId, key]). The security model page documents this and the other findings from the audit pass.

Mode is never optional, never a default

Four-fifths of the bugs in multi-mode billing systems come from mode defaulting silently. Duro never defaults it:
  • Keys carry it in the prefix (sk_test_ vs sk_live_).
  • Request-scoped code reads it from req.auth.mode and passes it to data(mode).
  • Public token lookups derive it from the token itself — a checkout token is cs_<mode>_…, so the mode is parsed from the prefix, not guessed by scanning both schemas.
  • The customer portal takes it explicitly — the dashboard’s mode switcher sends the current mode; the portal never silently lists both.
  • Background scanners loop both — and only background scanners, because a cron has no “current mode.”
That discipline is a documented invariant, and it’s the difference between a billing system you can trust and one you can’t. Next: a deeper look at multi-tenancy and the cache that sits in front of it.