From f828253695351a68c9448d9b24f3d21269cefba1 Mon Sep 17 00:00:00 2001 From: bponders12-cell Date: Tue, 1 Sep 2026 20:46:00 -0400 Subject: [PATCH] Define final MotorCloud data model and offline-first sync contract --- MOTORSIGNAL_FINAL_CLOUD_DATA_MODEL.md | 323 ++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 MOTORSIGNAL_FINAL_CLOUD_DATA_MODEL.md diff --git a/MOTORSIGNAL_FINAL_CLOUD_DATA_MODEL.md b/MOTORSIGNAL_FINAL_CLOUD_DATA_MODEL.md new file mode 100644 index 0000000..400b960 --- /dev/null +++ b/MOTORSIGNAL_FINAL_CLOUD_DATA_MODEL.md @@ -0,0 +1,323 @@ +# MotorSignal — Final MotorCloud Data Model & Offline-First Sync Contract + +**Status:** Phase 3 implementation baseline +**Date:** 2026-09-01 +**Backend:** Firebase Authentication + Firestore + authenticated Cloudflare R2 Worker +**Local source of truth:** Drift SQLite + +## 1. Ownership boundary + +Every cloud-owned resource belongs to exactly one Firebase UID unless it is explicitly introduced as a collaborative resource later. + +```text +Firebase Auth UID + ├── users/{uid} + ├── users/{uid}/motorcycles/{motorcycleId} + ├── users/{uid}/rides/{rideId} + ├── users/{uid}/trips/{tripId} + ├── users/{uid}/events/{eventId} + └── users/{uid}/friends/{friendId} +``` + +The Firebase UID is never supplied by the UI as an ownership claim. Server authorization derives ownership from `request.auth.uid`. + +## 2. Ride storage split + +Firestore stores queryable ride metadata. R2 stores the compressed forensic telemetry bundle. + +```text +users/{uid}/rides/{rideId} + metadata / summary / references + +users/{uid}/rides/{rideId}/telemetry.zip (Cloudflare R2) +``` + +The R2 object key must remain UID-scoped: + +```text +users/{uid}/rides/{rideId}/telemetry.zip +``` + +The client must never be able to select another user's UID as the destination. + +## 3. Ride document contract + +The canonical cloud ride document should contain: + +```text +id +ownerId +motorcycleId +tripId? +startedAt +endedAt +durationSeconds +distanceMiles +topSpeedMph +averageSpeedMph +maxLeanLeft +maxLeanRight +maxAccelG +maxBrakingG +elevationGain +elevationLoss +qualityScore +qualityRating +eventCounts +performanceRuns +telemetryBundle.provider +telemetryBundle.objectKey +telemetryBundle.bundleSizeBytes +telemetryBundle.sha256 +telemetryBundle.status +createdAt +updatedAt +uploadedAt +syncVersion +software +visibility +``` + +Telemetry samples and raw forensic records do not belong directly in the Firestore ride document. + +## 4. Stable identity + +The long-term canonical `rideId` must be an application-generated stable cloud identity, not a Firebase auto-ID and not a device-local database integer. + +The current implementation uses `ride__` as a deterministic compatibility identity. This is safe for the current local database but is a migration target because a reinstall/new device can assign a different local integer ID. + +Before multi-device restore is released, add a persisted stable ride cloud ID to the local ride model and migrate existing rides deterministically. All telemetry, metadata updates, retries, restore operations, and deletes must use that stable ID. + +## 5. Local-first lifecycle + +The riding path remains completely independent of cloud availability. + +```text +Sensors + ↓ +Local recording + ↓ +Drift transaction + ↓ +Ride completed + ↓ +Sync outbox entry + ↓ +Network/auth available + ↓ +Firebase metadata + R2 telemetry upload + ↓ +Server acknowledgement + ↓ +Local outbox = SYNCED +``` + +A ride is never marked synced before the provider returns success. + +## 6. Sync state contract + +Current implementation states: + +```text +localOnly +queued +waitingForNetwork +uploading +retryWait +authRequired +synced +failed +cancelled +``` + +Terminal states are `synced`, `failed`, and `cancelled`. + +`failed` is intentionally not equivalent to `synced`. Retrying a failed item must explicitly rearm it. + +## 7. Outbox persistence + +The outbox is persisted in SharedPreferences as a compact manifest containing: + +```text +localRideId +providerId +status +createdAt +attemptCount +lastAttemptAt +nextRetryAt +uploadedAt +remoteId +lastError +bundleLocalPath +``` + +The persisted outbox is now namespaced by account UID: + +```text +motorsignal_sync_outbox_manifest_v2_ +``` + +Signed-out/local operation uses a separate `local` namespace. + +This is mandatory for account isolation on a shared device. User A's synced state must never cause User B's same-numbered local ride to appear synced. + +## 8. Idempotency + +All cloud writes must be repeat-safe. + +```text +same stable rideId + same owner UID + repeated upload + = one logical cloud ride +``` + +Firestore writes use `set(..., merge: true)` against the deterministic ride document. R2 upload uses the deterministic object key and authenticated Worker validation. + +## 9. Authentication rules + +Normal MotorCloud backup requires a non-anonymous authenticated Firebase user. + +Signed-out or anonymous users can continue recording locally but cloud upload must resolve to `authRequired` and leave the local ride untouched. + +A missing/expired Firebase ID token must not result in a successful local sync flag. + +## 10. Retry policy + +Temporary failures use exponential backoff: + +```text +1st retry: 5 seconds +2nd: 30 seconds +3rd: 2 minutes +4th: 5 minutes +5th: 15 minutes +later: 30 minutes +``` + +Authentication failures are parked as `authRequired` and rearmed after successful authentication. + +Permanent failures remain `failed` until an explicit retry is requested. + +## 11. Backup All + +`Back Up All` operates on local rides and delegates every cloud operation to `SyncManager` using the Firebase provider. + +The operation is intentionally batch/enqueue based: + +```text +local rides + ↓ +SyncManager.backupAllRideIds() + ↓ +Firebase provider queue + ↓ +processQueue() +``` + +Existing synced or queued rides are not duplicated. + +## 12. Cloud status UX + +The account/cloud screen exposes: + +- signed-in MotorCloud identity +- synced count +- pending count +- failed count +- current processing state +- last successful sync +- last sync error/notice +- Sync Now +- Back Up All +- Retry + +The status is diagnostic information, not the source of truth for local ride availability. + +## 13. GitHub separation + +GitHub is not a MotorCloud backup destination. + +The normal ride upload path must never use a GitHub PAT or write to the developer's `ride_logs` repository. + +The legacy GitHub exporter is retained only as an explicit developer/export provider. It must call the dedicated `uploadRideToGitHub()` method and must never be selected as the default sync provider. + +## 14. Security + +Firestore rules must enforce: + +```text +request.auth.uid == userId +``` + +for user-owned resources. + +R2 Worker authorization must validate the Firebase token and derive the UID from verified authentication rather than trusting a client-provided owner ID. + +The application must never log Firebase tokens, GitHub PATs, or raw private telemetry unnecessarily. + +## 15. Offline and restart guarantees + +Required invariants: + +1. Local ride data survives cloud outages. +2. A failed upload remains unsynced. +3. An upload in progress is requeued after process restart. +4. Successful sync state survives process restart. +5. User A and User B have separate outbox persistence namespaces. +6. Duplicate enqueue/upload is idempotent. +7. Sign-out blocks cloud upload. +8. Re-authentication resumes only that account's pending work. + +## 16. Multi-device restore contract + +Restore should be additive and resumable: + +```text +Authenticate + ↓ +Read user metadata + ↓ +Discover cloud rides + ↓ +Compare stable ride IDs + ↓ +Download missing metadata + ↓ +Download required telemetry bundles + ↓ +Materialize local records + ↓ +Mark local/cloud reconciliation complete +``` + +Restore must never silently delete local rides. Conflicts must be surfaced or resolved using an explicit policy. + +## 17. Next implementation gates + +Before Free/Pro external testing: + +- [x] MotorCloud normal upload path separated from GitHub +- [x] Account-scoped sync outbox +- [x] Persistent sync state/counts +- [x] Back Up All UI and manager path +- [x] Duplicate enqueue protection +- [x] Explicit failed retry behavior +- [ ] Stable persisted cloud ride ID migration +- [ ] Automated Firebase Emulator ownership tests +- [ ] Automated R2 Worker authorization tests +- [ ] Multi-device restore implementation +- [ ] Conflict/reconciliation implementation +- [ ] Account deletion cascade +- [ ] Account-level data export +- [ ] Free/Pro cloud entitlement enforcement + +## 18. Product rule + +MotorCloud is downstream of the ride recorder. + +```text +Cloud failure ≠ ride failure + +Local ride data is authoritative for recording. +Cloud data is the durable synchronized copy. +```