← STATIONSCH 01 — SPEC/CH/01 · ESC TO CLOSEENFR

Legis — multi-tenant legal case management

A vertical SaaS product that digitises the working life of an Algerian law firm: client intake, case registration across the court hierarchy, hearing and postponement tracking, firm finances, and court-ready Arabic documents. The interesting engineering is not the CRUD — it is modelling a foreign legal system faithfully enough that the output is usable in court.

DOMAIN
29 entities, Algerian court hierarchy
TENANCY
Shared schema, per-request isolation
AUTH
Rotating refresh, reuse detection
SURFACE
212 REST endpoints, 180 authority guards
FIRST PAINT
977 → 168 KB, gzipped entry chunk
FIG. 1 — REQUEST PATH
RATE LIMIT
per IP
JWT
cookie
TENANT
request scope
LICENCE
read / write
SERVICE

↳ redis unreachable → fail open, login stays up↳ licence lapsed → read-only, never locked out

FIG. 2 — APPEAL CHAINING
CASE
first instance
APPEAL
links both
CASE
appeal court

↳ the new case is appealable in turn — the chain recurses

SOURCE
76,900 lines · 3 repositories
DATA
29 JPA entities · 31 migrations
API
212 REST · 2 GraphQL queries
BUILT
15 months · 5 contributors

THE HARD PART

With right-to-left set on the page, mixing an Arabic label and a Latin case number inside one text run crashes the PDF renderer in its bidirectional reordering pass — and every legal document pairs exactly those. The fix was to stop asking that algorithm to solve it: label and value became two text nodes in a flex row, each directionally uniform, so the failing path is never entered.

WHAT I REJECTED

Moving an appealed case up the hierarchy with a level column. An appeal is a second proceeding with its own hearings, decision and charges; collapsing the two loses the record a lawyer actually needs. An appeal now creates a new case and links back to the original, so the chain recurses without special-casing.

WHAT I'D FIX FIRST

Tenant isolation is enforced in the service layer from a request-scoped context, so one forgotten filter is a confidentiality breach. The answer is row-level security keyed on a session variable, so isolation survives a missing WHERE clause instead of resting on review discipline.