Skip to the case

Case — Clinical platform · 2025—26

Vera

AudGraph — audiology management and decision support

One system, two shapes of data. Patient identity is relational and rigid. Clinical reasoning is not relational at all — it is a network. Rather than compress both into one database, each was given the store that fits it.

Vera is an end-to-end clinical management platform for hearing-aid centres, built freelance under VeraHQ. Its own promise is a single line —"Kliniğinizin dijital kalbi", the clinic's digital heart — and the architecture behind it is the part worth writing down.

  • PostgreSQL
  • Neo4j · Cypher
  • Weighted graph traversal
  • Hash-chained audit log
  • AES-256-GCM
  • Server-sent events
Role
Freelance developer under VeraHQ, 10/2025 — present
Product
verahq.com.tr — repository Vera-Odyoloji-Yonetim-Sistemi, v0.3.0
Scope
Five modules, two databases, full Turkish/English i18n
Constraints
KVKK and ISO 27001 compliant, 24/7 Turkish support
-1001020304050607080901001101201252505001k2k4k8kFREQUENCY (Hz) — ONE OCTAVE PER COLUMNLOGARITHMICHEARING THRESHOLD (dB HL)NORMAL RANGE — CONVENTION↓ more hearing lossSYNTHETIC — DRAWN AT BUILD TIME, NOT A MEASUREMENT
  • Right ear, air
  • Left ear, air
  • Right ear, bone
  • Left ear, bone
Figure 1 — the audiogram, the chart this whole product is organised around.The axis layout is standard audiometry: frequency left to right, one octave per column, so the spacing is logarithmic; threshold top to bottom, so descending the chart means losing hearing. The shaded band is the conventional normal range down to 25 dB HL — a convention, not something this system measured. Every threshold plotted here is synthetic and written into the page source; it is generated deterministically at build time and is identical in every build. Derived from the drawn curves: pure-tone average 28.3 dB HL right and 33.3 dB HL left — arithmetic means of the 500, 1000 and 2000 Hz thresholds; a 50 dB fall from 125 Hz to 8 kHz on the right trace (65 − 15); and an air–bone difference never exceeding 5 dB, so the two conduction paths track each other.
Scene 02 — Problem

Two shapes of data, one habit

A hearing-aid centre's system has to hold two things at once, and they are not the same kind of thing. The first is patient identity: who this person is, when they came in, which device they were fitted with, what was invoiced, what is in stock. That has a fixed shape. Every row has the same columns, the columns mean the same thing every time, and one wrong row is a real-world error somebody has to answer for.

The second is clinical reasoning, and it has no fixed shape at all. Which other patients presented like this one? Which device was fitted after which finding, and what happened at the next visit? What connects this diagnosis to that one, three hops away, through a device nobody thought to compare? Those are questions about a network: variable depth, variable degree, and an answer that is a weighted path rather than a set of rows.

The habit is to pick one database and make the other shape fit. Model the graph as join tables and walk it with recursive CTEs; it works, right up to the point where the traversal depth stops being known in advance and what you actually want back is a ranking. Or go the other way — put identity in a graph and give up the constraints and transactional guarantees that make identity safe to trust in the first place.

The question that settled it

Not "which database is better", which has no answer, but "what shape is this data, and what is this store good at holding".

Two answers, so two stores.

That is a cost, not a free win — two stores mean two things to run, two things to back up, and a boundary between them that has to be decided rather than discovered. It was worth paying because the boundary here is obvious: identity on one side, relationships on the other.

ARelational — one shape, repeated

PATIENTSVISITSdeclared join

Fixed columns. Interchangeable rows. Joins written down in advance, and constraints that refuse anything else.

BNetwork — every node a different shape

3 hops · degree 2 to 5RELATIONSHIPS

Variable degree, variable depth. The interesting answer is a path and a weight, and nobody knows how long it is until it is walked.

Figure 2 — the same system, two shapes. The point of the drawing is not that one shape is better. It is that they are genuinely different, and a storage engine is good at one of them. Node radius on the right is derived from degree — the count of edges touching each node — so the unevenness is arithmetic rather than decoration. Schematic, generated at build time; not a capture of anything.
Scene 03 — Approach

Give each shape the store it deserves

PostgreSQL holds relational data and patient identity. Neo4j holds clinical relationships, diagnosis history and the similar-patient algorithms. One system, two stores, and a boundary drawn by ownership rather than by convenience.

