# MotorSignal — Phase 2D: Firebase Storage → Cloudflare R2 Migration & Architecture **Document Version**: 1.0.0 **Phase**: Phase 2D — Cloud Storage Migration & Cloud Sync Hardening **Status**: ARCHITECTURAL SPECIFICATION & MIGRATION GUIDE --- ## 1. Executive Summary & Authoritative Principles MotorSignal has migrated its large-object binary storage backend from **Firebase Cloud Storage** to **Cloudflare R2**, while preserving all existing offline-first guarantees, authentication models, and database contracts. * **Drift SQLite Schema 5 remains authoritative for all raw telemetry and local ride recordings.** * **Firebase Authentication remains authoritative for rider identity (`uid`).** * **Cloud Firestore remains the structured cloud database for queryable metadata, summaries, and performance runs.** * **Cloudflare R2 is the high-capacity, low-cost object/blob storage layer for compressed forensic bundles (`telemetry.zip`).** * **Cloud synchronization is completely downstream of local persistence.** --- ## 2. Previous vs. New Architecture ``` PREVIOUS ARCHITECTURE (Phase 2B/2C): Local SQLite (Schema 5) ──► SyncManager ──► FirebaseSyncProvider ├── Cloud Firestore (Metadata) └── Firebase Cloud Storage (telemetry.zip) NEW ARCHITECTURE (Phase 2D): Local SQLite (Schema 5) ──► SyncManager ──► FirebaseSyncProvider ├── Cloud Firestore (Metadata & R2 Object Key) └── Cloudflare Worker Gateway └── Cloudflare R2 (telemetry.zip) ``` --- ## 3. Cloudflare Worker Gateway Architecture ### Project Location ``` cloudflare/motorsignal-r2-worker/ ├── src/ │ └── index.ts # TypeScript Worker logic with Firebase JWT verification ├── wrangler.toml # R2 bucket bindings & environment configuration ├── package.json # Dependencies & scripts └── README.md # Deployment and testing instructions ``` ### Server-Side Path Derivation & Zero-Trust Boundary The Flutter client **NEVER** communicates with Cloudflare R2 using permanent access keys or secrets (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`). Instead, the client sends its Firebase ID token to the Cloudflare Worker. The Worker: 1. Validates the Firebase JWT cryptographic signature, expiration (`exp`), and project audience (`aud`). 2. Extracts the verified `sub` claim (`authenticatedUid`). 3. Validates the `X-Remote-Ride-Id` header against a strict regex (`/^[a-zA-Z0-9_-]{3,100}$/`). 4. Derives the canonical R2 object key server-side: ``` users/{authenticatedUid}/rides/{validatedRemoteRideId}/telemetry.zip ``` 5. Streams the binary payload directly into the Cloudflare `R2Bucket` binding. --- ## 4. Endpoints & API Contract ### Binary Upload Endpoint * **Method**: `PUT` * **Path**: `/r2/upload` * **Headers**: * `Authorization`: `Bearer ` * `Content-Type`: `application/zip` * `X-Remote-Ride-Id`: `ride__` * `X-Bundle-Sha256`: `` * `X-App-Version`: `` * **Body**: Raw binary bytes of `telemetry.zip`. * **Response**: ```json { "success": true, "objectKey": "users//rides//telemetry.zip", "size": 348120, "etag": "\"8f4b2c1...\"", "uploadedAt": "2026-08-31T12:00:00.000Z" } ``` ### Telemetry Download Endpoint * **Method**: `GET` * **Path**: `/r2/download?rideId=` * **Headers**: * `Authorization`: `Bearer ` * **Response**: Streamed binary `telemetry.zip` attachment. --- ## 5. Firestore Document Model Firestore documents continue to reside at `/users/{uid}/rides/{remoteRideId}` with queryable metadata, referencing Cloudflare R2: ```json { "localRideId": 14, "remoteRideId": "ride_14_20260831_080000", "uid": "aB3kL99zX...", "startTime": "2026-08-31T08:00:00.000Z", "endTime": "2026-08-31T08:45:12.000Z", "distanceMiles": 24.8, "topSpeedMph": 74.2, "maxLeanLeft": 32.1, "maxLeanRight": 35.4, "maxAccelG": 0.58, "maxBrakingG": -0.88, "averageSpeed": 41.5, "movingTime": 2160, "stoppedTime": 552, "qualityScore": 98, "qualityRating": "EXCELLENT", "telemetryBundle": { "provider": "cloudflare-r2", "objectKey": "users/aB3kL99zX.../rides/ride_14_20260831_080000/telemetry.zip", "status": "uploaded", "bundleSizeBytes": 348120, "sha256": "8f4b2c1..." }, "telemetryBundlePath": "users/aB3kL99zX.../rides/ride_14_20260831_080000/telemetry.zip", "bundleSizeBytes": 348120, "syncVersion": 2, "uploadedAt": "2026-08-31T08:46:00.000Z" } ``` --- ## 6. Security Model & Secrets Management 1. **Flutter App**: Contains **zero** Cloudflare credentials, API keys, or R2 tokens. 2. **Cloudflare Worker**: Managed via Cloudflare dashboard or Wrangler secrets (`wrangler secret put FIREBASE_PROJECT_ID`). 3. **Firestore Security Rules**: Unchanged (`users/{userId}/rides/{rideId}` allows read/write only if `request.auth.uid == userId`). 4. **Git Safety**: Secrets, `.env`, and private JSON files remain strictly excluded via `.gitignore`. --- ## 7. Migration & Backward Compatibility * **Existing Local Rides**: All local SQLite rides remain authoritative and fully accessible. * **Existing Firestore Records**: Legacy documents containing Firebase Storage references remain readable. New uploads write `syncVersion: 2` and `provider: "cloudflare-r2"`. * **Outbox Continuity**: The persistent local `SyncOutbox` continues tracking items by `${localRideId}_${providerId}`. --- ## 8. Failure & Retry Behavior | Failure Mode | Worker Status | Client Sync State | Recovery Strategy | | :--- | :--- | :--- | :--- | | **Worker 5xx / Timeout** | 500 / 503 | `RETRY_WAIT` | Exponential backoff ($5\text{s} \rightarrow 30\text{m}$) | | **Invalid / Expired Token** | 401 / 403 | `AUTH_REQUIRED` | Awaits token refresh / user login | | **Malformed Ride ID** | 400 | `FAILED` | Terminal validation error | | **Network Unavailable** | Client error | `WAITING_FOR_NETWORK` | Resumes automatically when online | | **R2 Success, Firestore Drop** | 200 OK | `RETRY_WAIT` | Re-attempts Firestore commit; R2 upload is idempotent | --- ## 9. Future Camera & Video Compatibility When video/camera features are introduced in Phase 3: * Video segments ($100\text{ MB} - 4\text{ GB}$) will be streamed to R2 under `users/{uid}/rides/{remoteRideId}/video/chunk_01.mp4`. * The Worker gateway can issue multipart chunk upload authorization URLs directly to Cloudflare R2 without routing massive video payloads through Firestore or mobile memory buffers.