Section 09
Tenancy, Roles & Plans
Individuals and agencies served by one model, one authorisation path, and one set of plan limits.
The founding decision
Everyone is an organisation
An individual advocate is an organisation with exactly one member, who holds the owner role. There is no separate "personal account" type.
Why: the alternative — a personal path and an agency path — means every query, every permission check, every billing rule, and every screen has two variants. They drift. The drift shows up as security bugs, because the personal path is the one where nobody remembers to check org_id.
Cost: mild conceptual overhead for solo users, invisible if the UI never says the word "organisation" to someone who has no colleagues. Benefit: one code path, one authorisation model, and an individual who hires their first clerk is an invite — not a migration.
Entities
erDiagram
ORGANISATION ||--o{ MEMBERSHIP : has
ORGANISATION ||--|| SUBSCRIPTION : "is billed by"
SUBSCRIPTION }o--|| PLAN : "is an instance of"
USER ||--o{ MEMBERSHIP : "belongs via"
MEMBERSHIP }o--|| ROLE : grants
ORGANISATION ||--o{ MATTER : owns
MATTER }o--o| USER : "assigned to"
ORGANISATION ||--o{ TEAM : "may group into"
TEAM ||--o{ MEMBERSHIP : contains
ORGANISATION ||--o{ AUDIT_EVENT : records
MATTER ||--o{ MATTER_NOTE : accumulates
Fig 9.1 — Tenancy entities
Two properties of this model deserve emphasis:
- Matters are owned by the organisation, not the user. An employee leaving does not orphan or remove cases. Assignment is a separate, nullable relationship.
- A user may belong to several organisations. An advocate can work independently and also be on a firm's roster. Sessions carry an active organisation context; data never crosses between them.
Roles
| Role | Matters | People | Billing | Typical holder |
|---|---|---|---|---|
owner | All, full | Invite, remove, set roles | Full | Solo advocate; firm principal |
admin | All, full | Invite, assign | View only | Office manager |
advocate | Assigned + team | View colleagues | None | Associate advocate |
clerk | Assigned, add and edit | View colleagues | None | Junior clerk, munshi |
viewer | Assigned, read only | None | None | Client-facing read access |
Deliberately five roles, not a general permission matrix. Every firm will eventually want a bespoke role; resisting that until there is real evidence keeps the authorisation surface small enough to reason about — and a small authorisation surface is a security property, not just a simplicity one.
Enforcing isolation
Cross-tenant data leakage is the failure that ends a legal-data business. Defence is layered, and no layer is trusted alone.
1. Session scope
Every authenticated request resolves to exactly one active org_id, taken from the token — never from a request parameter.
2. Repository layer
All data access goes through repositories that require an org scope. There is no ambient "find by id".
3. Postgres RLS
Row-level security on every tenant table, keyed to a session variable. The database refuses cross-tenant reads even if application code is wrong.
4. Test enforcement
A standing test suite that attempts cross-tenant access on every endpoint. Adding an endpoint without a scope check fails CI.
Why RLS despite the repository layer
Application-layer scoping is correct until someone writes one raw query for a report, one admin tool, or one migration script. Row-level security makes the database the final arbiter, so a mistake in application code becomes an empty result rather than a breach.
It costs a little query-planning overhead and some care with connection pooling — the session variable must be set per checkout, not per connection. That is a worthwhile trade for a product holding privileged case data.
Plans and limits
Illustrative shape rather than final pricing — the mechanism is the point.
| Limit | Individual | Agency Starter | Agency Pro |
|---|---|---|---|
| Seats | 1 | 5 | 25 |
| Active matters | 50 | 500 | 5,000 |
| Courts | All | All | All |
| Manual refreshes/day | 20 | 200 | 1,000 |
| AI order summaries/mo | 25 | 250 | 2,000 |
| WhatsApp alerts | Included | Included | Included |
| Teams | — | — | Yes |
| Audit log retention | 30 days | 1 year | 3 years |
Which limits are real
Limits should track cost, otherwise they are arbitrary and users resent them.
- Active matters is the honest one — it maps directly to daily polls, which is the actual marginal cost per the unit economics.
- Manual refreshes and AI summaries are also real costs and should be capped.
- Seats is a pricing lever, not a cost driver. Fine — but price it as value capture and do not pretend otherwise.
- Courts should never be limited. Restricting court coverage by plan makes the product worse at its one job and saves nothing, since coverage cost is per matter, not per court.
Enforcement
flowchart LR
A[Action requested] --> B{Limit type}
B -->|hard: seats, matters| C{At limit?}
C -->|no| D[Proceed]
C -->|yes| E[Block + upgrade prompt]
B -->|metered: refresh, AI| F{Quota left?}
F -->|yes| G[Consume + proceed]
F -->|no| H[Soft fail:
queue for next period
or offer top-up]
D --> I[Record usage]
G --> I
Fig 9.2 — Limit enforcement
Never stop tracking a matter because a plan lapsed
The single most damaging thing this product could do is silently stop watching cases when a subscription expires or a limit is exceeded, and let a user miss a hearing they believed was covered.
On downgrade or expiry: keep polling through a grace period, tell the user clearly and repeatedly which matters are at risk, and require an explicit choice about which to keep. If tracking must stop, say so unambiguously in the app, by push, and by WhatsApp — never by silence. Degrading loudly is a product requirement, not a courtesy.
Assignment and workload
What agencies are actually buying: knowing who is responsible for what.
- Assignment — each matter has zero or one assignee, plus optional watchers who receive alerts without ownership.
- Bulk assignment — by court, by client, by case type. Assigning 200 matters one at a time is not a product.
- Reassignment on departure — removing a member forces an explicit reassignment of their matters. Their alerts stop; the matters do not.
- Workload view — matters per person, hearings this week per person. The screen a firm principal opens first.
- Tomorrow's board — every listed matter across the firm, grouped by assignee. The daily operational artifact for an agency, and the agency analogue of the individual's personalised cause list.
Client assignment v2.0
Assigning a matter to a staff member (who works it) and assigning it to a client (who it is done for) are two independent relationships — see CLIENT in the Domain Model. Conflating them breaks the moment a firm wants "every matter for this client" as a view, or an invoice that spans matters handled by different staff.
- Client record — name, contact, GSTIN, notes. No login, no seat, by default.
- Client portal login — optional, uses the existing
viewerrole scoped to that client's matters. Not a sixth role; the same read-only role the table above already defines. - Client view — every matter for one client across courts and assignees, the natural grouping for a status call or an invoice.
This deliberately reuses the tenancy model rather than adding a parallel "client account" concept: a client with portal access is a MEMBERSHIP holding the viewer role, scoped by client_id in the same row-level-security layer that isolates organisations from each other. One isolation mechanism, not two.
Audit
Agencies need to answer "who changed this, and when" — for internal accountability and, occasionally, for a client. Every mutation of a matter, every assignment change, every membership and role change, and every export writes an AUDIT_EVENT with actor, organisation, target, before/after, and timestamp.
Audit events are append-only and readable by owner and admin. Retention follows the plan. This also serves the security requirement for access logging over personal data under the DPDP Act — one mechanism, two obligations.