MotorSignal-App/docs/architecture/cloud-architecture.md

681 lines
16 KiB
Markdown

# MotorSignal Cloud Architecture
**Status:** Planning baseline
**Date:** 2026-09-01
**Backend direction:** Firebase Authentication + Firebase cloud services, integrated with the existing local-first Flutter application
## Purpose
Define the cloud architecture required for MotorSignal accounts, ride backup, synchronization, profiles, friends, planned rides, trips, and group rides.
This document intentionally favors an **offline-first** architecture. Local data remains the immediate source for recording and ride operation; cloud services provide identity, backup, synchronization, collaboration, and cross-device continuity.
---
# Architectural Goals
1. Recording must work without internet access.
2. Cloud synchronization must be resilient to intermittent connectivity.
3. A user's data must be isolated from other users by security rules.
4. Synchronization must be idempotent and safe to retry.
5. Reinstalling the app must not require recreating a user's ride history.
6. Cloud functionality must support both Free and Pro entitlements.
7. The data model must support future social/group-riding features without a major rewrite.
8. Sensitive location information must receive stronger privacy treatment than ordinary profile information.
9. Account deletion must have a defined, testable data-deletion path.
10. Raw high-volume telemetry should not automatically be treated like ordinary profile documents.
---
# High-Level Architecture
```text
MotorSignal Flutter App
│
┌────────────────────┴────────────────────┐
│ │
Local Database Cloud Services
/ Local Files / Firebase
│ │
│ ┌──────────┼──────────┐
│ │ │ │
│ Auth Firestore Storage
│ │ │ │
└──────────────┬───────────────┴──────────┴──────────┘
│
Sync Engine
│
Entitlement
│
Pro / Free
```
---
# Identity
Firebase Authentication provides the account identity.
The internal account identifier should remain stable even if a user changes:
- Display name.
- Username.
- Profile photo.
- Optional public location.
Google sign-in should remain compatible with the existing application authentication flow.
The Firebase user ID should be treated as an internal identifier, not a public username.
---
# Proposed Data Model
The exact collection structure can be refined during implementation, but the logical model should include:
```text
users/{userId}
users/{userId}/motorcycles/{motorcycleId}
users/{userId}/rides/{rideId}
users/{userId}/trips/{tripId}
users/{userId}/events/{eventId}
users/{userId}/friends/{friendId}
```
Potential future top-level collaborative resources:
```text
groupRides/{groupRideId}
publicRoutes/{routeId}
```
These should only be introduced when query patterns and security requirements justify them.
---
# User Document
Conceptual fields:
```text
id
username
displayName
profilePhotoUrl
bio
location
createdAt
updatedAt
privacySettings
preferences
```
Do not store sensitive secrets in the user document.
Avoid storing a complete ride history or large telemetry payload directly inside the user document.
---
# Motorcycle Document
Conceptual fields:
```text
id
ownerId
year
make
model
nickname
photoUrl
photoSource
photoAttribution
createdAt
updatedAt
privacy
```
The motorcycle should have a stable ID independent of its displayed name.
---
# Ride Document
Conceptual metadata:
```text
id
ownerId
motorcycleId
tripId?
groupRideId?
startedAt
endedAt
duration
distance
summaryStatistics
routeReference?
telemetryReference?
createdAt
updatedAt
syncState
visibility
```
High-volume telemetry should not be duplicated unnecessarily across multiple Firestore documents.
Where appropriate, use compressed/structured files in Firebase Storage and keep searchable summary metadata in Firestore.
---
# Trip Document
Conceptual fields:
```text
id
ownerId
name
startDate
endDate
description
routeReferences
waypoints
participants
createdAt
updatedAt
visibility
```
Trips may contain multiple daily routes and completed rides.
---
# Event Document
For meetups and planned events:
```text
id
ownerId
name
description
startAt
location
attendees
linkedGroupRideId?
createdAt
updatedAt
visibility
```
An event does not automatically imply live location sharing.
---
# Friend Relationship
Friend relationships should have explicit state.
```text
PENDING
ACCEPTED
DECLINED
BLOCKED
```
Friendship should not automatically grant access to private ride data.
Visibility remains controlled by the owner's privacy settings.
---
# Group Ride
A group ride should be a shared activity context while each rider retains ownership of their own ride record.
Conceptually:
```text
Group Ride
├── Organizer
├── Participants
├── Route
├── Meeting Point
├── Schedule
├── Live Ride State
└── Completed Ride References
```
A rider's individual telemetry remains associated with that rider and motorcycle.
---
# Local-First Sync
The app should never require a successful cloud write before considering a ride locally saved.
### Write flow
```text
User records ride
↓
Save locally
↓
Mark PENDING_UPLOAD
↓
Continue normal app operation
↓
Connectivity available
↓
Sync engine uploads
↓
Server acknowledgement
↓
Mark SYNCED
```
### Offline flow
```text
No internet
↓
Record normally
↓
Queue cloud operation
↓
Internet returns
↓
Retry automatically
```
---
# Sync States
Recommended common state model:
```text
LOCAL_ONLY
PENDING_UPLOAD
SYNCED
PENDING_DELETE
CONFLICT
SYNC_FAILED
```
The implementation may combine or rename states, but synchronization must be observable and debuggable.
---
# Idempotency
Cloud writes must be safe to retry.
Do not create a new ride document every time an upload request is retried.
Use stable application-generated IDs so:
```text
same ride ID + repeated upload = same logical ride
```
not:
```text
same ride + repeated upload = duplicate rides
```
---
# Conflict Handling
Conflicts should be rare but must be explicitly designed.
Potential strategy:
- Server metadata determines whether an object changed.
- Immutable ride telemetry should generally not be merged field-by-field.
- User-editable metadata can use last-write-wins or explicit conflict resolution where appropriate.
- Never silently overwrite a user's ride data without a defined policy.
For example, if two devices edit a motorcycle nickname, last-write-wins may be acceptable.
If two devices produce different telemetry for the same ride ID, preserve enough information to diagnose the conflict rather than silently destroying one copy.
---
# Cloud Storage
Use Firebase Storage for large binary objects where appropriate:
- Ride telemetry files.
- Exported reports if cloud retention is required.
- Profile photos.
- Motorcycle photos where user-uploaded.
- Future ride media.
Keep Firestore documents focused on metadata, indexes, relationships, and summaries.
---
# Security Model
Every user-owned resource must enforce ownership server-side.
Conceptually:
```text
request.auth.uid == resource.ownerId
```
or an equivalent rule based on the final data model.
Never rely solely on Flutter UI restrictions to protect data.
A hidden button is not a security boundary.
---
# Social Security
Friendship and group participation must not automatically permit access to all account data.
Examples:
- Being a friend does not expose private ride history.
- Joining a group ride does not expose a user's historical rides.
- Live location is only available during explicitly authorized group activity.
- Public profile data is distinct from private account data.
---
# Location Privacy
Ride telemetry and GPS tracks can reveal a user's home, workplace, and routine.
The platform should support:
- Private rides.
- Friends-only rides.
- Public rides.
- Optional location sharing.
- Home/start/end location obfuscation for shared rides.
- No continuous background social tracking merely because a user has an account.
Live group location must require explicit participation/consent and should automatically stop at the end of the group ride.
---
# Cloud Entitlements
Cloud usage must be controlled through the centralized entitlement system.
Example capabilities:
```text
canCloudBackup
canUnlimitedCloudHistory
canMultiDeviceRestore
```
The client may use these capabilities for UX, but backend rules must also enforce limits and ownership where practical.
Do not trust a client-provided `isPro=true` field as proof of payment.
---
# Subscription / Account Separation
A user account should continue to exist even if a Pro subscription expires.
Conceptually:
```text
Firebase Account
│
├── Profile
├── Garage
├── Rides
└── Entitlement
│
┌──────┴──────┐
│ │
Free Pro Monthly/Lifetime
```
Billing provider state determines the paid entitlement. The account owns the data.
---
# Expired Pro Data Policy
The final product policy must be defined before launch.
Recommended direction:
- Do not silently delete historical rides when Pro expires.
- Preserve account and existing data.
- Restrict creation/synchronization of additional premium-only data if the user exceeds Free limits.
- Allow upgrade to restore full Pro functionality.
The exact storage allowance and retention policy must be finalized after Firebase cost modeling and legal review.
---
# Backup / Restore
Backup should be automatic where the user's entitlement allows it.
Restore should be explicit and understandable.
New device flow:
```text
Install MotorSignal
↓
Sign in
↓
Authenticate
↓
Discover cloud account
↓
Download motorcycle/profile metadata
↓
Download ride metadata
↓
Download required ride data
↓
Rebuild local database
↓
Ready
```
The restore process should be resumable if interrupted.
---
# Account Deletion
Account deletion must be a real backend workflow.
It should address:
- Firebase Authentication identity.
- User profile.
- Motorcycles.
- Rides.
- Telemetry files.
- Profile photos.
- Social relationships.
- Trips/events owned by the user.
- Group ride participation data where legally/technically appropriate.
Do not simply delete the authentication record while leaving user data orphaned indefinitely.
---
# Data Export
Provide an account-level export capability where required/appropriate.
Potential export contents:
- Profile information.
- Motorcycle data.
- Ride metadata.
- Ride telemetry.
- Routes.
- Trip data.
The existing CSV/PDF ride exports are not necessarily sufficient to satisfy an account-level data export requirement.
---
# Observability
Cloud synchronization should provide enough logging to diagnose:
- Authentication failures.
- Upload failures.
- Download failures.
- Permission-denied errors.
- Duplicate prevention failures.
- Conflict events.
- Storage failures.
- Billing/entitlement synchronization problems.
Do not log raw sensitive telemetry or credentials unnecessarily.
---
# Testing Matrix
### Authentication
- [ ] Fresh account.
- [ ] Existing account.
- [ ] Sign out.
- [ ] Sign in again.
- [ ] Multiple devices.
- [ ] Account deletion.
### Offline
- [ ] Record without internet.
- [ ] Save ride locally.
- [ ] Restore connectivity.
- [ ] Automatic upload.
- [ ] Retry after failure.
### Sync
- [ ] New motorcycle sync.
- [ ] Ride sync.
- [ ] Ride update.
- [ ] Ride deletion.
- [ ] Duplicate upload prevention.
- [ ] Conflict behavior.
### Restore
- [ ] Reinstall.
- [ ] New device.
- [ ] Large ride history.
- [ ] Interrupted restore.
- [ ] Partial network failure.
### Security
- [ ] User A cannot read User B's private rides.
- [ ] User A cannot write User B's data.
- [ ] Friend access respects privacy.
- [ ] Group membership does not grant historical access.
- [ ] Client cannot self-grant Pro entitlement.
---
# Implementation Order
1. Audit existing Firebase/auth implementation.
2. Document current local Room/database models.
3. Define cloud DTOs and stable IDs.
4. Define Firestore/Storage collections.
5. Write security rules before exposing new data.
6. Implement profile document.
7. Implement motorcycle synchronization.
8. Implement ride metadata synchronization.
9. Implement telemetry/blob synchronization.
10. Implement retry queue.
11. Implement conflict/error handling.
12. Implement backup status UI.
13. Implement restore.
14. Add account deletion/data export.
15. Connect cloud capability checks to entitlement manager.
16. Test Free/Pro storage behavior.
17. Add friends/social resources.
18. Add planned rides/trips.
19. Add group rides and explicit live-location sessions.
---
# Design Rule
The cloud system must make MotorSignal more reliable, not more fragile.
The critical riding path should remain:
```text
Sensors → Local Recording → Local Database → Ride Processing
```
Cloud synchronization should be downstream:
```text
Local Ride → Sync Queue → Cloud Backup
```
A cloud outage must not prevent a rider from recording a ride.
---
# Server-Verified Google Play Entitlements (Stage 4B-3)
## Verification Pipeline
```text
Google Play Billing Client
↓
MotorSignal BillingService (PlayBillingService)
↓
Unverified StorePurchase (productId, purchaseToken)
↓
DurableVerificationQueue (FlutterSecureStorage)
↓
PurchaseVerificationService
↓
Cloudflare Worker (POST /billing/verify-play-purchase)
↓ (Authenticated via Service Account JWT assertion)
Google Play Developer API v3 (Android Publisher)
- Subscriptions: purchases.subscriptionsv2.get
- Lifetime: purchases.products.get
↓
Verified Entitlement Payload (Plan, Status, ExpiresAt, TokenHash)
↓
EntitlementCoordinator
↓
Secure Local Entitlement Cache (FlutterSecureStorage)
↓
PlayBillingService.completePurchase() (Google Play Acknowledgement)
```
## Security & Architecture Contracts
1. **Server Authority**: The mobile client never self-asserts its entitlement tier. Entitlement is determined authoritatively by Google Play Developer API responses.
2. **Account Independence**: MotorSignal login is strictly optional. Riders without a MotorSignal account can purchase, verify, restore, and own Pro entitlements. Optional Firebase ID tokens sent in verification requests associate convenience metadata without overriding Google Play ownership.
3. **Secret Isolation**: Google Service Account credentials (`GOOGLE_SERVICE_ACCOUNT_EMAIL`, `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY`) reside exclusively as Cloudflare Worker secrets. They are never compiled into the APK, never committed, and never exposed in logs.
4. **Token Handling**: Raw Google Play purchase tokens are never logged or stored in general application databases (Drift, SharedPreferences). Bounded retry queuing utilizes encrypted `FlutterSecureStorage` and purges tokens upon terminal verification.
5. **Rider Safety Invariant**: Active ride recording and telemetry persistence are strictly isolated from billing verification operations and network outages.