On the relational side nothing exotic happens, and that is the intent. Identity wants a schema that refuses malformed data, transactions that either happen or do not, and constraints that are the same on Tuesday as they were on Monday. A clinical record is a legal document as much as a technical one; the store under it should be boring.

On the graph side the shape of the question changes. The Clinical Network Map module renders the relationships between patient, visit, diagnosis, disease and device as an interactive graph, and differential diagnosis is answered by weighted traversal across it: start at the visit, follow the edges that carry weight, and accumulate. Patient, Disease and Device nodes are indexed automatically, so the entry points into a traversal do not have to be scanned for.

What the traversal returns is a ranking for a clinician to read, not a verdict. The system is decision support; it puts candidates in an order and shows the weights that produced the order. Anything stronger than that would be a claim this software has no business making.

Who owns what

by shape

PostgreSQL

  • Relational data
  • Patient identity

Neo4j — Cypher

  • Clinical relationships
  • Diagnosis history
  • Similar-patient algorithms
  • Automatic indexing on Patient, Disease and Device nodes

Modules

five
Dashboard
The clinic at a glance
Patients
Identity, records, history
Network Map
Relationships as an interactive graph
Diagnosis
Weighted traversal for differential diagnosis
Audiogram
Thresholds per frequency, per ear
0.920.740.880.660.690.580.120.440.390.05PATIENTthis visitHigh-frequency shiftSymmetric across earsNo air–bone gapAge-related pattern1.42Noise-related pattern0.98Conductive component0.04Asymmetry — flag for review0.09VISITFINDINGS — WEIGHT × PRESENCECANDIDATES — ACCUMULATED SCORE
Figure 3 — weighted traversal, schematically. Every weight here is invented for the drawing; the figure shows the mechanism, not clinical guidance, and the real system ranks candidates for a clinician rather than deciding anything. Each candidate's score is the sum of the products along the two-hop paths that reach it, computed at build time: 0.92×0.66 + 0.74×0.58 + 0.88×0.44 = 1.42 for the top-ranked one. The detail worth pausing on is that the heaviest single two-hop path (0.92 × 0.69 = 0.63) ends at Noise-related pattern, which is not the winner. Three findings feed Age-related pattern and only two feed the other, so accumulation decides the ranking rather than the single strongest edge. That is the whole reason the question wants a traversal instead of a lookup.

Ranking — each candidate's score as a share of the highest

1.42score
Age-related pattern — 100% of the top score
0.98score
Noise-related pattern — 68.7% of the top score
0.09score
Asymmetry — flag for review — 6.2% of the top score
0.04score
Conductive component — 3.1% of the top score

Synthetic weights, and the arc is a real ratio rather than a decorative fill: each score divided by 1.42, the highest of the four.

Scene 04 — Obstacle

Trust cannot live in the application layer

A clinical system is defensible only as far as its audit trail is. If the trail is written by the application, then every code path that forgets to write is a hole — and every code path that can write can also rewrite.

That is not a hypothetical failure mode, it is the ordinary one. Audit logging added as a helper function is audit logging that a refactor can quietly drop, that a migration script can bypass, and that anyone with write access to the table can edit afterwards without leaving a mark. The log looks complete precisely because the thing that would have shown the gap is the log.

So the guarantee was moved below the application. The audit log is append-only and hash-chained, and it is a database trigger that enforces it: the application layer cannot bypass the trigger, because the trigger is not in the application layer. Two independent properties come out of that. The trigger refuses the write — an update or a delete against the log does not succeed. And the chain testifies — because each entry's digest is computed over the previous entry's digest, altering a row in the middle leaves every row after it unable to reproduce its own link.

Patient data itself is encrypted with AES-256-GCM, and the platform is run under KVKK and ISO 27001 compliance. Those are constraints the product operates within rather than achievements of the architecture — but the append-only chain is what makes them checkable instead of merely asserted.

Turning point

A rule that lives in the application is a rule that survives until somebody forgets it. Put it in the database and forgetting stops being possible.

Enforced below

not by convention
  • Append-only — updates and deletes are refused
  • Hash-chained — each entry's digest covers the previous digest
  • Enforced by a database trigger, not by application code
  • AES-256-GCM for patient data

