Multi-tenant platforms, and the dashboards behind them.
Software other people log into every day. Built multi-tenant, typed end to end, and instrumented so you can see it run.
Overview
The same console, per workspace.
Switch the workspace and every widget changes. Each tenant sees only its own data, enforced by the database, not the UI. That is the whole product, on one screen.
The preview above is a still. The one behind this button is the real thing, running on seeded data, with row-level isolation you can try to break.
The platform under that screen.
The dashboard is the easy half. The half that keeps one customer out of another's data is this: a workspace pinned on every request, a typed API, and row-level security in the database, all instrumented so the numbers on that screen are real queries you can trust.
Every request proves who it is and which workspace it belongs to before it reads a byte.
The workspace is pinned on the connection, so everything downstream is scoped to it automatically.
Requests and responses are typed end to end, so a wrong shape fails loud, not silently in prod.
The isolation rule lives in the database, so a forgotten filter still can't cross tenants.
Usage, metrics, and an audit trail feed the dashboard, so you see it run and can bill it.
One codebase. Every customer sealed off.
Every tenant runs on the same code and the same database, and still can't touch each other's rows. The isolation isn't a filter in every query someone has to remember, it's a policy the database enforces on every read and write. A forgotten filter, a crafted request, an injection: none of them reach across.
1-- one codebase, one deploy, isolation in the database2CREATE POLICY tenant_isolation ON invoices3 USING (tenant_id = current_setting('app.tenant'));tenant_id scoped · no query crosses the line
Typed, so a bad shape fails loud.
One schema is the contract for both the API and the client, generated into types and validated at the edge. A request with the wrong shape is rejected at the door with the exact field, in code review or a 422, instead of a null that corrupts a row and surfaces as a support ticket next week.
1// schema.ts — the shape both sides agree on2export const CreateInvoice = z.object({3 customerId: z.string().uuid(),4 amount: z.number().int().positive(), // cents5 currency: z.enum(["USD", "EUR"]),6 dueInDays: z.number().int().max(90),7});8export type CreateInvoice = z.infer<typeof CreateInvoice>;A product is only as good as the parts nobody demos.
The tenancy that keeps customers apart, the types that catch a bad shape before a user does, the dashboard that shows you it's running. They're the least glamorous work and the first thing that decides whether the thing holds up. We build those first, then the features on top.
What we build
8 buildsTell us what your product has to hold.
Send the shape of the system and the outcome it has to keep. We come back with a scope, a plan, and where a demo proves it.