MotorSignal-App/MOTORSIGNAL_PHASE_2D_R2_MIG...

169 lines
6.7 KiB
Markdown

# 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 <FirebaseIdToken>`
* `Content-Type`: `application/zip`
* `X-Remote-Ride-Id`: `ride_<localRideId>_<timestamp>`
* `X-Bundle-Sha256`: `<sha256-hash>`
* `X-App-Version`: `<semantic-version>`
* **Body**: Raw binary bytes of `telemetry.zip`.
* **Response**:
```json
{
"success": true,
"objectKey": "users/<uid>/rides/<remoteRideId>/telemetry.zip",
"size": 348120,
"etag": "\"8f4b2c1...\"",
"uploadedAt": "2026-08-31T12:00:00.000Z"
}
```
### Telemetry Download Endpoint
* **Method**: `GET`
* **Path**: `/r2/download?rideId=<remoteRideId>`
* **Headers**:
* `Authorization`: `Bearer <FirebaseIdToken>`
* **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.