Htracker — case study
A personal health dashboard I designed and built single-handedly — from the database schema to the interface. Here are the decisions I made, and why each one is the way it is.
Why
Htracker is a personal health dashboard. The patient keeps monthly snapshots of their metrics (height, weight, blood pressure, pulse, temperature, well-being, sleep), sees the trends across them, records examinations, lab results and consultations, describes conditions and treatment courses with a timeline, attaches files, and opens access to the record to their attending doctor. There's also a small admin panel for managing users.
For me it was not just a product but a testbed: a real domain on which I could honestly check how a well-thought-out architecture holds up from the database schema to the interface, when you do everything yourself and own every decision.
Demo and code — on request
Htracker is deployed and running on test data. There are demo accounts for three roles — patient, doctor and admin — so you can look at the system from each side. I don't publish access to the demo or the code (a private repository) openly, so bots don't scrape it — drop me a line and I'll hand over logins or walk you through the project and architecture in person.
How it's built
The backend is FastAPI and PostgreSQL, built on Domain-Driven Design. The system is cut into eight bounded contexts (Identity, MedicalRecord, Examinations, Analyses, Consultations, Conditions, DoctorAccess, References), and inside each are four layers: domain (aggregates and invariants, without a single framework import), application (use cases), infrastructure (SQLAlchemy and repositories), api (the HTTP wrapping). Dependencies point strictly in one direction, and contexts talk only through a neighbor's application use cases and through lazy imports — so each context stays separately bootable.
The frontend is Vue 3 / TypeScript on Feature-Sliced Design: the frontend mirror of the same rule, "links between modules — only through public entry points". The frontend types are generated from the OpenAPI spec, so the API contract and the client physically cannot drift apart. Auth is a JWT in an httpOnly cookie with a silent refresh on 401. There's a self-documenting design-system page at /design.
Decisions I made on this project
Metric trends are a read-model, not a table
A monthly snapshot (HealthCardStatic) is an ordinary row in the database, immutable once the month is closed. But the trend data (averages, slope, deviations from the norm, sparkline) is stored nowhere — it's computed on the fly by a pure function over the requested window.
Why: the window is configurable by the client (1–36 months), and materializing would mean doing so for each one. The data is small — aggregation in Python is instant, and the calculator stays a pure function that's trivial to cover with a unit test. The database isn't complicated by it. When the data grows large, the evolution point is already marked out: a materialized view with a fallback to the same function.
Privacy by design: the admin doesn't see medical data
The single authorization point for all medical resources is the require_patient_access(pid) guard. A patient passes if it's their record; a doctor — if there's an active link with the patient; the admin always gets a 403.
Why: the admin is an operational role (creates users, looks at links), not a supervisory one. Access to diagnoses shouldn't be a side effect of administrative rights. All the auth logic is gathered in one place — any change to the rules goes there, rather than being smeared across the routes.
Attachments — one flat route instead of a route per record type
Files (PDF/JPEG/PNG) live as metadata in JSONB right on the parent record, and are served through a single /files/{fid}: a shared resolver finds the owner by attachment_id and applies the same access check.
Why: the alternative is a download endpoint of its own on each of the three owners, with duplicated auth logic. The flat route gives the frontend one URL regardless of the record type. And the binary is deliberately not in the database and not in a separate polymorphic table: the file itself sits behind a Storage protocol (local FS now, S3/MinIO a trivial swap), and the metadata is read atomically together with the aggregate.
The architecture's boundaries are checked by the linter, not the reviewer
On both the frontend and the backend, the direction of dependencies isn't a verbal agreement. On the frontend it's held by ESLint with eslint-plugin-boundaries and Steiger, gated by pre-commit + CI.
Why: an architecture that's followed "in good faith" degrades at the first deadline. When a layer violation is a red CI rather than a review comment, the rule lives on its own. I deliberately didn't turn on style rules — the linter guards only the structure.
What came out and what stayed out of scope
The MVP core is taken to a working state and deployed: registration with an auto-created dashboard, monthly cards and trends, CRUD for examinations/lab results/consultations with attachments, conditions and treatment courses with links, a "doctor ↔ patient" invite flow, an admin panel, a design system, OpenAPI with three UIs.
Deliberately out of MVP scope: S3 storage (the protocol is ready, but a single host doesn't need it yet), age- and sex-dependent reference ranges, notifications, 2FA, multi-tenancy. These aren't "didn't get to it" but decisions deferred by priority — for each one, an extension point is marked out in the architecture. Honestly: the frontend architecture is still maturing (the FSD refactor went in iterations), and not everything planned for the design system made it all the way.
How it's deployed
Production is a Docker stack on my server: PostgreSQL, FastAPI (migrations are applied at startup) and nginx with the SPA's static files. In front is a host nginx with TLS from certbot, single-origin (everything under /api, no CORS). A rollout is triggered not by a push but by publishing a release: the CD runner builds images from the tag and brings the stack up. Between "ready" and "rolled out" there's a human gate — I'm calmer that way.
This same approach — my own managed server, Docker, nginx, CD — I consider part of an engineer's work, not something "not mine": the whole product includes how it lives in production too.