MMHL.dev

SaaS · Multi-tenant

AdvBoard

Multi-tenant SaaS for Brazilian labour law firms: case flow on a kanban board, a calendar derived from the cases themselves, and court notices fetched automatically every business day.

Year
2026
Role
Solo project — product, architecture, back-end, front-end and deployment
Status
Live
Visit the project
advboard.app.br
AdvBoard kanban board showing the eight stages of a labour lawsuit with cases distributed across them

The context

A small law firm runs its caseload on spreadsheets and WhatsApp. The workflow exists in people's heads — intake, calculation, filing, hearing, settlement — but nobody can see where a given case is, and a court notice only surfaces when someone remembers to check the official gazette. AdvBoard puts that workflow on a screen and fetches the notices without being asked.

Workflow stages
8
API routes
9
Official data sources
2
Lines of Security Rules
154

01

Isolation lives in the path, not in a field

Each firm is a tenant under /orgs/{orgId}/... — clients, cases, calendar, notices, departments and users, all inside the firm's path. The common alternative would be an orgId field on every document plus a filter on every query; forgetting that filter once would leak one firm's data into another's.

The orgId claim is written by the Admin SDK on the server, never by the client, and the Security Rules compare that claim against the {orgId} in the path. A wrong id on the client produces permission denied — never another firm's data. Isolation does not depend on any screen behaving well.

02

Suspension has to take effect immediately

A firm has a status: trial, active, suspended or cancelled. The tempting move is to put that in a custom claim alongside everything else — it is faster to read. The problem is that a claim lives inside an already-issued token: suspending a firm would only take effect once each person refreshed their token, which can take hours.

So the rules read the status from the firm's document, paying one extra read per request. Suspension applies on everyone's next request. And suspended means read-only, not locked out: a firm does not lose its history over an unpaid invoice, it just stops working until the account is settled.

03

A session that expires without getting in the way

Login is email and password through Firebase Auth, but the ID token never stays on the client: it is exchanged for an httpOnly session cookie issued by the Admin SDK. proxy.ts does the fast guard on cookie presence, and getCurrentUser() does the real validation on the server — signature, revocation and claims.

The session dies after an hour of inactivity. That is a requirement for a system holding case files, but dropping someone mid-form is unacceptable, so the cookie slides while the person is working and a countdown warning appears five minutes before the end. Activity is shared across tabs through localStorage: working in one tab does not log you out of another.

04

Court notices arrive on their own

Two public sources from the Brazilian National Council of Justice, with different jobs. The electronic gazette is queried by the firm's registered bar numbers and returns notices addressed to them — including on cases nobody has registered yet, which is precisely how a firm finds out something was filed in its name. The case-tracking API is queried by case number and returns updates on cases already registered.

A cron job runs every business morning. The sync button exists for anyone who wants to check right now, not for the system to work. Deadlines and hearings come flagged as urgent, and the calendar derives hearings from the cases themselves — nobody enters the same hearing twice.

05

Permissions the server enforces

Role and permissions live in custom claims and are the basis of the Security Rules. The interface only reflects what the server already guarantees: the front desk can register a client but not touch a case; the calculations role can move the workflow but delete nothing.

The detail that takes work: when a department's permissions change, the claims of every member are reapplied. Without that, already-issued tokens would keep the old access until they expired — the permission would have been revoked on screen and still be valid in the database.

06

What I would test again

The Security Rules have tests running against the Firestore emulator, separate from the unit tests. It was the decision that saved the most time: permission code is the kind where a mistake does not show up on screen, it shows up as a leak. Testing it by looking at the UI does not work — either it passes in the emulator, or you do not know.

What I would do differently is start multi-tenant. The system was born with data at the root and needed a migration script to move everything inside a firm. Migrating a data structure with people already using it is always more expensive than having been born right.

Screens

Case flow — the eight stages, with priority and drag between columns
Case flow — the eight stages, with priority and drag between columns
Dashboard — KPIs, urgent actions and this week's hearings
Dashboard — KPIs, urgent actions and this week's hearings
Cases — search, filter by stage and owner
Cases — search, filter by stage and owner
Administration — users and departments, the basis for permissions
Administration — users and departments, the basis for permissions
The product's public page
The product's public page

Contact

Got a similar project?

Need a system built from scratch, an API for an app that already exists, or someone to own the product end to end? Get in touch — I reply fast.

Let's work together

Other projects