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, paid, and owed. Three roles — administrator, doctor, assistant — with different reach over the same records. Used daily by a working clinic, so features arrive from a real reception desk: half-hour slots, a mandatory reason on every appointment, adding a patient mid-booking with only a name and phone number.

02

Deleting the backend

The first version was a Quarkus service: hexagonal layering, Hibernate, Liquibase, MapStruct, DTOs, mappers, resource classes. It worked, but it was thousands of lines translating HTTP to SQL for a product whose actual logic is arithmetic over a handful of tables — so the browser now talks to PostgreSQL directly, through PostgREST, with a Postgres function over RPC wherever 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's no server to deploy, patch, scale or pay for — less code, one fewer network hop, and every access rule living next to the data it guards. A trade I'd make again for a product this size.

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: derived payment figures stay visible to every role even though the raw ledger is administrator-only, and taking a payment goes through functions with elevated rights that can only ever insert one row — both pinned by tests. The anonymous role is then stripped of every privilege in the public schema; 28 checks assert an unauthenticated caller is refused everywhere, signup is closed, and a client can't promote its own role.

04

What a port tells you about your own code

Porting a working system is the most honest audit available: 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, rather than against the Java's own output — otherwise you're only proving the bug moved successfully. Three defects fell out of that exercise, all of which the original had been quietly carrying.

05

Documents, without putting X-rays in a table

Patient documents live in a private bucket, fetched through short-lived signed URLs. The practice-wide storage cap is enforced by a database trigger reading each object's real size from the storage catalogue, not the size the client reports. The cap itself comes from one function, shared by the trigger and the upload UI, so a test fails if the two ever drift apart.

06

Knowing what the platform cannot carry

Before real patient records went in, I audited the hosting plan against this specific application, ranked by criticality. The deciding factor turned out to be backups: a scripted snapshot takes roles, schema, table data, auth users and every stored document, with a manifest of checksums, filed per practice — run before every production deployment, with restores actually exercised rather than assumed.

07

Shipping it

A static bundle on Cloudflare Pages, deployed from a locally built dist/, with no build configuration or repository access granted to the host. The client uses hash routing, so only index.html is ever requested and no rewrite rules are needed, and the deployed origin is registered with the auth service at build time.

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