Mohamed Melki.

Case study 02  ·  Freelance, solo  ·  2026  ·  Live, in daily use at a dental practice

DocDock

I built the backend as a Quarkus service. Then I deleted it, and the application got better.

Runs on
Vue 3 · Pinia · TanStack Query · PostgreSQL · PostgREST · hosted Auth and Storage · Cloudflare Pages
Authorisation
Row-level security. One boundary, expressed as policy, next to the data.
Verified against the live project
101 assertions driving the shipped client modules, 28 boundary checks, 46 SQL checks, clean typecheck.
DocDock's dashboard: upcoming appointments, cash-flow chart, payment status rings, appointment types and missed appointments.
One screen the practice actually starts its day on.

01

What it does

Patients, appointments, consultations, documents, certificates, prescriptions, and the money: what was billed, what was paid, what an insurer owes and what the patient still does. Three roles — administrator, doctor, assistant — with different reach over the same records.

It is used every day by a working clinic, which changes the job. Features arrive from a real reception desk: half-hour slots because that is how a chair is booked, a mandatory reason on every appointment, a way to add a patient mid-booking with only a name and a phone number, because the person is standing there waiting.

02

Deleting the backend

The first version was a Quarkus service: hexagonal layering, Hibernate, Liquibase, MapStruct, DTOs, mappers, resource classes. It worked. It was also several thousand lines of translation between HTTP and SQL, for a product whose actual logic is arithmetic over a handful of tables.

So the browser now talks to PostgreSQL directly, through PostgREST. Everything that was a Java endpoint became either a query against a table or a view, or a Postgres function called over RPC where there was genuine logic to run.

WasBecame
REST resource classesPostgREST, plus one small client module per former resource
Payment enrichment in Javaa database view that computes it once
Dashboard aggregation servicea single Postgres function
Auth service and request filterhosted Auth plus row-level security policies
Document bytes in a table columnobject storage and a path
Liquibase changelogsplain SQL migrations

There is no server to deploy, patch, scale or pay for. The trade is real and I would make it again for a product this size: less code, one fewer network hop, and every rule about who may read what living next to the data it guards. What you give up is the freedom to put arbitrary logic in the middle — which, for this application, turned out to be freedom it did not need.

03

The boundary is the whole design

With no application server, authorisation has exactly one place to live. The anonymous key shipped in the bundle is public by design; what a caller may read or write is decided by policies, and the role comes from the session rather than from anything the client sends.

DataReachable by
Patients, appointments, consultations, documentsany authenticated staff member
Certificates and prescriptionsdoctor or administrator
The financial ledgeradministrator
Calendar settingseveryone reads, administrator writes

Two things deliberately cross those lines, because the Java API had allowed them and removing them would have been a silent product change: derived payment figures stay visible to every role even though the raw ledger is administrator-only, and taking a payment goes through functions that run with elevated rights and can only ever insert one row. Both are decisions, written down, with tests pinning them.

The anonymous role is then stripped of every privilege in the public schema. That is not redundant with row-level security: the derived view and the payment functions deliberately bypass it, so for those two surfaces the grant is the only boundary there is. Twenty-eight checks assert an unauthenticated caller is refused on every surface, that signup is closed, and that a client cannot promote its own role.

04

What a port tells you about your own code

Porting a working system is the most honest audit available. You cannot hand-wave a behaviour you are reimplementing: every figure has to come out the same, so every figure has to be understood.

I checked the new payment arithmetic against values computed by hand from the Java it replaced, rather than against the Java's own output — otherwise you are only proving the bug moved successfully. Three defects fell out of that exercise, all of which the original had been quietly carrying.

The tests run the actual client modules the browser ships, bundled the same way, instead of a reimplementation of them in the test file. It is a small distinction that decides whether a green suite means anything at all.

05

Documents, without putting X-rays in a table

Patient documents live in a private bucket and are fetched through short-lived signed URLs. The practice-wide storage cap is enforced by a database trigger that reads each object's real size from the storage catalogue rather than trusting the size the client reports — a client-reported number is a suggestion.

The cap itself comes from one function, not from a literal repeated in two places. The interface refuses an oversized upload using the same figure the trigger enforces, and a test fails if the two ever drift apart. Moving the bytes out of PostgreSQL is also what keeps this deployment inside a free database tier: twenty radiographs as table data would have consumed the whole allowance.

06

Knowing what the platform cannot carry

Before real patient records went in, I wrote a study of the hosting plan against this specific application: every limit, what it does to this code, and what it would take to treat it — each finding ranked by criticality rather than by how interesting it was.

The conclusion was not a capacity number. It was that the deciding factor is backups, so backups exist: a scripted snapshot takes roles, schema, table data, the auth users and every stored document, with a manifest of checksums, filed per practice. It runs before every deployment to production, and a restore has been exercised rather than assumed.

The rest of the study is a list of things that are fine now and the trigger that would make each one urgent. Some are code, not plan limits, and would need doing on any tier — worth writing down precisely because paying more money would hide them rather than fix them.

07

Shipping it

A static bundle on Cloudflare Pages, deployed from a locally built dist/. There is no build configuration in the host and no repository access granted to it, which is one fewer system holding a key to the source.

Two details that decide whether this is pleasant or fiddly: the client uses hash routing, so only index.html is ever requested and no rewrite rules are needed; and the project's public values are inlined at build time, which means the deployed origin has to be registered with the auth service — the step whose omission produces a wall of CORS errors and a bad afternoon.

Next case study Carini Care plans, consent, and a closed money loop