クラウド同期アーキテクチャ
Supabase Storageによるデバイス間完全バックアップ同期の仕組み
クラウド同期アーキテクチャ (Simple Full Sync)
このドキュメントでは、Supabase Storageを介してユーザーデータをデバイス間で同期するための「Simple Full Sync(完全バックアップ同期)」システムについて説明します。
1. 概要
同期システムは、ローカルのIndexedDB全体を単一のバックアップファイル(Blob)としてエクスポートし、暗号化してクラウドにアップロードします。復元時は、クラウド上のファイルをダウンロード・復号化し、ローカルデータを完全に置き換えます。
- 単純性: 差分計算やマージロジックが不要で、バグの発生源を大幅に削減。
- 整合性: データベース全体がアトミックに保存・復元されるため、テーブル間の不整合が発生しません。
- 安全性: クライアントサイドでの完全な暗号化(E2EE)により、プライバシーが保護されます。
2. ストレージ構造
2.1. オブジェクトストレージ (Supabase Storage)
ファイルは、Supabase Storageの user_backups バケットに保存されます。
user_backups/
└── [user_id]/
└── full_db_dump_sync/
└── backup.enc <-- 暗号化された完全なデータベースダンプ2.2. メタデータ (Supabase Database)
同期の状態確認や競合検知のため、軽量なメタデータを user_sync_state テーブルに保存します。
create table user_sync_state (
user_id uuid primary key references auth.users(id),
last_synced_at timestamptz,
device_name text,
backup_size bigint, -- バックアップサイズ(バイト)
current_backup_filename text, -- 現在は固定で 'full_db_dump_sync/backup.enc'
created_at timestamptz default now(),
updated_at timestamptz default now()
);3. 同期プロセス
3.1. バックアップ(アップロード)フロー
- エクスポート:
dexie-export-importを使用して、IndexedDB全体をJSON Blobにエクスポートします。この際、localSnapshotsテーブルは除外され、バックアップの肥大化を防ぎます。 - 暗号化: ユーザーのSync Key(AES-GCM)を使用して、Blobを暗号化します。
- アップロード: 暗号化データを
backup.encとしてSupabase Storageにアップロードします。 - メタデータ更新:
user_sync_stateテーブルのlast_synced_at,backup_size,device_nameを更新します。
3.2. 復元(ダウンロード)フロー
- ダウンロード: Supabase Storageから
backup.encをダウンロードします。 - 復号化: Sync Keyを使用してデータを復号化し、解凍後の
Blobを取得します(メモリ節約のため巨大文字列化を回避)。 - パッチング: インポート前に
Blob内のデータベースバージョンを抽出し、現在のアプリケーションのバージョンに合わせて動的にパッチを適用します。これにより、スキーマ変更後のデータ復元を安定化させます。 - アトミックインポート & スナップショット退避:
- インポート前に現在の
localSnapshotsテーブルの内容をメモリ(JavaScript変数)に退避させます。これにより、復元によって既存のスナップショットが消去されるのを防ぎます。 dexie-export-importのオプションでclearTablesBeforeImport: trueおよびoverwriteValues: trueを有効化し、一つのトランザクション内でクリーンアップと復元を完結させます。- インポート完了後、退避させていたスナップショットをデータベースに書き戻します。
- インポート前に現在の
- 正規化 & リロード:
normalizeAllDatesを実行し、全データの日付型を標準のDateに変換(索引の整合性確保)。- アプリケーションの状態(Jotai Atoms等)を整合させるため、ページをリロードします。
3.3. 競合解決 (Conflict Resolution)
自動マージは行いません。常に「ユーザーの選択」に基づきます。
SyncConfirmationDialog が以下のロジックで推奨アクションを提示します:
- Cloud Newer: クラウドの
last_synced_atが、ローカルの最終同期時刻よりも新しい場合。- 推奨: 「Restore from Cloud」(ローカルへの上書き注意)
- Local Newer: ローカルデータの最終更新日時が、クラウドの
last_synced_atよりも新しい場合。- 推奨: 「Upload Backup」
- Synced: 日時がほぼ一致する場合(トレランス 2秒以内)。
ローカル更新日時の判定ロジック:
- 主要テーブル(
projects,characters,chatSessions等)のupdatedAtインデックスの最大値をスキャンします。 - 削除操作への対応: アイテムが完全に削除された場合、およびメッセージ等の末端データの更新を確実に同期システムに伝えるため、
localStorageのグローバルタイムスタンプ (blueperiod_global_last_modified) も併用します。削除・編集操作時にtouchGlobalUpdate()を呼び出すことで、テーブルが空であっても「ローカルが変更された」ことを同期システムに確実に通知します。
削除の扱い: フルダンプ方式であるため、「削除マーカー(Tombstone)」は不要です。バックアップ時点の状態がそのまま正となります。ローカルで削除してアップロードすれば、クラウド上のバックアップもそのアイテムがない状態になります。ローカルで削除してもアップロード前にクラウドからダウンロードすれば、アイテムは復活します。
3.4. 同期モード (Sync Modes)
本アーキテクチャは実装上、複数の同期モード(リアルタイム、定期実行、マニュアル)をサポートするように設計されていますが、現在は信頼性とテストの観点から「マニュアル同期」のみを有効化しています。
- マニュアル同期(現在有効): ユーザーが明示的にアップロード/復元を選択します。
- オート同期 / 定期実行(一時無効化): 複数デバイスでの同時編集による競合リスクや、予期せぬ上書きを避けるため、UI上では設定できないよう制限されています。
4. UIコンポーネント
SyncSetupWizard: 初回セットアップ(Sync Key生成/入力)と最初の同期待ちを行います。SyncConfirmationDialog: 同期実行前に、ローカルとクラウドの状態を比較し、ユーザーに方向(アップロード vs ダウンロード)を選択させます。SyncStatusIndicator: ヘッダーに現在の同期状態(Cloud/Localのどちらが最新か)を表示します。
5. デンジャーゾーン (Wipe Cloud Backup)
ユーザーがクラウド上のバックアップを完全に消去するための機能です。
5.1. 処理内容
- Storage削除:
backup.encを削除します。歴史的な経緯によりファイルがルート直下またはfull_db_dump_sync/フォルダ内のいずれかに存在する可能性があるため、両方のパスに対して削除を試行します。 - メタデータ初期化:
user_sync_stateテーブルの各フィールドをnullにリセットします。 - ローカルキャッシュクリア:
cloudSyncStateAtomおよびlastSyncedAtAtomをnullにし、UI上の「クラウドデータあり」の状態を解消します。
5.2. 技術的制約と実装 (RLS)
Supabase の RLS ポリシーにおいて user_sync_state テーブルに対する DELETE 権限が定義されていない場合、delete() リクエストは静かに失敗(0件削除)します。
この制約を回避し、DB構成変更なしでワイプを実現するため、本システムでは DELETE ではなく UPDATE を使用して全カラムを null に設定するアプローチを採用しています。
6. 将来の拡張性
現在のフルダンプ方式は、データサイズが数百MBを超えるとパフォーマンス(ネットワーク転送量・処理時間)に影響が出る可能性があります。 将来的にデータ量が肥大化した場合は、以下の拡張を検討します:
- 増分バックアップ:
dexie-cloudなどの専用ソリューションへの移行。 - メディア分離: 画像などの大容量アセットのみ別ファイル化し、テキストデータのみフルダンプする。
- データベーススキーマの改善:
user_sync_stateテーブルにDELETEポリシーを追加し、完全なレコード削除を可能にする。
現時点のテキスト中心の小説執筆ユースケースでは、フルダンプ方式で十分なパフォーマンスと最大の信頼性が得られます。
7. 型の復元と正規化
JSON形式でダウンロード・エクスポートされるバックアップは、保存時に日付情報がISO文字列に変換されます。復元時にはこれを正しい JavaScript オブジェクトに戻す必要がありますが、その際に IndexedDB の索引を壊さないよう「標準の Date オブジェクト」に正規化することが不可欠です。
- スナップショットの保護: 復元中、既存の
localSnapshotsテーブルは一時的にメモリに退避され、インポート完了後に自動的に書き戻されます。これにより、復元作業そのものによるデータの完全な喪失を防ぎます。 - 日付の標準化:
src/lib/sync/typeRestoration.tsおよびsrc/lib/db/migrations.tsを通じて、すべてのcreatedAt,updatedAtフィールドをensureDateユーティリティにより標準のDateオブジェクトに変換してから保存します。 - インデックス保護: 非標準の日付オブジェクト(Temporalポリフィル等)がデータベースに混入すると索引が機能しなくなるため、インポート時は常に
normalizeAllDatesを呼び出し、最新の型でインデックスを再構築します。
関連ドキュメント:
04_database_schema.md05_migration_guide.md06_development_guidelines.md