▲ Case 02 · Multi-tenant SaaS in production
Selling a solar system is a relay between five departments. The baton drops at the handoff, always at the boundary, which is where nobody is the owner. The architecture exists so the handoff happens without anyone having to remember to pass the word.
pnpm workspaces and turborepo. Nine module packages, plus a shared core and a types package.tRPC 11 on top of Express 4. Types flow from the server to the browser with no client generation and no duplicated schema.Drizzle ORM on MySQL, with migrations versioned in the repository.wouter for routing and recharts for charts.Capacitor, consuming the same API, with push notifications.bcryptjs with cost 12 for passwords, tokens signed with jose, and Google OAuth sign-in as an alternative.vitest, running on every change. The end-to-end tests call the tRPC router directly, without starting an HTTP server.Nine modules with explicit boundaries, published as separate packages, compiled and deployed as a single application.
What I ruled out: microservices. In an operation this size they would trade a code organization problem for a network problem, with distributed transactions, contract versioning and tracing across services, all of it with nobody on call to look after it.
The boundary between modules exists in the code and the compiler enforces it. The operational cost of a distributed application does not come with it.
An approved proposal fires an event, and whoever is listening reacts: the project module creates the handoff, and finance builds the payment plan. A completed project triggers purchasing. Available material opens the work order with its six stages. A finished installation registers the warranties, and the signed invoice releases the payment.
What I ruled out: direct calls between modules. That works with three and turns into an impossible graph with nine, where adding a new module forces you to edit the old ones.
A new module starts listening to an event that already exists, and none of the other eight is touched.
The column that identifies the company that owns the record has existed since the first migration, and the company filter is checked in CI on every new query. There are six access roles per company, from the owner to the installer.
What I ruled out: leaving it for later. Doing it at the start costs one column. Doing it later means rewriting every query in the system, and living with the risk of forgetting exactly one of them, which is the one that leaks one customer's data to another.
The site survey is filled in on the computer and on the phone, in screens written separately. The field is defined in a single source file and implemented in both screens, and a script checks all three ends.
What I ruled out: agreeing with the team that everyone will remember to change both sides. A rule that depends on nobody making a mistake, every day, for months, does not hold.
There is a file that maps, field by field, which is the canonical source of the data and which copies are allowed.
Why: without it, two places start answering the same question with different numbers, and there is no way to know which one is right. In a system that calculates commission and cash flow, that is more than an inconvenience.
The integration tests call the tRPC router directly, in process, instead of bringing up the application and hitting it over HTTP.
What I gain: the suite runs on every change without depending on a free port, on startup time or on cleaning up a hung process. A slow test is a test someone turns off.

A system the team does not know how to use has not been rolled out. Every flow has written instructions in the language of the person who does the work, and they open on the phone at the moment the question comes up.


Counted in the repository on September 22, 2026.