Floor 3 of 5 of the tower
Harbor CRM
The interesting part of a multi-tenant app is what happens when someone forgets a filter.
The problem
Multi-tenant tools usually leak in one way: a query that forgot to ask “whose data is this?”. It works in every test until the day two clients share a screen.
The rules I set
- Every row carries a tenant, and the database knows it.
- The tenant comes from the session, never from the request body.
- A boundary that lives in only one layer isn’t a boundary.
What I built
A tenant-aware CRM. Next.js and React on top, a typed tRPC API in the middle, PostgreSQL through Drizzle underneath. Row-level rules in the database, role-aware screens, and dashboards that read only through the tenant’s own scope.
How it’s layered
- UI Next.js and React screens that change with the user’s role.
- Typed API tRPC contracts the screens are checked against.
- Data PostgreSQL with row-level rules, defined through Drizzle.
Decisions
- Tenant separation is a boundary in the application and again in the database.
- Tests deliberately try to read another tenant’s rows and are expected to fail.
- Screens stay in step with the API types, so a renamed field breaks the build, not a customer.
Where it stands
A demo system with generated sample data. It exists to show the layering and the two locks on every door.
TypeScript · Next.js · React · tRPC · PostgreSQL · Drizzle