Multi-Tenant Core API
The middleware every utility-customer portal in the fleet stands on - two generations of it, and the migration from one to the other.
- Role
- Primary author of both generations, architecture and security owner
- Period
- 2023 - present
- Ownership
- 58% of v1 · 78% of v2
- Stack
- Fastify 5 · TypeScript (strict) · Zod · MongoDB · AWS SDK v3 · ECS Fargate · Vitest
Problem
A fleet of utility companies each needed a customer portal talking to Oracle Utilities CCS. The v1 server I inherited when I joined in 2023 - an Express codebase that predates me, 58% mine by commit today - had authorization gaps and no clean way to vary behavior per tenant. Every new client meant forked code and re-audited security.
The v2 rebuild, started in early 2026, had one mandate: a single API where a new tenant is configuration, not code, and where authorization failure is impossible to ship by accident. Two tenants run production on it today, both launched on it; the rest of the fleet still runs on v1 and moves one tenant at a time.
Constraints
Utility customers pay bills through this thing - it has to be up and it has to be right. Oracle CCS is the system of record, reached over OAuth 2.0 with per-tenant credentials. The team is small, so the architecture had to make the secure path the easy path - and the migration cannot pause billing, so both generations run side by side, the portals carry interceptors for either backend, and a tenant moves only when its parity checks pass.
Architecture
Fastify 5 on Node 22, ESM-only, TypeScript strict with no `any`. Zod validates every input at the boundary; responses use one fixed envelope so clients never parse ad-hoc shapes.
Tenant behavior is composed from a base configuration plus per-tenant overrides, assembled by strategy factories - the request path never branches on tenant name. Authorization is enforced as a route-level gate: every endpoint proves the caller owns the account it touches before any data access, which is what kills IDOR as a class.
Accountability does not stop at the customer. Admin reads and impersonated sessions are audited as well as writes, attributed to the acting admin, with field-level capture of what a write actually changed and deep redaction so tokens and provider passwords never reach the log. Admin writes are kept for a year, admin reads for ninety days.
Where the billing system of record and the payment provider disagree, a detection-only reconciler surfaces the drift instead of silently writing to either, and the account-linking path guards against duplicate registrations and signup conflicts. Small guards pay: enforcing the ten-digit account number on every sign-in form removed about 215 CCS errors a day that partial or oversized numbers had been generating.
The integration runs in both directions. Where the fix belongs on the CCS side, I write the IWS change specification - request and response examples, tenant scope, blast radius - and the vendor team implements it; the portal side lands in step.
An 80% coverage gate and OWASP checks run in CI; Bitbucket Pipelines builds to ECR and deploys to ECS Fargate behind a shared ALB.
Outcome
- On v2 a tenant is config plus provisioning, not a fork; two tenants launched on it, the first v1-to-v2 cutover is certified and awaiting DNS, and v1 stays patched and audited until the last one moves.
- Authorization is structural, not reviewed-in: the route contract enforces ownership checks on every endpoint.
- Billing drift surfaces through a detection-only reconciler instead of a silent write to either system.
- The integration patterns became the fleet standard other services adopt.