#0141visit opened

prev
00000000
hash
7c8f7009

verified

#0142thresholds recorded

prev
7c8f7009
hash
478319de

payload edited — recomputes to 66996976

altered

#0143device assigned

prev
478319de
hash
79e02c34

unverifiable

#0144report exported

prev
79e02c34
hash
f74cb832

unverifiable

Figure 4 — what the chain does when a row is altered. Each entry's digest is computed over the previous entry's digest together with its own payload, so the entries are linked rather than merely ordered. Editing entry #0142 changes its digest from 478319de to 66996976, and the 2 entries after it can no longer reproduce their own prev value — that count is arithmetic, 4 entries minus the 2 up to and including the altered one. The digests shown are a short FNV-1a hash computed at build time purely so the divergence on this page is real rather than drawn; the shipped system's digest algorithm is not what is illustrated here. In practice the edit never gets this far: the trigger refuses it, and the chain is the second line — the one that still works if somebody reaches the table directly.
Scene 05 — Result

What shipped, and what was not measured

Vera runs in production for hearing-aid centres across Turkey. The figures below are counts and properties of the delivered system — none of them is a performance claim, because no performance comparison was run.

40+

Hearing-aid centres running the platform across Turkey.

2stores

PostgreSQL and Neo4j, divided by the shape of the data rather than by module.

5modules

Dashboard, Patients, Network Map, Diagnosis, Audiogram.

3labels

Node labels indexed automatically in Neo4j — Patient, Disease and Device; the count is those three named types.

Operational detail

shipped
  • Stock is entered from a phone by scanning a QR code, and the desktop view updates over server-sent events — the person at the counter does not refresh anything, and the browser does not poll.
  • Deploys run a smoke test afterwards and roll back automatically when it fails, so a bad release is a short outage rather than a phone call.
  • Turkish and English throughout, via react-i18next, switchable in one click — both languages are complete rather than one being a fallback.
  • Process management on PM2; the client is React 19 on Vite, with Recharts for the chart surfaces and react-force-graph-2d for the network map.

What was not measured

  • No benchmark comparing this two-store design against a single-store one. The claim is fit, not speed.
  • Traversal latency was not measured for this write-up. The ranking is described, not timed.
  • The deployment count comes from the product site, not from an instrumented count I ran myself.
  • Every figure on this page is drawn from synthetic values. No patient record, screenshot or client name appears anywhere.
Delivered properties of the Vera platform and where each comes from
PropertyValueDetail
Centres40+Hearing-aid centres running it across Turkey
Stores2PostgreSQL and Neo4j, split by the shape of the data
Modules5Dashboard, Patients, Network Map, Diagnosis, Audiogram
Languages2Turkish and English — react-i18next, one-click switch
Indexed node labels3Patient, Disease and Device, indexed automatically in Neo4j
EncryptionAES-256-GCMPatient data
Audit logHash-chained, append-onlyEnforced by a database trigger
ComplianceKVKK · ISO 27001Stated by the product, with 24/7 Turkish support
Version0.3.0Repository Vera-Odyoloji-Yonetim-Sistemi

Client

React 19 · Vite · Recharts · react-force-graph-2d

Server

Node.js · Express

Stores

PostgreSQL · Neo4j (Cypher)

Process

PM2 · post-deploy smoke test with automatic rollback

Scene 06 — Close

What this case is evidence of

A system holds more than one shape of data. Choose storage by shape, not by habit.Ali Batuhan Özsürmeli

The technical content here is polyglot persistence and a hash-chained audit log. The transferable part is narrower and more useful: noticing that two questions in the same product have different shapes, being willing to pay the operational cost of answering them separately, and then putting the guarantee that matters underneath the code rather than inside it.

The same move shows up elsewhere in this work. Şeffaf Şehir puts a privacy rule into the type system so the compiler enforces it; Limon Akademi pushes tenant isolation into PostgreSQL row-level security so no endpoint has to remember. Vera is the same instinct applied to an audit trail — the rule belongs one layer below the place where it can be forgotten.

Stack

applied
  • React 19
  • Vite
  • Recharts
  • react-force-graph-2d
  • Node.js
  • Express
  • PostgreSQL
  • Neo4j · Cypher
  • PM2
Engagement
Freelance, VeraHQ · 10/2025 — present
Product
verahq.com.tr