forge back up sync 성공 success! 333 저그 백업
# Blueprint: MemoSquare data backup, survival, and recovery
**Purpose:** Hand this document to another Cursor agent (or human architect) so they can **design a similar system** in another app. It describes **how this Flutter app keeps memo data recoverable** across crashes, bad saves, many in-place upgrades, and **some** reinstall scenarios — and where recovery **cannot** be automatic.
**Canonical implementation (this repo):** Dart under `lib/data/`, `lib/services/`, orchestration in `lib/main.dart`.
---
## 1. Why it feels like “rebuild / override still allows backup”
The app does **not** rely on a single store. It uses **layered persistence**:
| Layer | Typical location | Survives in-place app upgrade? | Survives “Delete app” on iOS? |
|--------|------------------|-------------------------------|-------------------------------|
| A. Working copy | `SharedPreferences` (per-app container) | Yes | **No** |
| B. Local safety net | `getApplicationSupportDirectory()` → `memo_data/` | Yes | **No** |
| C. Survival / folder backup | User-visible path (e.g. `Documents/…`, or **iCloud Drive** folder picked in Files) | N/A (not inside app sandbox) | **Often yes** if folder is outside sandbox / synced |
| D. iCloud board file | App’s **iCloud Drive** container (native channel) | Yes (synced) | **Often yes** if iCloud account + container intact |
| E. Google Drive (optional) | User’s Drive file | Yes | Yes (account + file) |
| F. Backup code | User copy/paste (email, Notes, etc.) | N/A | **Yes** (user-owned) |
**Key idea:** An **in-place** install from Xcode / TestFlight / App Store **keeps the same app container**, so layers **A + B** (and prefs-backed trash) remain. That is why frequent **rebuilds that replace the binary without deleting the app** still show the same data.
If the user **deletes the app** on iPhone, **Apple removes the sandbox** — **A and B are gone** unless something already copied data to **C, D, E, or F**.
---
## 2. Architecture snapshot (mental model)
```mermaid
flowchart TB
subgraph runtime [Runtime UI state]
UI[Board + editors]
end
subgraph primary [Primary persistence]
Prefs[SharedPreferences: memos, settings, windows, trash JSON]
end
subgraph safety [Local safety net - same machine, app container]
Auto[autosave_latest.json]
Snap[snapshots / rotating JSON]
Log[backup_recovery.log + data_guardian_audit.log]
end
subgraph escape [Outside sandbox or user-owned]
Surv[current.json in chosen folder]
iCloud[iCloud Drive board JSON - native]
Drive[Google Drive file - optional]
Code[Base64 backup code - clipboard / text]
end
UI -->|debounced persist| Prefs
UI -->|same payload| Auto
Auto --> Snap
UI --> Surv
UI --> iCloud
UI --> Drive
UI --> Code
Single canonical payload shape: A JSON object (and the same structure inside MemoBackupPack) carrying at least: schema, updatedAt, memos[], trash[], settings, windowStates, windowOrder, plus optional appVersion / appBuild. The backup code is that JSON + checksum, Base64-encoded (lib/data/memo_backup_code.dart).
MemoRepository)lib/data/memo_repository.dartSharedPreferences keys (memo_squares_v2, etc.). Settings, window layout, and related keys are separate prefs entries.runStartupMigrations() copies legacy v1 keys to v2 without deleting v1, so a reader upgrade does not orphan old data in one shot.saveMemos(..., allowEmptyOverwrite: false) refuses to write an empty memo list if MemoSafetyNet.findBestRecoveryOffer() still finds a non-empty backup — prevents prefs from wiping the last known good snapshot after a bug or race.lib/data/memo_safety_net.dartgetApplicationSupportDirectory() + /memo_data/autosave_latest.json — latest full-board JSON payload.snapshots/snapshot_<iso-timestamp>.json — time-stamped copies; only the newest N retained (maxRotatingSnapshots, e.g. 5)._writeQueue): all disk writes for this subsystem run one-after-another to avoid iOS rename/delete races.allowEmptyOverwrite is explicitly true (coordinated with “session peak memo count” and one-shot flags in main.dart).findBestRecoveryOffer() scans autosave, all snapshot JSONs, picks the newest valid file that parses to a non-empty memos list. On macOS, also checks legacy paths under ~/Library/Application Support/ for older app names.appendLog → memo_data/backup_recovery.log with AppBuildIdentity tag on each line.lib/services/memo_survival_backup_service.dart~/Documents/MemoSquare Backup/current.json on desktop; on iOS the user should pick iCloud Drive (or another writable Documents provider path) via directory picker — not “File Provider Storage” paths that are read-only.onDataChanged() once markReadyForSync() has been called post-bootstrap.current.json via tmp + rename; optional daily snapshot under snapshots/ when the filesystem allows subfolders.tryAutoRestoreIfLocalEmpty(...) — if local memo count is 0 but current.json has meaningful memos (or trash), returns a MemoBackupPack so main.dart can replay into prefs + trash store before UI settles.lib/services/apple_icloud_board_sync.dartMethodChannel('appmemo/icloud_board') — native side reads/writes a board JSON file in the iCloud Drive container (path surfaced to user via boardFilePath())._tryRecoveryOfferFromICloud() in main.dart can build a RecoveryOffer from that file — survives reinstall better than container-only autosave when iCloud is available and the file was populated by prior sync.lib/services/google_drive_sync_service.dart (and merge hooks in main.dart)lib/data/memo_backup_code.dart, UI MemoDataBackupScreen (from main.dart navigation).MemoBackupCode.encode(MemoBackupPack) → Base64 string with embedded checksum (tamper detection on decode).MemoBackupPack.main.dart handlers).lib/data/memo_trash_store.dartlib/services/memo_data_guardian.dartdata_guardian_audit.log in the same memo_data/ tree with tagged lines for: blocked empty saves, snapshot writes, recovery sources, merge decisions, iCloud failures, etc. Pairs with MemoSafetyNet for post-mortems.main.dart)Bootstrap order (simplified):
MemoRepository.runStartupMigrations()MemoSurvivalBackupService.ensureBootstrapped() (validate/clear broken folder prefs)tryFindRecoveryOffer() (safety net), then _tryRecoveryOfferFromICloud()setState with loaded data; _finishDeferredStartup may show “Local backup found” dialog for RecoveryOfferMemoSurvivalBackupService.markReadyForSync() and stage payload after first frame (deferred startup)Every successful persist path should:
MemoRepository where appropriate._createBackupPayload / MemoBackupPack) and call MemoRepository.persistSafetyNetPayload → safety net files.MemoSurvivalBackupService.stagePayload + onDataChanged() for folder backup.Guards against “empty board” wiping real data:
MemoRepository.saveMemos empty guard (prefs vs safety net).MemoSafetyNet.writeAutosaveAndRotateSnapshots empty guard (disk vs disk).MemoSurvivalBackupService.syncNow empty guard (folder current.json).main.dart: if the session ever had memos, block persisting an empty list until explicitly allowed (_allowEmptyPersistOnce after user chooses “Start fresh” on recovery dialog).Minimum viable multi-layer design:
getApplicationSupportDirectory() (or equivalent), atomic writes, rotating snapshots, non-empty clobber protection.tryAutoRestoreIfLocalEmpty + memoV2KeyPresent).toJson / fromJson “pack” used for file export, safety net, folder backup, and optional cloud — avoids incompatible parallel formats.Non-goals / caveats to document for users:
| Concern | Primary file(s) |
|---|---|
| Prefs + migrations + save guards | lib/data/memo_repository.dart |
| Autosave, snapshots, recovery scan, recovery log | lib/data/memo_safety_net.dart |
| Folder survival backup + auto-restore | lib/services/memo_survival_backup_service.dart |
| iCloud board file bridge | lib/services/apple_icloud_board_sync.dart |
| Backup code codec + pack schema | lib/data/memo_backup_code.dart |
| Trash | lib/data/memo_trash_store.dart |
| Audit trail | lib/services/memo_data_guardian.dart |
| Boot, dialogs, persist pipeline, merge | lib/main.dart |
| Tests (examples) | test/memo_safety_net_test.dart, test/memo_backup_code_test.dart |
.cursorrules): do not ship silent wipes; prefer migrations and recovery..cursor/rules/volt-battery-efficiency.mdc): debounce persistence; avoid redundant timers; keep safety paths but coalesce writes.Document generated for cross-agent handoff. Implementation truth remains the Dart sources cited above.