6.7 KiB
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:
- Validates the Firebase JWT cryptographic signature, expiration (
exp), and project audience (aud). - Extracts the verified
subclaim (authenticatedUid). - Validates the
X-Remote-Ride-Idheader against a strict regex (/^[a-zA-Z0-9_-]{3,100}$/). - Derives the canonical R2 object key server-side:
users/{authenticatedUid}/rides/{validatedRemoteRideId}/telemetry.zip - Streams the binary payload directly into the Cloudflare
R2Bucketbinding.
4. Endpoints & API Contract
Binary Upload Endpoint
- Method:
PUT - Path:
/r2/upload - Headers:
Authorization:Bearer <FirebaseIdToken>Content-Type:application/zipX-Remote-Ride-Id:ride_<localRideId>_<timestamp>X-Bundle-Sha256:<sha256-hash>X-App-Version:<semantic-version>
- Body: Raw binary bytes of
telemetry.zip. - Response:
{ "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.zipattachment.
5. Firestore Document Model
Firestore documents continue to reside at /users/{uid}/rides/{remoteRideId} with queryable metadata, referencing Cloudflare R2:
{
"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
- Flutter App: Contains zero Cloudflare credentials, API keys, or R2 tokens.
- Cloudflare Worker: Managed via Cloudflare dashboard or Wrangler secrets (
wrangler secret put FIREBASE_PROJECT_ID). - Firestore Security Rules: Unchanged (
users/{userId}/rides/{rideId}allows read/write only ifrequest.auth.uid == userId). - 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: 2andprovider: "cloudflare-r2". - Outbox Continuity: The persistent local
SyncOutboxcontinues 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 underusers/{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.