为什么你的健康数据应该保存在你的设备上(而不是云端):Local-First Manifesto

发布: (2025年12月27日 GMT+8 00:00)
7 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将按照要求保留源链接、格式和技术术语进行简体中文翻译。

介绍

说实话,你有多少个患者门户的登录账号?

我有一个给我的全科医生,一个给牙医,一个给三年前看过的专科医生,还有一个给我的眼科医生。它们之间互不通信。如果我失去互联网连接(或者他们的服务器决定打个盹),我就完全无法访问自己的病史。

多年来,我们一直以标准的 SaaS 思维构建健康应用:服务器至上。客户端只是一个通过 REST 或 GraphQL 请求数据的笨终端。

但对于像个人健康记录(PHR)这样敏感且关键的东西,这种架构……嗯,有点儿问题。

我最近一直在尝试一种不同的方法:本地优先架构。是时候把模型颠倒过来了。

The Problem with “Centralized by Default”

当我们将健康数据存放在一家初创公司拥有的集中式 SQL 数据库中时,会遇到三大难题:

  • Privacy is a Policy, not a Guarantee: 你必须相信管理员不会查看这些记录,或者他们已经对静止数据进行了正确的加密。
  • Latency & Connectivity: 曾经在医院地下室信号为零的情况下尝试调取疫苗接种记录吗?那简直是噩梦。
  • Data Sovereignty: 如果这家初创公司倒闭,你的健康历史将会消失。

进入本地优先软件 (LoFi)

本地优先背后的理念很简单:数据首先存储在你的设备上。 云端仅仅是同步中继或备份,而不是唯一的真相来源。

如果我构建一个个人健康记录应用,我希望它的感觉像我拥有的文件——比如 .txt 文件或电子表格,但拥有良好的用户界面。

技术栈:SQLite + WASM

几年前,在浏览器中运行关系型数据库仍是遥不可及的梦想。你只能使用 localStorage(lol)或 IndexedDB(它的 API 只有母亲才会爱)。

现在,多亏了 WebAssembly(WASM),我们可以直接在浏览器中运行 SQLite。它速度快、使用 SQL,并且具备持久化。

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

使用这种设置,用户拥有 .sqlite3 文件。他们可以下载、删除该文件,并且在飞行模式下也能无缝工作。

但是……我们如何同步?(CRDT 的魔法)

这里才是难点。如果我在手机上更新了过敏信息,而我的伴侣在 iPad 上更新了紧急联系人,并且我们都处于离线状态……重新连接后会发生什么?

在普通的 SQL 设置中,你会遇到冲突。通常“后写入者胜出”会导致数据丢失。

对于健康应用来说,我们不能承受数据丢失。这时 Conflict‑free Replicated Data Types(CRDT) 就派上用场了。像 YjsAutomerge 之类的工具把数据视为一系列更改的流,而不是静态快照。它们确保无论更改以何种顺序到达,所有设备上的最终状态都是一致的。

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']

我们不需要智能的后端 API。只需要一个“哑巴”中继服务器,在设备之间传递这些加密的二进制块即可。服务器甚至不知道这些数据到底 是什么

隐私设计

这种架构解决了医疗技术领域最大的难题:信任。

如果在将 CRDT 更新发送到同步服务器之前在客户端对其进行加密(端到端加密),服务器实际上根本无法读取你的健康数据。它只是在转发毫无意义的字节。

我在我的另一篇博客中写了更多关于这些架构模式和技术指南的内容,深入探讨我们如何构建更好的数字健康工具。

仍未完美(尚在完善中)

Local‑first 仍然是前沿技术。

  • 大型数据集: 通过 WASM 将 5 GB 的医学影像历史加载到浏览器中尚未完全实现(但我们已经接近了)。
  • 迁移: 客户端数据库的模式更改很棘手。你必须编写在用户设备上运行的迁移脚本,而不是在服务器上。

但说实话?权衡是值得的。

我们需要停止把健康数据当作存放在弗吉尼亚服务器农场的社交媒体帖子来对待。它是你的身体;它应该是你的数据库。

你尝试过使用 sqlite-wasm 或通用 CRDT 吗?在评论中告诉我吧。我仍在摸索处理 blob‑storage 同步的最佳方式!

祝编码愉快! 🏥💻

Back to Blog

相关文章

阅读更多 »