Local-first 应用通常强调离线可用、数据归用户所有,以及网络恢复后自动同步。Jake Lazaroff 的分享把这个思路与 AT Protocol 连接起来:用户数据保存在个人数据服务器(PDS)中,应用则复用协议提供的同步与更新基础设施。这样一来,AT Protocol 不再只服务于社交网络,也可以成为协作文档、任务工具和其他分布式应用的底座。
从“应用拥有数据”转向“应用读取用户数据”
传统 SaaS 往往把数据库、身份、同步接口和业务逻辑绑定在一个后端中。客户端一旦离线,或者服务停止运营,用户对数据的访问能力就会明显下降。
AT Protocol 支持的方向有所不同:用户在 PDS 中维护自己的数据,应用围绕这些记录提供界面和业务能力。共享基础设施负责传播更新,应用不必为每一种客户端重新设计一套专有同步协议。
这种架构可以拆成三层:
- 本地数据层:客户端保留可直接查询和修改的数据副本,断网时仍能工作。
- 用户数据层:PDS 保存用户发布的协议记录,是跨设备同步和数据迁移的重要边界。
- 派生服务层:索引器、搜索服务或 AppView 消费更新,生成适合查询的视图,但不应成为用户原始数据的唯一存放处。
关键变化不是简单地把数据库搬到另一台服务器,而是重新划分所有权:应用可以失效或被替换,但用户记录仍应留在用户控制的数据边界内。
韧性来自哪些地方
本地副本让“能否打开应用”不再完全取决于网络。用户的写操作可以先落盘,再进入待同步队列;连接恢复后,客户端将增量推送到 PDS。即使远端暂时不可用,编辑动作也不必丢失。
共享协议基础设施还降低了对应用专用后端的依赖。开发团队仍可能需要索引、搜索、通知或权限服务,但这些服务可以被视为可重建的派生组件,而不是数据唯一真相来源。
这并不意味着冲突会自动消失。两台设备可能同时修改同一条记录,协作工具还可能出现乱序更新。应用必须明确选择冲突策略,例如:
- 对简单状态采用版本号或 compare-and-swap。
- 对独立字段执行字段级合并。
- 对富文本协作引入 CRDT 等成熟算法。
- 对无法安全自动合并的内容保留两个版本,让用户裁决。
可以这样实践:先跑通本地写入与延迟同步
下面是一个可直接运行的最小实验。它使用 SQLite 保存本地数据和 outbox,并以共享目录模拟远端 PDS。这个目录适配器只是教学替身,不是 AT Protocol 的真实 API;接入真实 PDS 时,应将 push_to_pds 替换为对应的记录写入客户端,同时保留本地事务和重试边界。
将代码保存为 local_first_demo.py,需要 Python 3.10 或更高版本:
import argparse
import json
import sqlite3
import time
from pathlib import Path
DB = Path("local.db")
REMOTE = Path("mock-pds")
def connect():
db = sqlite3.connect(DB)
db.execute("PRAGMA journal_mode=WAL")
db.executescript("""
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
text TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS outbox (
id TEXT PRIMARY KEY,
payload TEXT NOT NULL
);
""")
return db
def edit(note_id, text):
now = time.time_ns()
payload = {"id": note_id, "text": text, "updated_at": now}
with connect() as db:
db.execute(
"INSERT OR REPLACE INTO notes VALUES (?, ?, ?)",
(note_id, text, now),
)
db.execute(
"INSERT OR REPLACE INTO outbox VALUES (?, ?)",
(note_id, json.dumps(payload, ensure_ascii=False)),
)
print(f"saved locally: {note_id}")
def push_to_pds(payload):
REMOTE.mkdir(exist_ok=True)
target = REMOTE / f"{payload['id']}.json"
target.write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def sync():
with connect() as db:
pending = db.execute("SELECT id, payload FROM outbox").fetchall()
for note_id, raw in pending:
try:
push_to_pds(json.loads(raw))
except OSError as exc:
print(f"sync deferred for {note_id}: {exc}")
continue
db.execute("DELETE FROM outbox WHERE id = ?", (note_id,))
print(f"synced: {note_id}")
def list_notes():
with connect() as db:
for row in db.execute(
"SELECT id, text, updated_at FROM notes ORDER BY updated_at DESC"
):
print(row)
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
edit_parser = sub.add_parser("edit")
edit_parser.add_argument("id")
edit_parser.add_argument("text")
sub.add_parser("sync")
sub.add_parser("list")
args = parser.parse_args()
if args.command == "edit":
edit(args.id, args.text)
elif args.command == "sync":
sync()
else:
list_notes()
运行以下命令即可观察“本地先提交、随后同步”的过程:
python local_first_demo.py edit note-1 "离线时写下的内容"
python local_first_demo.py list
python local_first_demo.py sync
cat mock-pds/note-1.json
这个示例故意保持简单,只演示两个关键约束:本地修改和 outbox 写入处于同一个 SQLite 事务中;只有远端写入成功后,待同步记录才会删除。生产实现还需要幂等键、指数退避、认证、远端版本检查和冲突处理。
接入 AT Protocol 时要补齐的边界
把实验改造成真实应用时,可以定义自己的记录类型,并通过词典描述记录结构和操作约定。记录应该保持小而独立,避免每次编辑都重写一个巨大的项目文档。对于多人实时编辑,可以把快照与操作日志分开:PDS 保存可迁移的用户记录,实时协作层处理低延迟操作,再把稳定结果写回用户数据层。
还要警惕几个工程风险:
- 协议可用不等于应用可用:搜索索引或派生视图宕机时,客户端仍需提供基本的本地浏览和编辑能力。
- 数据归用户不等于自动私密:敏感协作内容需要明确的加密、密钥分发和访问撤销方案。
- 同步成功不等于语义正确:最后写入者获胜容易实现,却可能静默覆盖重要编辑。
- 共享基础设施不等于零后端:通知、反滥用、索引和实时协作仍可能需要独立服务。
采用前的检查清单
适合优先尝试 AT Protocol local-first 架构的场景,通常具有用户生成记录、跨设备访问、离线编辑和可迁移需求。落地前至少确认:本地数据库能否独立启动,远端不可用时写操作是否持久化,重试是否幂等,冲突是否可见,以及派生索引能否从 PDS 数据重新构建。
真正的韧性并不来自“没有服务器”,而是来自服务器故障时应用仍可工作、派生服务丢失后仍可重建、应用退出市场后用户仍能带走数据。AT Protocol 提供了可复用的分布式基础设施,但本地事务、冲突语义、隐私与恢复流程仍然是应用开发者必须完成的工程工作。