---
title: Migraine Tracker
description: >-
  A migraine-tracking mobile app whose export is real FHIR 4.0.1: attacks modelled as Observations coded in SNOMED CT
  and HL7 terminology, not ad-hoc JSON dressed up to look like it. iOS and Android, works offline.
lang: en
source: https://jimrarras.com/en/projects/migraine-tracker/
fetched: '2026-09-19T05:53:17.675Z'
---

Lead Developer

A migraine-tracking mobile app whose export is real FHIR 4.0.1: attacks modelled as Observations coded in SNOMED CT and HL7 terminology, not ad-hoc JSON dressed up to look like it. iOS and Android, works offline.

## Stack
## The ten-minute appointment

Anyone living with migraine keeps notes: in a notebook, in the phone’s notes app, in screenshots. At the neurologist’s appointment all of that becomes a spoken summary, and the doctor re-types whatever fits. I built an app that records the attack while it is happening and produces a file the doctor’s system reads on its own.

## Logging while the attack is still on

Logging happens with the pain still going, so it has to be short: intensity, where in the head, symptoms, what came before it, what was taken. A timer keeps the attack open and closes it when it passes.

Nothing waits for a network. The entry is written on the device first and synced afterwards, because an attack does not wait for you to find signal.

![Pixel-art illustration of the logging screen: an intensity bar, a duration field, icons for food, weather and sleep with checkboxes, and a strip of dates](https://jimrarras.com/images/projects/migraine-tracker/tracker-screen.webp)

An illustration of the logging screen, not a screenshot with real data

How it works

- **What an entry holds:** intensity 0 to 10, start and end time, duration type, pain sites on a head map, symptoms with a severity, triggers, medications with a dose and whether they helped, and free notes.
- **Local first:** a local SQLite database with a write queue, so the screen never waits on the server.
- **Sync:** a bidirectional `/api/sync` endpoint with last-write-wins, the right call for one person’s own data: simple, with no conflict-resolution screen.
- **Deletes:** removed entries travel as tombstones, so a delete does not reappear from another device.
- **Backend:** Express 5 with Drizzle ORM over PostgreSQL, with Better Auth.

## Patterns, not impressions

After a few weeks the app starts answering questions nobody remembers accurately: how often, how badly, what tends to come first, which medication actually works.

The weather is recorded with every attack, so a change in barometric pressure is a measurement rather than a feeling.

![Pixel-art illustration of the trends screen: a frequency chart, a row of trigger icons and a list of medications with times](https://jimrarras.com/images/projects/migraine-tracker/triggers.webp)

An illustration of the trends screen

How it works

- **Weather:** temperature, humidity, condition and barometric pressure are stored with each entry, along with the pressure change over the previous 24 hours.
- **Analysis:** frequency per period, trends, sensitivity per trigger and a risk reading for the day, all computed from the entries themselves.
- **Medications:** doses, reminders and a taken-history per medication.
- **Checks:** 793 automated tests across 115 files cover the calculations, the sync and the screens.

## A file the doctor’s system can read

![Pixel-art illustration of the clinical data screen: cards for an observation, patient details and medication information, with download and share buttons](https://jimrarras.com/images/projects/migraine-tracker/fhir-export.webp)

The export gives either a PDF to read or FHIR to import

Most health apps hand over a PDF. Here there is a second road: the history comes out as FHIR, the language hospital systems already speak, with international medical codes instead of free text. The doctor imports it.

The key the doctor reads with is issued by the patient, who names it, gives it an expiry date, and can revoke it whenever they want.

How it works

- **What is exposed:** a REST API publishing a CapabilityStatement with `fhirVersion` 4.0.1, serving Patient, Observation (read and search by date, `_count`, `_sort`) and MedicationStatement as `application/fhir+json`, with errors as OperationOutcome.
- **An attack as an Observation:** `status` final, category `survey` from the HL7 terminology, `code` set to SNOMED CT 37796009 (Migraine), a `subject` reference to the Patient, and `effectivePeriod` for start and end.
- **Intensity:** carried as `valueInteger` on a 0 to 10 scale, with `interpretation` from the HL7 v3-ObservationInterpretation system.
- **Sites and symptoms:** 8 SNOMED codes for head regions in `bodySite` and 6 for symptoms as components, each with a severity.
- **Duration and triggers:** components under SNOMED codes 103335007 and 134296009, with the trigger itself as a text value.
- **Access:** the endpoint accepts either the patient’s session or a Patient Access Token, stored only as a SHA-256 hash, with a label, a scope, an expiry and a revocation.

## The data stays the patient’s

A pain diary is one of the most personal records a person keeps. Nothing leaves without an explicit action, and the user can take all of it with them or delete it.

How it works

- **Export and deletion:** a GDPR endpoint returning everything on the account, and account deletion behind a confirmation.
- **Device health:** the `health-records` HealthKit entitlement on iOS and Android Health Connect through an in-house expo plugin, so history that already exists does not have to be typed again.
- **Lock:** an app-lock overlay when the app goes to the background.
- **Error tracking:** Sentry on both the app and the backend.

## Links

## Related work

- Repo: `jimrarras/migraine-app` (private)

- Zosimades Healthcare Ops Stack: other healthcare software, but institutional ops rather than personal health
- collection-app (part of the Zosimades stack): another offline-first mobile app
