Renan Oliveira
← all systems

▲ Case 02 · Multi-tenant SaaS in production

Nine modules that talk through events

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.

Period
2026
My role
architecture and most of the code
Language
TypeScript, end to end
Status
in production

How it is built

Structure
Monorepo with pnpm workspaces and turborepo. Nine module packages, plus a shared core and a types package.
API
tRPC 11 on top of Express 4. Types flow from the server to the browser with no client generation and no duplicated schema.
Database
Drizzle ORM on MySQL, with migrations versioned in the repository.
Interface
React 19 with Vite 7, Tailwind v4, Radix UI for component accessibility, TanStack Query for server state, wouter for routing and recharts for charts.
Field
Android app built with Capacitor, consuming the same API, with push notifications.
Authentication
bcryptjs with cost 12 for passwords, tokens signed with jose, and Google OAuth sign-in as an alternative.
Isolation
Every business table carries the company that owns the record. Tenants are resolved by subdomain. The company filter is written by hand into the queries. A CI check rejects any new query without it, and the older ones that still lack it are frozen in a list to be fixed.
Tests
vitest, running on every change. The end-to-end tests call the tRPC router directly, without starting an HTTP server.

The decisions, and what I ruled out in each one

Decision 01

A modular monolith, not microservices

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.

Decision 02

Modules publish events instead of calling each other

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.

Decision 03

Multi-tenant from the first table

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.

Decision 04

A form field is born in a single file, and a script checks all three ends

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.

Decision 05

A registry declares the true source of each business field

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.

Decision 06

End-to-end tests without starting a server

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.

Sign-in screen of the platform in production
The platform's sign-in screen in production. The inside requires access and contains customer data.

Rollout is engineering too

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.

Instructions for filling in the site survey
Site survey, for whoever is up on the roof.
Instructions for tracking the installation from the phone
Installation tracking from the phone, even with no signal.
9independent modules
6access roles

Counted in the repository on September 22, 2026.