Define final MotorCloud data model and offline-first sync contract
This commit is contained in:
parent
2146a718fb
commit
f828253695
|
|
@ -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_<localRideId>_<UTC start timestamp>` 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_<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 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.
|
||||
```
|
||||
Loading…
Reference in New Issue