MotorSignal-App/MOTORSIGNAL_FINAL_CLOUD_DAT...

8.0 KiB

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.

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.

users/{uid}/rides/{rideId}
    metadata / summary / references

users/{uid}/rides/{rideId}/telemetry.zip   (Cloudflare R2)

The R2 object key must remain UID-scoped:

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:

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.

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:

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:

localRideId
providerId
status
createdAt
attemptCount
lastAttemptAt
nextRetryAt
uploadedAt
remoteId
lastError
bundleLocalPath

The persisted outbox is now namespaced by account UID:

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.

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:

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:

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:

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:

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:

  • MotorCloud normal upload path separated from GitHub
  • Account-scoped sync outbox
  • Persistent sync state/counts
  • Back Up All UI and manager path
  • Duplicate enqueue protection
  • 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.

Cloud failure ≠ ride failure

Local ride data is authoritative for recording.
Cloud data is the durable synchronized copy.