Schwarz Limited Partners runs a B2B platform that delivers software license keys to retailers. It has an admin portal, a portal where retailers support their customers' orders and manage keys, an API, and a set of Azure Functions. A separate ordering platform serves convenience stores. I'm the engineer on all of it. The work came in three passes, each making the next one safe: harden it, upgrade it, then change its shape.
Hardening first
I measured the security work with OSV scans before and after: 617 known
vulnerabilities resolved, 153 of them in the first week. Each repository got
the same treatment, with osv-scanner on source and container images alongside
npm audit and dotnet list package --vulnerable. Snyk was replaced by
osv-scanner plus gitleaks in every pipeline, so a new vulnerable dependency or a
leaked secret now blocks the merge.
Most of it fell out of the upgrade itself. The ordering front end went from 189 findings to zero, and the framework jump alone cleared about 93% of them. The rest was pinning safe versions of transitive dependencies and deleting what nothing used. One API went from 243 packages to 92. A handful were real code fixes, from missing authorization checks to unverified webhook signatures. Adding a test suite to the ordering API took its coverage from 1.4% to 68.6% and found eight bugs along the way.
The upgrade
- Five back ends moved from .NET 6 and 8 to .NET 10, and the Functions moved from the in-process model to the isolated worker.
- Three Angular apps went from 17 to 21, with Nx 17 → 23, PrimeNG 17 → 20, MSAL 3 → 5, EF Core 8 → 10, and Jest replaced by Vitest.
- The main client's initial bundle ended up 41% smaller before compression and 44% smaller over the wire.
Nine stacks, one tier
The platform was single-tenant by replication. Every retailer got a full copy of everything: front end, APIs, Logic Apps, Azure Functions, and database. It was a low-volume B2B estate, about 500 requests a month per production API, paying for high-availability infrastructure nine times over.
The move to multi-tenant pools the compute, not the data. Each environment gets one front end, one API, one Logic App, and one Function App, all stateless and shared by every retailer. Each retailer keeps its own database, Key Vault, and blob storage. The keys are the product, so "no shared license-key database" is the rule the design is built around.
- Resolving the tenant. A registry in the admin API maps each request to a retailer by host name, by a claim on the portal login, or by the retailer's machine-to-machine credentials. It fails closed, with no default tenant, ever. New endpoint protection ran in shadow mode, logging what it would have blocked, before it was allowed to enforce.
- Proving isolation. A dedicated test suite checks that one tenant can't reach another's data, alongside Playwright end-to-end tests and SQL contract tests.
- Letting evidence move the plan. QA sizing showed every tenant together peaking around 32 DTU, so the tenant databases now move into one elastic pool per environment, still one database per retailer. The convenience-store platform keeps row-level tenancy in a single database; not every system needs the same answer.
- Migrating reversibly. Each database is copied online into the pool and then switched over in the registry, so rollback means pointing the registry back. Cutover goes one retailer at a time behind a canary, and bug fixes were split out of the refactor so the before-and-after measures only the consolidation.
The run rate was about $8,000 a month. The consolidated target is $1,200–1,550, 80–85% lower. Tearing down resources that billing and monitoring showed were unused has already saved $1,934 every 30 days, about $23,500 a year. One lesson: a retailer we believed was gone turned out to be dormant, so the teardown now comes with a written recovery path.
All of it is defined in Bicep, 61 files across five services, deployed through Azure DevOps pipelines. Everything is off by default, and production needs explicit approval and at least two instances. The consolidation has been rehearsed end to end on one tenant in QA, with the isolation suite passing 20 of 20. Production cutover is waiting on a few administrative steps on the client's side.