BluePeriod Docs
開発

クラウド同期アーキテクチャ

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. バックアップ(アップロード)フロー

  1. エクスポート: dexie-export-import を使用して、IndexedDB全体をJSON Blobにエクスポートします。この際、localSnapshots テーブルは除外され、バックアップの肥大化を防ぎます。
  2. 暗号化: ユーザーのSync Key(AES-GCM)を使用して、Blobを暗号化します。
  3. アップロード: 暗号化データを backup.enc としてSupabase Storageにアップロードします。
  4. メタデータ更新: user_sync_state テーブルの last_synced_at, backup_size, device_name を更新します。

3.2. 復元(ダウンロード)フロー

  1. ダウンロード: Supabase Storageから backup.enc をダウンロードします。
  2. 復号化: Sync Keyを使用してデータを復号化し、解凍後の Blob を取得します(メモリ節約のため巨大文字列化を回避)。
  3. パッチング: インポート前に Blob 内のデータベースバージョンを抽出し、現在のアプリケーションのバージョンに合わせて動的にパッチを適用します。これにより、スキーマ変更後のデータ復元を安定化させます。
  4. アトミックインポート & スナップショット退避:
    • インポート前に現在の localSnapshots テーブルの内容をメモリ(JavaScript変数)に退避させます。これにより、復元によって既存のスナップショットが消去されるのを防ぎます。
    • dexie-export-import のオプションで clearTablesBeforeImport: true および overwriteValues: true を有効化し、一つのトランザクション内でクリーンアップと復元を完結させます。
    • インポート完了後、退避させていたスナップショットをデータベースに書き戻します。
  5. 正規化 & リロード:
    • normalizeAllDates を実行し、全データの日付型を標準の Date に変換(索引の整合性確保)。
    • アプリケーションの状態(Jotai Atoms等)を整合させるため、ページをリロードします。

3.3. 競合解決 (Conflict Resolution)

自動マージは行いません。常に「ユーザーの選択」に基づきます。

SyncConfirmationDialog が以下のロジックで推奨アクションを提示します:

  1. Cloud Newer: クラウドの last_synced_at が、ローカルの最終同期時刻よりも新しい場合。
    • 推奨: 「Restore from Cloud」(ローカルへの上書き注意)
  2. Local Newer: ローカルデータの最終更新日時が、クラウドの last_synced_at よりも新しい場合。
    • 推奨: 「Upload Backup」
  3. 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 および lastSyncedAtAtomnull にし、UI上の「クラウドデータあり」の状態を解消します。

5.2. 技術的制約と実装 (RLS)

Supabase の RLS ポリシーにおいて user_sync_state テーブルに対する DELETE 権限が定義されていない場合、delete() リクエストは静かに失敗(0件削除)します。 この制約を回避し、DB構成変更なしでワイプを実現するため、本システムでは DELETE ではなく UPDATE を使用して全カラムを null に設定するアプローチを採用しています。

6. 将来の拡張性

現在のフルダンプ方式は、データサイズが数百MBを超えるとパフォーマンス(ネットワーク転送量・処理時間)に影響が出る可能性があります。 将来的にデータ量が肥大化した場合は、以下の拡張を検討します:

  1. 増分バックアップ: dexie-cloud などの専用ソリューションへの移行。
  2. メディア分離: 画像などの大容量アセットのみ別ファイル化し、テキストデータのみフルダンプする。
  3. データベーススキーマの改善: 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.md
  • 05_migration_guide.md
  • 06_development_guidelines.md

On this page