Project Case Study
Dentra
Enterprise Healthcare Platform · Private
A clinic platform built around one fully isolated deployment per customer, and the reference implementation that ZothKit was extracted from.
Overview
Dentra is currently deployed in production for real dental clinic operations, covering patients, appointments, treatments, clinical imaging, inventory, payments and staff.
It runs one fully isolated deployment per customer: separate container, separate database, separate object storage. That single constraint, chosen because a tenancy bug in a system holding patient records is not a degraded experience but a data breach, determined almost everything else in the architecture.
Dentra also became the proving ground for ZothKit, a private platform framework. What follows is the engineering story in six phases, including the decisions that were reversed after implementation proved them wrong.
It is one of three related products built on a shared foundation. Atlas, the property-operations platform, is where many of the patterns Dentra inherited on day one were invented, and its case study covers the commercial and entitlement engineering that Dentra deliberately kept simple.
- Fork to production-hardened
- 7 wks
- Backend modules
- 27
- Backend test suites
- 70
- Duplicate validators merged
- 4 → 1
- Refuse-to-boot gates
- 5
Deep Dive · 35 min read
The Dentra Engineering Notebook
The long-form record: every investigation, the reasoning behind each trade-off, the production learnings, and the work that was deliberately not built.
The System at a Glance
Before the story, the shape. Everything after this is a consequence of the boundary drawn here.
ONE DEPLOYMENT PER CLINIC, and everything follows from it
browser ─▶ Next.js ─▶ reverse proxy ─▶ API container
│
┌───────────────────────┼───────────────────┐
▼ ▼ ▼
Postgres object storage stdout logs
own database public + private correlated
buckets JSON
WORKSPACE
apps/ backend (27 modules) · web · operator CLI
packages/ types · permissions · ui · utils · configs
the shared contract both runtimes must agree on
SCALE one clinic's data volumes are small by design.
Almost every query is milliseconds, which is exactly why
an outlier is worth investigating rather than shrugging at.Phase 01
Inheriting a Foundation
Dentra did not start from an empty repository. It began by forking a working foundation from an earlier product in the same family: guards, interceptors, exception filters, an upload module, session services, a config-driven data table. The transplant took about 48 hours and bought roughly four months of work.
It also introduced a category of debt that took the rest of the project to name. Some of the inherited code was a decision; some of it was an artifact of a different product with a different architecture, still running because it worked well enough not to demand attention. A URL builder that reassembled paths from four environment variables was not a design, it was glue from a filesystem-only predecessor.
The lasting change was not a refactor but a question added to every review: did we decide this, or did we receive it? Inherited code is held to a different standard, because keeping it is a choice being made silently every day.
Phase 02
Modelling the Clinic Day
The clinical domain is twenty-five tables and seventeen enumerated types, and the parts worth describing are the ones where a modelling decision prevented a class of mistake rather than merely stored a fact.
An appointment moves through eight states: scheduled, checked in, waiting, in treatment, completed, cancelled, no show, rescheduled, with a separate outcome recorded when it ends. The current state lives on the appointment, and every transition is also written to an event log that records what changed, from which state to which, the queue number at the time, and the queue positions before and after a reorder. That last detail is the one that earns the table. A receptionist moves an urgent patient up the queue and someone who arrived earlier waits longer, and the question that follows is not answerable from a status column. It is answerable from an ordered record of who moved, when, and past whom.
The waiting room has a display screen, and it is deliberately the thinnest surface in the system. It shows queue numbers and nothing else: no names, no phone numbers, no procedure. It authenticates, polls every ten seconds, and fails silently on a poll error because a blank panel in a public room is better than an error message in one. A screen in a room full of strangers, attached to a database of medical records, is a place where the safest design is the one that has almost no data to leak.
Procedures record where in the mouth they happened as either a quadrant or the whole mouth, with the quadrant left null in the second case. Modelling it as two location kinds rather than an optional quadrant field means a cleaning cannot accidentally carry a quadrant and a filling cannot accidentally lack one. Medical conditions and allergies follow a catalogue and instance pattern: a clinic-wide list of known conditions, and per-patient records that reference it, so two spellings of the same allergy cannot coexist in one patient's chart.
Treatment decomposes rather than flattening. A plan holds items, items are delivered across sessions, sessions produce the treatments actually performed, and treatments consume materials from inventory. Each level exists because clinics quote at one level, schedule at another, and bill from a third.
APPOINTMENT current state on the row
scheduled ─▶ checked_in ─▶ waiting ─▶ in_treatment ─▶ completed
└──▶ cancelled └──▶ no_show └──▶ rescheduled
FLOW EVENTS how it got there, appended never updated
event · fromStatus · toStatus · queueNumber
previousQueueSortOrder ─▶ newQueueSortOrder
└─ answers "why was I seen after someone
who arrived later than me?"
WAITING ROOM DISPLAY
queue numbers only. no names, no phones, no procedures
polls every 10s · fails silently · almost nothing to leak
TREATMENT decomposes because the clinic does
plan ─▶ items ─▶ sessions ─▶ treatments ─▶ materials
quote at schedule at bill fromPhase 03
Meeting Production
The obvious architecture for a clinic SaaS is row-level multi-tenancy: one database, a tenant column everywhere, one instance to operate. It is cheaper to run and far cheaper to deploy. It was rejected, because careful query review reduces the risk of a cross-tenant leak but never eliminates it, and physical separation makes that failure structurally impossible rather than carefully avoided.
The cost was paid in operations rather than in code. Every clinic is a separate deploy, migration run, backup and credential set. There is no cluster, no rolling deploy, no shared cache to hide behind, and onboarding is a procedure rather than a signup form.
One dividend was unplanned. Because every customer is a separate deployment, the first clinic to receive a release is a natural canary, which is the staged-rollout capability blue/green infrastructure exists to provide. It arrived for free and is the reason that infrastructure has never been needed.
CLINIC A CLINIC B CLINIC C ┌──────────┐ ┌──────────┐ ┌──────────┐ │ API │ │ API │ │ API │ ├──────────┤ ├──────────┤ ├──────────┤ │ Postgres │ │ Postgres │ │ Postgres │ ├──────────┤ ├──────────┤ ├──────────┤ │ Bucket │ │ Bucket │ │ Bucket │ └──────────┘ └──────────┘ └──────────┘ no shared database · no shared cache · no shared bucket
Phase 04
The Compressed Rebuild
Ahead of the first clinic going live, the operational half of the system did not exist. Storage, deployment identity, boot gates, observability and the operator tooling were designed and built in days rather than sequentially over months, which is why they share a vocabulary.
Storage was the deepest piece. The inherited layer treated the filesystem as the database and had no concept of where bytes lived that was separable from what they were called. The rebuild split four fused concerns: a provider contract owning where bytes live, a key builder owning naming, a metadata table owning what is known about a file, and visibility owning who may see it.
The most consequential finding came from reading Cloudflare R2's access model directly. The original design used a private key prefix inside a single bucket. It was internally consistent and had been reviewed as correct, and it could not work: R2 public access is bucket-wide, so a prefix is not an access boundary however carefully it is applied. The boundary moved to the bucket, and the provider now refuses to boot on a single-bucket configuration rather than falling back to it.
The same phase established deployment identity as immutable. The deployment identifier is written into every object key and persisted across several tables, so it can never change once real data exists, while the clinic's display name is referenced by nothing and is safe to edit at any time. Conflating those two, which is the natural thing to do since both are the clinic's name, would have made the first rebrand a data migration.
Upload
│
▼
Validation magic number · MIME · size · pixel cap
│
▼
Key Builder visibility / namespace / owner / uuid
│
├─ public/… ─▶ PUBLIC BUCKET ─▶ CDN URL
│
└─ private/… ─▶ PRIVATE BUCKET ─▶ authenticated
stream only
(no public URL
can be built)Phase 05
The Correction
By this point a private framework already existed, extracted from a sibling product to stop three codebases reimplementing authentication, permissions and audit. Reading the actual chronology produced the most uncomfortable finding of the project: the framework had been extracted from a codebase that was four days old, single-tenant, with no production deployment, no object storage and no operations story.
Almost every capability worth sharing was written afterwards. Storage, deployment configuration, boot gates, request correlation, database observability and the operator CLI all postdate the framework's creation. It was not behind because it was built badly. It was behind because it was built first.
The measurements that followed were worse than the diagnosis. One package had zero consumers anywhere. Four modules in one adapter had none, and two of those had been actively reimplemented by the single product that had adopted the framework elsewhere. Exactly one duplicate implementation had ever been deleted across the entire family.
That last number reframed the strategy. Package count measures nothing; the only metric that proves the mechanism works is how many duplicates have actually been deleted. And when a product routes around a capability, the capability goes on trial rather than the product.
MAR ────── JUN ────── JUL 08 ────── JUL 22 ─────────▶
Atlas Dentra ZothKit Storage rebuild
invents forks it extracted Deployment identity
the in 48h from a Observability
patterns 4-day-old Boot gates
codebase Operator CLI
▲ │
└───────────────┘
the framework was built BEFORE the lessons it existed
to carry, and none of them were in itPhase 06
Closing the Loop
Extraction was reordered around evidence. A capability is only extracted once real duplication exists, and it is only finished once the code it replaced has been deleted. Two have completed that cycle.
File content validation consolidated four independent implementations, roughly six hundred lines doing one job. Adoption exposed a flaw in the product rather than the framework: the content check was the first gate on the upload path, so its signature table was silently both verifying that bytes matched the declared type and deciding which types the surface accepted. The fix required no framework change at all, only separating what the product had merged.
Permission evaluation unified the semantics used by the backend guard and the web client, so both runtimes now call the same functions. It also corrected a framework default: an empty requirement list had evaluated as satisfied, which on a security gate means an accidentally-empty requirement admits everyone. It now fails closed. The deciding evidence was that the only existing consumer had already written a compensating workaround with a comment explaining the framework was wrong, which is a bug report the framework never received.
One earlier conclusion was reversed outright. A review had recommended retiring a zero-consumer package as speculative. Attempting to adopt it showed the opposite: it had anticipated a genuine behavioural difference between two products that neither had reported. Zero consumers is evidence that adoption failed, not that the design is wrong.
Duplicate ─▶ Implement ─▶ Verify ─▶ Extract ─▶ Adopt ─▶ Delete
observed in the kit against into a in the the
in 2+ once the package product copy
products product │
▲ │
└────────── the loop only closes here ─────────────────┘
completed twice: file validation · permission evaluationEngineering Research
Several investigations changed the architecture without necessarily producing features. They are listed because the reasoning was the deliverable.
- A patient-number generator that failed permanently, and the fix that shipped without a migration. Numbers were allocated by taking the maximum existing one and adding one, and that query excluded soft-deleted patients while the unique index behind it did not. So a tombstoned row kept occupying its number, deleting the highest-numbered patient made the generator hand back a number that was still taken, and every subsequent create failed from then on rather than transiently. It was also a plain race: two creates in the same instant read the same maximum. The replacement is a per-clinic counter incremented atomically through an upsert, which is monotonic and never reissues a released number. The part worth copying is how it deploys: the counter seeds itself on first use from the highest number ever issued including soft-deleted rows, so no backfill is required, which matters when each customer is a separate database that might be restored from a backup taken before the change. A regex guard on the seed subquery exists because a cast over an unexpected value would raise and take patient creation down entirely.
- Configuration classification: separating identity, profile, environment and secrets by lifecycle rather than treating configuration as one concern. Deployment configuration lives in the repository as typed code, with two hard rules: no secrets, because the file is committed, and no infrastructure, because that describes where the app runs rather than who it serves. Both rules were later applied retroactively to existing code, which is the evidence they are real rather than decorative.
- Capability over identity: providers expose what an instance can actually do rather than what it is called. The flags describe the configured instance, not the vendor's brochure, so a provider without a public domain reports that it cannot produce public URLs. That honesty is what makes the branch correct rather than superstitious.
- Fail-fast startup validation: five independent checks refuse to start rather than run in an unsafe state, covering transport security, schema currency, credential completeness, provider selection and configuration validity. Where the unsafe answer is what silence produces, the system refuses to boot until the choice is stated.
- Scaffold placeholder detection: generated configuration deliberately produces values that pass shape validation, so a plausible placeholder is indistinguishable from a finished one. The validator sweeps for the generator's own markers and refuses. The failure mode of a good scaffold is that its output looks finished, and no type can express that.
- Validation as diagnostics: validators return every problem at once rather than throwing on the first, because tooling and operators both want the complete list. Having the better pattern in the same codebase is what made the weaker one visible elsewhere.
- Package-boundary investigation: a framework test helper was a strict capability upgrade and looked like a one-file adoption. Implementation showed it transitively required an entire composition layer the product does not use, even though the file itself imported none of it. Adoption was abandoned and documented, because moving the file would have created a package boundary as a side effect of an extraction, and boundaries are an architectural decision rather than an implementation convenience.
Engineering Outcomes
- Validation logic consolidated from four independent implementations into one shared capability, with the product's own copies deleted.
- Permission semantics unified across backend and frontend, with a fail-open default corrected to fail closed.
- Storage rebuilt around a provider abstraction with verified public and private bucket separation, replacing a filesystem-coupled layer that was deleted rather than wrapped.
- Five independent boot gates, plus a meaningful liveness and readiness split wired into the container health check.
- Request-level observability with automatic correlation and per-request database aggregates that make an N+1 visible on a single log line without a monitoring vendor.
- Medical-data safety enforced structurally: the query log record type has no field for parameter values or result rows, so a leak requires a visible signature change rather than reviewer vigilance.
- Deployment configuration as validated, committed code, with an operator CLI that orchestrates existing scripts rather than duplicating their logic.
- Operational runbooks covering release sequencing, rollback decisions, backup and restore, schema-change classification and clinic go-live.
- Two subsystems deliberately frozen by written decision records, each stating what was built, what was not, and the evidence that would justify reopening it.
Known Gaps
Every system this size has them, and the ones nobody mentions are the ones a reader discounts hardest. These are tracked in the repository, not just here.
- No CI. Every guarantee described above, from the route-permission matrix to the migration invariants to all 70 test suites, is currently enforced by someone remembering to run it. This is the largest single gap and the first item on the roadmap.
- No session revocation, in a system where the session cookie is the credential.
- Buffer-only file I/O, so a large radiograph holds its decoded frame in memory twice. Streaming is awkward to retrofit once external consumers exist, which is why it is scheduled before extraction rather than after.
- The provider-migration primitive is the best-reasoned code in the project and has never been run against a real migration.
- Environment configuration still has no schema, while deployment configuration is typed, defaulted and validated. The same problem was solved twice with very different care, and nobody noticed until a review put the two side by side.
Lessons Learned
- Implementation is the source of truth. Two designs that were internally consistent and carefully reviewed, storage access scoping and reverse-proxy trust, were simply wrong about how the underlying systems behaved. Only reading the primary source exposed either.
- Adoption matters more than abstraction. A capability shipped alongside the code it was meant to replace has replaced nothing.
- Fail closed. On a security primitive an empty or missing requirement must deny, and a security-relevant setting should have no default at all if the unsafe answer is what silence produces.
- Validate infrastructure before traffic. Constructing a client proves nothing; an object-store client builds happily from entirely wrong credentials and fails on first use.
- Package boundaries can violate architecture. A dependency rule can be sound in design and broken entirely by how the code is packaged, and no amount of care inside the files will fix it.
- Make the unsafe thing unrepresentable rather than forbidden. A convention that must be remembered will eventually be forgotten; a type that cannot express the mistake will not.
- Write down what you did not build, and what would change your mind. A deferral without stated reopening criteria is indistinguishable from an oversight six months later.
What I Would Do Differently
Distinct from the lessons above, which are rules that came out well. These are the calls I would make differently starting over.
- Extract the framework last, not first. It was built before the product had learned anything worth sharing, so it captured the composition patterns and none of the operational ones. Building to production first and extracting afterwards would have produced a smaller, better-evidenced framework instead of packages waiting for consumers.
- Design the storage layer around streams from the first line. Buffer-only I/O was the fastest thing to write and is the one decision that is genuinely awkward to reverse, because every call site now assumes a buffer. Nothing else in the system has that property.
- Give environment configuration the same treatment as deployment configuration, at the same time. One got a typed, validated, defaulted, placeholder-swept model with its own command; the other is still scattered accessor calls with per-call-site defaults. They are the same problem, and the gap only became visible when someone put them side by side.
- Write the boot gates as one mechanism rather than five. Transport security, schema currency, credential completeness, provider selection and configuration validity are the same idea implemented five times, and the fifth was no cheaper than the first.
- Set up continuous integration on day one. It is the cheapest item on this list and the only one still outstanding, and every month without it makes the eventual first run noisier.
Future Direction
The ongoing work is turning proven capabilities into ZothKit while keeping product-specific concerns permanently in Dentra. Clinical domain logic, schema, migrations and tenancy stay in the product; configuration, identity, storage, observability and access control are the shared surface.
Near-term priorities are continuous integration, streaming file I/O before extraction rather than after, session revocation, and a composable platform bootstrap so the security reasoning currently expressed as imperative startup code can reach the sibling products that lack it.
The measure of success is not the number of packages. The oldest product in the family took roughly four months to reach production hardening and Dentra took about seven weeks. The goal is to make the next product's number days, by shipping the container, the deployment stack, the backup tooling, the boot gates, observability and storage as a starting point rather than as things each product acquires one incident at a time.
Technologies
Explore More
Other Case Studies
Enterprise Property Operations Product
Atlas
A real-estate operations platform where the commercial model lives inside the transactions: plan limits, add-ons and capability tiers enforced under row locks rather than checked on a billing page.
Internal Enterprise Operations Product
Jinara
Internal platform designed to manage the complete lifecycle of agency operations, from client relationships and contracts to payments, responsibilities, and future project management.