DEV Community

Beck_Moulton
Beck_Moulton

Posted on

Why Your Health Data Belongs on Your Device (Not the Cloud): A Local-First Manifesto

Let’s be real for a second. How many patient portals do you have logins for?

I have one for my GP, one for the dentist, one for that specialist I saw three years ago, and another for my eye doctor. None of them talk to each other. If I lose my internet connection (or if their server decides to take a nap), I have zero access to my own medical history.

For years, we've been building health apps with the standard SaaS mindset: The Server is God. The client is just a dumb terminal that begs for data via REST or GraphQL.

But for something as sensitive and critical as Personal Health Records (PHR), this architecture is... well, kind of broken.

I’ve been experimenting lately with a different approach: Local-First Architecture. It’s time we flip the model upside down.

The Problem with "Centralized by Default"

When we put health data in a centralized SQL database owned by a startup, we run into three massive headaches:

  1. Privacy is a Policy, not a Guarantee: You have to trust that the admins aren't looking at the rows, or that they encrypted the data at rest properly.
  2. Latency & Connectivity: Ever try to pull up a vaccination record in a hospital basement with zero signal? It’s a nightmare.
  3. Data Sovereignty: If the startup goes bust, your health history evaporates.

Enter Local-First Software (LoFi)

The idea behind Local-First is simple: The data lives on your device first. The cloud is just a synchronization relay or a backup, not the source of truth.

If I build a Personal Health Record app, I want it to feel like a file I own. Like a .txt file or a spreadsheet, but with a nice UI.

The Stack: SQLite + WASM

A few years ago, running a relational DB in the browser was a pipe dream. You had localStorage (lol) or IndexedDB (which has an API only a mother could love).

Now, thanks to WebAssembly (WASM), we can run SQLite directly in the browser. It’s fast, it’s SQL, and it’s persistent.

Here is a messy little snippet from a prototype I was hacking on last night using sqlite-wasm. It initializes a local DB for patient vitals:

import sqlite3InitModule from '@sqlite.org/sqlite-wasm';

const startDB = async () => {
  const sqlite3 = await sqlite3InitModule({
    print: console.log,
    printErr: console.error,
  });

  const oo = sqlite3.oo1; // object-oriented API

  // Storing this in OPFS (Origin Private File System) so it persists!
  const db = new oo.OpfsDb('/my-health-data.sqlite3');

  db.exec(`
    CREATE TABLE IF NOT EXISTS vitals (
      id TEXT PRIMARY KEY,
      type TEXT NOT NULL,
      value REAL NOT NULL,
      timestamp INTEGER
    );
  `);

  console.log("Local DB is ready to rock.");
  return db;
};

// TODO: add error handling later lol
Enter fullscreen mode Exit fullscreen mode

With this setup, the user owns the .sqlite3 file. They can download it. They can delete it. It works seamlessly in Airplane Mode.

But... How do we Sync? (The Magic of CRDTs)

Here is the hard part. If I update my allergies on my phone, and my partner updates my emergency contacts on the iPad, and we are both offline... what happens when we reconnect?

In a normal SQL setup, you get a conflict. "Last write wins" usually destroys data.

For a health app, we can't afford to lose data. This is where Conflict-free Replicated Data Types (CRDTs) come in. I know, the name sounds like a boring academic paper, but they are magic.

Tools like Yjs or Automerge treat data like a stream of changes rather than a static snapshot. They ensure that no matter what order the changes arrive in, the final state is identical on all devices.

Imagine a simple JSON CRDT for a medication list:

import * as Y from 'yjs';

// The document holds our data
const ydoc = new Y.Doc();
const meds = ydoc.getArray('medications');

// Device A adds Ibuprofen
meds.push(['Ibuprofen - 200mg']);

// Device B adds Amoxicillin (while offline)
meds.push(['Amoxicillin - 500mg']);

// When they sync...
// Both arrays merge perfectly. No merge conflicts.
console.log(meds.toArray()); 
// Result: ['Ibuprofen - 200mg', 'Amoxicillin - 500mg']
Enter fullscreen mode Exit fullscreen mode

We don't need a smart backend API. We just need a "dumb" relay server to pass these encrypted binary blobs between devices. The server doesn't even know what the data is.

Privacy by Design

This architecture solves the biggest issue in MedTech: Trust.

If you encrypt the CRDT updates on the client side before sending them to the sync server (End-to-End Encryption), the server literally cannot read your health data. It’s just shuffling meaningless bytes.

I actually wrote a bit more about these kinds of architectural patterns and tech guides over on my other blog, where I dive deeper into how we can structure better digital wellness tools.

It's Not Perfect (Yet)

Local-first is still bleeding edge.

  1. Large datasets: Loading a 5GB medical imaging history into the browser via WASM isn't quite there yet (though we're getting close).
  2. Migrations: Schema changes on a client-side DB are tricky. You have to write migration scripts that run on the user's device, not your server.

But honestly? The trade-offs are worth it.

We need to stop treating health data like social media posts living on a server farm in Virginia. It’s your body, it should be your database.

Have you tried building with sqlite-wasm or generic CRDTs yet? Let me know in the comments. I'm still trying to figure out the best way to handle blob storage sync!

Happy coding! 🏥💻

Top comments (1)

Collapse
 
xwero profile image
david duymelinck

In a normal SQL setup, you get a conflict. "Last write wins" usually destroys data.

It has nothing to do with SQL. It is a choice how to handle data.
The last write wins can be based on when the request arrives on the server, but a more error proof method is to add the date of the chance as a part of the request.

The problem with the allergies and emergency contact changes is that the data synchronization should only contain the differences between the state online and the device/offline app.
Those two changes should not override each others data.

CRDT's can be a tool, but it is not a requirement. There are easier solutions.

I wasn't aware of OPFS, so thank you for bringing that to my attention.