
- Role
- Data model, access policies, custom extension, provisioning, branding — all of it
- Year
- 2026
- Languages
- EnglishGermanDutchFrench
- Stack
- DirectusTypeScriptVue 3PostgreSQLRedisDocker
Most Directus demos model a blog. A blog has one kind of user and nothing worth protecting, so it never has to answer the question that decides whether a real project succeeds. A dental practice answers it on day one: the receptionist books the appointment and takes the payment, so she needs the patient list — and has no business reading the medical alerts.
Field-level access, not hidden inputs
Front desk reads patient contact details and is denied medical alerts, allergies and clinical notes at the field level — and denied treatment records and the tooth chart outright. Request the field explicitly with a token and the API returns 403. No frontend is involved, so no frontend can leak it.
Multi-tenancy as a row filter
Every collection carries a clinic, and every policy compares it to the signed-in user's. Creates are stamped with the same value, so nobody can plant a row in another practice. The demo seeds two practices on purpose — with one, a broken tenant filter is invisible because everything you can see happens to be yours.
The permission model is tested
A permission model nobody has tried to break is a hope, not a policy. A suite logs in as each of the five roles and asserts both directions — what they must be able to do and what they must not. A hundred and eight checks, including a cross-tenant fetch by ID, a radiograph requested by its asset URL, an invoice header weighed against the lines it claims to total, and a deliberately sabotaged instance to prove the checks can fail at all.
A tooth chart as a custom interface
FDI notation, arches laid out as a clinician faces the patient, molars sized wider than incisors. It reads an append-only findings log rather than mutable rows, so a chart is a view over history. Missing teeth are drawn as an absence, not a pale tooth.
Fewer teeth, more teeth, and neither
A chart with 32 boxes for an adult and 20 for a child, picked from the date of birth, is wrong in three directions. Agenesis is ordinary — the lower second premolars are absent in 29.9% of affected people. Supernumerary teeth do not fit FDI at all, which is why ISO 10394 exists: a mesiodens is AB, not 11, and the same code may legitimately describe two teeth. And age does not decide dentition, because a retained milk tooth in a forty-year-old keeps its primary designation. So dentition is recorded per tooth, never derived.
Records that cannot be rewritten
Clinical notes are entries, never edits: a correction is a new entry pointing at the one it corrects, with a reason, leaving the original legible. No policy grants update or delete on notes, histories or consents — including the practice owner's — and a check asserts no such rule exists. Consent stores the answers and a fingerprint of what was signed, not a PDF that can go stale in silence.
The modules a dentist looks for
Recall with NICE's three-month interval steps and the field most schemas omit — whether the patient agreed with the interval. Treatment planning where the presented total is frozen at presentation, so case acceptance by value cannot be rewritten by a price rise. Periodontal charting with six sites per tooth and the gingival margin named recession_mm, because it is the one field where an inverted sign produces clinically wrong output in silence. Payment plans that amortise to exactly zero, a cancellation list that does not invent a priority scheme the products do not have, and lab work as four timestamps rather than a status — because a status cannot tell you whether the fit was checked before the patient sat down.
Provisioned as reviewable code
Schema, policies, roles, bookmarks, flows and branding are declarative TypeScript rather than a schema-snapshot blob nobody can read. Fully idempotent, so a permission change arrives as a diff in a pull request instead of a click nobody witnessed.
7 languages · Retail