MotorSignal-App/MOTORSIGNAL_FINAL_CLOUD_DAT...

332 lines
8.7 KiB
Markdown

# 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` is an application-generated stable cloud identity, not a Firebase auto-ID and not a device-local database integer.
The local `rides.cloudRideId` column was introduced in schema v6. New rides receive a random `ride_<hex>` identity once and retain it across sync retries, database restarts, and future restore operations.
For pre-v6 local rides, the migration deliberately preserves compatibility with existing MotorCloud documents by assigning the exact legacy identity format `ride_<localRideId>_<UTC yyyyMMdd_HHmmss>`. Those IDs become persisted stable identities after migration, so existing cloud records remain addressable instead of being silently duplicated under new IDs. Newly created rides do not use local database IDs as their cloud identity.
All telemetry, metadata updates, retries, restore operations, and deletes must use the persisted stable cloud 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_<firebaseUid>
```
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 stable ride document. R2 upload uses the stable 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 is additive and resumable:
```text
Authenticate
↓
Read user metadata
↓
Discover cloud rides
↓
Compare persisted stable ride IDs
↓
Download missing metadata
↓
Download required telemetry bundles
↓
Materialize local records
↓
Mark local/cloud reconciliation complete
```
`CloudRestoreService` currently performs the metadata phase through `CloudReconciliationService`. It never deletes an existing local ride and never overwrites an existing local ride with cloud metadata merely because the cloud document differs. Divergent metadata is surfaced as a conflict for a future revision-aware policy.
The authenticated R2 download operation is intentionally a separate gate. No client code should pretend a telemetry bundle was restored until the Worker exposes a verified download contract.
## 17. 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
- [x] Persisted stable cloud ride ID
- [x] v5 → v6 migration coverage
- [x] Fresh database coverage
- [x] Firebase Emulator ownership test suite
- [x] Additive cloud metadata reconciliation service
- [ ] R2 Worker authenticated download operation
- [ ] Telemetry bundle download/restore
- [ ] Revision-aware conflict resolution
- [ ] Automated R2 Worker authorization tests
- [ ] 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.
```