モバイルアプリやローカルファースト志向のプロダクトで、オフライン中の書き込みをどう同期させるか設計している方に向けた内容です。読み込みのオフライン対応は、SQLite(軽量な組み込みデータベース)にデータを置いてローカルで参照するだけなので、比較的シンプルに実現できます。難しいのは書き込み側で、複数端末が同じ行を編集した場合の整合性や、サーバー応答が失われたときの再送処理など、考慮すべき論点が一気に増えます。
この領域は「オフラインファースト同期」と呼ばれ、CRDT(Conflict-free Replicated Data Type、競合しないよう設計されたデータ構造)を使った合意形成の研究が進んできました。ただし実務でつまずくのは合意アルゴリズムそのものより、権限管理・再起動後の復旧・スキーマ変更への耐性・デバッグのしやすさといった、地味だが壊れると致命的な非機能要件です。この記事では、それらの設計判断をどう整理すればよいか、既存の同期アーキテクチャの違いを手がかりに解説します。
オフライン書き込みが難しい理由を分解する
オフライン対応クライアントの状態は、大きく2層に分けて考えると整理しやすくなります。ひとつは「確定済みのサーバー状態」、もうひとつは「未確定の操作の順序付きキュー(pending intent)」です。
このpendingキューは、単なるHTTPリクエストの配列であってはいけません。アプリが強制終了しても消えないよう、ローカルデータの更新とキューへの追加を同一トランザクションでコミットする必要があります。これは俗に「アウトボックスパターン」と呼ばれる設計で、メッセージングシステムでも使われる考え方です。
さらに、すべての操作には再送しても安全なように一意な識別子(冪等キー)を持たせる必要があります。サーバーが書き込みを処理したのに応答パケットだけが消えた場合、クライアントは「成功したか失敗したか分からない」状態になるためです。識別子があれば、同じ操作を再送してもサーバー側で重複適用を防げます。
認可(authorization)が絡むとさらに厄介になります。端末がオフラインの間にユーザーがプロジェクトから外されていた場合、クライアントはそれを知る術がありません。サーバー側が権限の最終判断者であり続けるのは当然として、単に書き込みを拒否するだけでは不十分です。ユーザーが3日間ためた作業をどう扱うか、拒否された操作の証跡をどう残して後から説明・修復できるようにするか、という設計まで踏み込む必要があります。
主要な同期アーキテクチャの比較
ローカルファースト同期の実装は複数存在し、それぞれ「何を正とするか(source of truth)」が異なります。ここを理解しておくと、自分のプロダクトに合う方式を選びやすくなります。
- PowerSync: バケット単位の行操作をレプリケーションし、アップロード処理はアプリ側が持つ
- Zero: ライブクエリの結果を維持し、楽観的にミューテーションを再生する
- Electric: 読み込みパスに特化し、書き込みはアプリのAPIが担当する
- Replicache: ミューテーションの意図を記録し、正規状態の上でリベースする
- Turso Sync: ローカルデータベースの履歴そのものをレプリケーション単位にする
この他にも、ドメインイベントを正とするLiveStoreや、行バージョン履歴を統合データベース内に保持するJazzといった方式があります。どれが優れているというより、プロダクトが何を優先するかで最適解が変わります。常時接続でのコラボレーションが中心なのか、長期間のオフライン作業を前提とするのか、分散所有権を重視するのか、クエリ駆動のレプリカが欲しいのか、あるいはサーバーが現行のビジネスルールを強制する信頼できる審判役であってほしいのか。この優先順位の違いが、アーキテクチャ選定の分岐点になります。
スペックファースト設計という考え方
興味深いのは、同期プロトコル自体を仕様(spec)として先に定義し、複数言語で実装を独立に作るアプローチです。SSP2という草案プロトコルでは、コミット・カーソル・スコープ・ブートストラップ・競合・オフライン再生・プルーニング(不要データの刈り込み)・リアルタイム通信・バイナリデータ・CRDT列・暗号化までを、バイナリのワイヤーフォーマットとして定義しています。
TypeScript実装とRust実装という2つの独立した実装が、同じスキーマ契約と同じゴールデンバイトベクター、95シナリオの適合性カタログに対してテストされます。2つのコア実装の間で挙動差異が出た場合、それは「実装の細部」ではなく「プロトコルのバグ」として扱われます。
これはマイクロサービス間のAPI契約をOpenAPIやProtocol Buffersで先に固定してから実装する発想と近いものです。日本の開発現場でもgRPCのprotoファイルを契約として運用しているチームには馴染みやすい考え方だと思います。同期のような複雑な非機能要件こそ、実装の言語や個々のチームの裁量に委ねるより、仕様レベルで挙動を固定した方が長期的な技術的負債を抑えられます。
導入判断のために確認すべきポイント
ここまでの整理を踏まえ、自分のプロダクトに導入するかどうかを判断する際は、次の観点を確認しておくと後戻りが少なくなります。
| 確認observation | 見るべきポイント |
|---|---|
| 正とするデータ単位 | 行単位のレプリケーションか、イベントか、クエリ結果かを設計書やドキュメントで確認 |
| 権限失効時の挙動 | オフライン中に権限が変わった場合、拒否だけでなく意図の保存・再照合の仕様があるか |
| 再送の安全性 | 操作に冪等キーがあり、サーバー側で重複適用を防ぐ仕組みがあるか |
| 成熟度 | プロトコルがドラフト段階か、マネージドサービスの有無、対応プラットフォームの成熟度 |
実際に候補技術を評価する際は、まず公式リポジトリのライセンス表記とバージョン番号(1.0未満かどうか)を確認してください。プロトコル自体がまだドラフト段階のものは、本番導入前にAPIの後方互換ポリシーがあるかをリリースノートやCHANGELOGで確かめる必要があります。またサーバーストレージにPostgresやCloudflare D1など複数の選択肢がある場合は、既存のインフラ構成(自社がAWSかCloudflareかなど)との親和性も選定基準に加えるとよいでしょう。
ピアツーピア型ではなく、サーバーが権限・検証・監査・最終順序付けの責任を持つ「サーバー権威型」の設計を選ぶ場合、サーバーダウン時の書き込みキューの扱いや、監査ログの保持期間も併せて設計しておく必要があります。これはSRE(サイト信頼性エンジニアリング)観点での可用性設計とも直結する部分です。
まとめ
オフライン書き込み同期は、CRDTのような合意アルゴリズムだけでは解決しません。認可の失効、再起動後の復旧、スキーマ進化への耐性、デバッグ可能な証跡づくりまで含めて設計する必要があります。
候補技術を評価する際は、まず「何を正とするデータ単位にしているか」を確認し、次に権限失効時の挙動と再送の冪等性を仕様書で確かめてください。プロトコルが1.0未満であれば、後方互換性のポリシーと自社インフラとの親和性も判断材料に加えることをおすすめします。
最後に、同期基盤は後から差し替えるコストが非常に高いインフラです。導入前の比較検討に時間をかける価値は十分にあります。