# 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. ```