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.




