native-update 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/Readme.md +36 -22
  2. package/docs/CHANGELOG.md +168 -0
  3. package/docs/EXAMPLE_APPS_SIMPLIFICATION_PLAN.md +384 -0
  4. package/docs/EXAMPLE_APPS_SIMPLIFICATION_TRACKER.md +390 -0
  5. package/docs/MARKETING_WEBSITE_PLAN.md +659 -0
  6. package/docs/MARKETING_WEBSITE_TRACKER.md +661 -0
  7. package/docs/ROADMAP.md +143 -0
  8. package/docs/SECURITY.md +356 -0
  9. package/docs/api/API.md +557 -0
  10. package/docs/api/FEATURES.md +414 -0
  11. package/docs/guides/key-management.md +1 -1
  12. package/docs/plans/PLANNING_COMPLETE_SUMMARY.md +361 -0
  13. package/docs/plans/TASK_1_ANDROID_EXAMPLE_APP.md +401 -0
  14. package/docs/plans/TASK_2_API_ENDPOINTS.md +856 -0
  15. package/docs/plans/TASK_2_DASHBOARD_UI_UX.md +820 -0
  16. package/docs/plans/TASK_2_DATABASE_SCHEMA.md +704 -0
  17. package/docs/plans/TASK_2_GOOGLE_DRIVE_INTEGRATION.md +646 -0
  18. package/docs/plans/TASK_2_SAAS_ARCHITECTURE.md +587 -0
  19. package/docs/plans/TASK_2_USER_AUTHENTICATION.md +600 -0
  20. package/docs/reports/AUDIT_SUMMARY_2025-12-26.md +203 -0
  21. package/docs/reports/COMPLETE_VERIFICATION.md +106 -0
  22. package/docs/reports/EVENT_FLOW_VERIFICATION.md +80 -0
  23. package/docs/reports/EXAMPLE_APPS_SIMPLIFICATION_COMPLETE.md +369 -0
  24. package/docs/reports/FINAL_STATUS.md +122 -0
  25. package/docs/reports/FINAL_VERIFICATION_CHECKLIST.md +425 -0
  26. package/docs/reports/MARKETING_WEBSITE_COMPLETE.md +466 -0
  27. package/docs/reports/PACKAGE_COMPLETENESS_REPORT.md +130 -0
  28. package/docs/reports/PRODUCTION_STATUS.md +115 -0
  29. package/docs/reports/PROJECT_RESTRUCTURE_2025-12-27.md +287 -0
  30. package/docs/reports/PROJECT_RESTRUCTURE_FINAL_SUMMARY.md +464 -0
  31. package/docs/reports/PUBLISHING_VERIFICATION.md +144 -0
  32. package/docs/reports/RELEASE_READY_SUMMARY.md +99 -0
  33. package/docs/tracking/IMPLEMENTATION_TRACKER.md +303 -0
  34. package/package.json +2 -3
  35. package/backend-template/README.md +0 -56
  36. package/backend-template/package.json +0 -20
  37. package/backend-template/server.js +0 -121
@@ -0,0 +1,704 @@
1
+ # Task 2: Database Schema Design
2
+
3
+ **Created:** 2025-12-27
4
+ **Status:** 📝 Planning
5
+ **Database:** Firebase Firestore
6
+
7
+ ---
8
+
9
+ ## 🎯 Objectives
10
+
11
+ Design a complete, scalable Firestore database schema that supports:
12
+ - User management and authentication
13
+ - Multi-app management per user
14
+ - Build versioning and distribution
15
+ - Google Drive token storage (encrypted)
16
+ - Analytics tracking (future)
17
+
18
+ ---
19
+
20
+ ## 📊 Collections Overview
21
+
22
+ ```
23
+ firestore/
24
+ ├── users/ # User profiles
25
+ ├── apps/ # User's applications
26
+ ├── builds/ # Build versions
27
+ ├── drive_tokens/ # Encrypted Drive credentials
28
+ └── analytics/ # Usage analytics (future)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 📁 Collection 1: `users`
34
+
35
+ ### Purpose
36
+ Store user profile information and preferences
37
+
38
+ ### Document ID
39
+ `{userId}` - Firebase Auth UID
40
+
41
+ ### Schema
42
+
43
+ ```typescript
44
+ interface UserDocument {
45
+ // Identity
46
+ uid: string; // Firebase Auth UID (redundant but helpful)
47
+ email: string; // User email
48
+ displayName: string | null; // Full name
49
+ photoURL: string | null; // Profile picture URL
50
+
51
+ // Authentication
52
+ provider: 'email' | 'google.com'; // Auth provider
53
+ emailVerified: boolean; // Email verification status
54
+ createdAt: Timestamp; // Account creation
55
+ lastLogin: Timestamp; // Last login time
56
+
57
+ // Google Drive Integration
58
+ driveConnected: boolean; // Drive connected?
59
+ driveEmail: string | null; // Drive account email
60
+ driveConnectedAt: Timestamp | null; // When connected
61
+
62
+ // Subscription (future)
63
+ plan: 'free' | 'pro' | 'enterprise';
64
+ planStartDate: Timestamp | null;
65
+ planEndDate: Timestamp | null;
66
+
67
+ // Usage Stats
68
+ appsCount: number; // Total apps created
69
+ buildsCount: number; // Total builds uploaded
70
+ storageUsed: number; // Bytes used (in Drive)
71
+
72
+ // Preferences
73
+ preferences: {
74
+ emailNotifications: boolean; // Send email notifications
75
+ updateNotifications: boolean; // Notify on updates
76
+ theme: 'light' | 'dark' | 'auto';
77
+ language: 'en' | 'es' | 'fr'; // i18n support
78
+ };
79
+
80
+ // Metadata
81
+ updatedAt: Timestamp; // Last profile update
82
+ }
83
+ ```
84
+
85
+ ### Example Document
86
+
87
+ ```json
88
+ {
89
+ "uid": "abc123xyz789",
90
+ "email": "user@example.com",
91
+ "displayName": "John Doe",
92
+ "photoURL": "https://lh3.googleusercontent.com/...",
93
+
94
+ "provider": "google.com",
95
+ "emailVerified": true,
96
+ "createdAt": {"seconds": 1703721600, "nanoseconds": 0},
97
+ "lastLogin": {"seconds": 1703808000, "nanoseconds": 0},
98
+
99
+ "driveConnected": true,
100
+ "driveEmail": "user@gmail.com",
101
+ "driveConnectedAt": {"seconds": 1703722000, "nanoseconds": 0},
102
+
103
+ "plan": "free",
104
+ "planStartDate": null,
105
+ "planEndDate": null,
106
+
107
+ "appsCount": 2,
108
+ "buildsCount": 5,
109
+ "storageUsed": 157286400,
110
+
111
+ "preferences": {
112
+ "emailNotifications": true,
113
+ "updateNotifications": true,
114
+ "theme": "auto",
115
+ "language": "en"
116
+ },
117
+
118
+ "updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
119
+ }
120
+ ```
121
+
122
+ ### Indexes Required
123
+
124
+ ```javascript
125
+ // Composite indexes
126
+ users: {
127
+ fields: ['email', 'createdAt'],
128
+ fields: ['plan', 'createdAt']
129
+ }
130
+ ```
131
+
132
+ ### Security Rules
133
+
134
+ ```javascript
135
+ match /users/{userId} {
136
+ allow read: if request.auth.uid == userId;
137
+ allow create: if request.auth.uid == userId;
138
+ allow update: if request.auth.uid == userId
139
+ && !request.resource.data.diff(resource.data).affectedKeys()
140
+ .hasAny(['uid', 'email', 'createdAt', 'provider']);
141
+ allow delete: if request.auth.uid == userId;
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 📁 Collection 2: `apps`
148
+
149
+ ### Purpose
150
+ Store user's application configurations
151
+
152
+ ### Document ID
153
+ Auto-generated
154
+
155
+ ### Schema
156
+
157
+ ```typescript
158
+ interface AppDocument {
159
+ // Identity
160
+ id: string; // Auto-generated doc ID
161
+ userId: string; // Owner (Firebase Auth UID)
162
+
163
+ // App Info
164
+ name: string; // App display name
165
+ packageId: string; // com.example.app
166
+ icon: string | null; // App icon URL
167
+ description: string; // App description
168
+
169
+ // Platform Support
170
+ platforms: ('ios' | 'android' | 'web')[];
171
+
172
+ // Update Channels
173
+ channels: {
174
+ production: ChannelConfig;
175
+ staging: ChannelConfig;
176
+ development: ChannelConfig;
177
+ };
178
+
179
+ // Stats
180
+ totalBuilds: number; // Total builds uploaded
181
+ activeUsers: number; // Active installs (future)
182
+ lastBuildDate: Timestamp | null; // Last build upload
183
+
184
+ // Metadata
185
+ createdAt: Timestamp;
186
+ updatedAt: Timestamp;
187
+ }
188
+
189
+ interface ChannelConfig {
190
+ enabled: boolean; // Channel active?
191
+ autoUpdate: boolean; // Auto-update enabled?
192
+ updateStrategy: 'immediate' | 'background' | 'manual';
193
+ requireUserConsent: boolean; // Ask before update?
194
+ minVersion: string | null; // Minimum app version (semver)
195
+ }
196
+ ```
197
+
198
+ ### Example Document
199
+
200
+ ```json
201
+ {
202
+ "id": "app_abc123",
203
+ "userId": "abc123xyz789",
204
+
205
+ "name": "My Awesome App",
206
+ "packageId": "com.example.awesome",
207
+ "icon": "https://storage.googleapis.com/.../icon.png",
208
+ "description": "A great mobile application",
209
+
210
+ "platforms": ["ios", "android"],
211
+
212
+ "channels": {
213
+ "production": {
214
+ "enabled": true,
215
+ "autoUpdate": true,
216
+ "updateStrategy": "background",
217
+ "requireUserConsent": false,
218
+ "minVersion": "1.0.0"
219
+ },
220
+ "staging": {
221
+ "enabled": true,
222
+ "autoUpdate": false,
223
+ "updateStrategy": "manual",
224
+ "requireUserConsent": true,
225
+ "minVersion": null
226
+ },
227
+ "development": {
228
+ "enabled": true,
229
+ "autoUpdate": true,
230
+ "updateStrategy": "immediate",
231
+ "requireUserConsent": false,
232
+ "minVersion": null
233
+ }
234
+ },
235
+
236
+ "totalBuilds": 5,
237
+ "activeUsers": 1234,
238
+ "lastBuildDate": {"seconds": 1703808000, "nanoseconds": 0},
239
+
240
+ "createdAt": {"seconds": 1703721600, "nanoseconds": 0},
241
+ "updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
242
+ }
243
+ ```
244
+
245
+ ### Indexes Required
246
+
247
+ ```javascript
248
+ // Composite indexes
249
+ apps: {
250
+ fields: ['userId', 'createdAt'],
251
+ fields: ['userId', 'platforms', 'createdAt']
252
+ }
253
+ ```
254
+
255
+ ### Security Rules
256
+
257
+ ```javascript
258
+ match /apps/{appId} {
259
+ allow read: if request.auth.uid == resource.data.userId;
260
+ allow create: if request.auth.uid == request.resource.data.userId
261
+ && request.resource.data.keys().hasAll(['name', 'packageId', 'userId']);
262
+ allow update: if request.auth.uid == resource.data.userId;
263
+ allow delete: if request.auth.uid == resource.data.userId;
264
+ }
265
+ ```
266
+
267
+ ---
268
+
269
+ ## 📁 Collection 3: `builds`
270
+
271
+ ### Purpose
272
+ Store build metadata and distribution info
273
+
274
+ ### Document ID
275
+ Auto-generated
276
+
277
+ ### Schema
278
+
279
+ ```typescript
280
+ interface BuildDocument {
281
+ // Identity
282
+ id: string; // Auto-generated doc ID
283
+ userId: string; // Owner
284
+ appId: string; // Parent app
285
+
286
+ // Version Info
287
+ version: string; // Semantic version (1.0.0)
288
+ buildNumber: number; // Integer build number
289
+ versionCode: number; // Android version code
290
+ bundleVersion: string; // iOS bundle version
291
+
292
+ // Distribution
293
+ channel: 'production' | 'staging' | 'development';
294
+ platform: 'ios' | 'android' | 'web';
295
+
296
+ // File Info
297
+ fileName: string; // Original filename
298
+ fileSize: number; // Size in bytes
299
+ fileType: 'zip' | 'apk' | 'ipa';
300
+ mimeType: string; // MIME type
301
+
302
+ // Security
303
+ checksum: string; // SHA-256 hash
304
+ signature: string | null; // Digital signature (future)
305
+
306
+ // Google Drive
307
+ driveFileId: string; // Google Drive file ID
308
+ driveFileUrl: string; // Direct download URL (expiring)
309
+ driveFolderId: string; // Parent folder ID
310
+
311
+ // Release Info
312
+ releaseNotes: string; // What's new
313
+ releaseType: 'major' | 'minor' | 'patch' | 'hotfix';
314
+ isPreRelease: boolean; // Beta/alpha?
315
+
316
+ // Upload Info
317
+ uploadedAt: Timestamp; // Upload timestamp
318
+ uploadedBy: string; // User email
319
+ uploadDuration: number; // Upload time (ms)
320
+
321
+ // Status
322
+ status: 'uploading' | 'processing' | 'active' | 'archived' | 'failed';
323
+ error: string | null; // Error message if failed
324
+
325
+ // Analytics (aggregated)
326
+ downloads: number; // Total downloads
327
+ installs: number; // Successful installs
328
+ rollbacks: number; // Rollback count
329
+ errors: number; // Installation errors
330
+
331
+ // Metadata
332
+ updatedAt: Timestamp;
333
+ }
334
+ ```
335
+
336
+ ### Example Document
337
+
338
+ ```json
339
+ {
340
+ "id": "build_xyz456",
341
+ "userId": "abc123xyz789",
342
+ "appId": "app_abc123",
343
+
344
+ "version": "1.0.1",
345
+ "buildNumber": 2,
346
+ "versionCode": 101,
347
+ "bundleVersion": "1.0.1",
348
+
349
+ "channel": "production",
350
+ "platform": "android",
351
+
352
+ "fileName": "app-release-1.0.1.zip",
353
+ "fileSize": 31457280,
354
+ "fileType": "zip",
355
+ "mimeType": "application/zip",
356
+
357
+ "checksum": "sha256:a1b2c3d4e5f6...",
358
+ "signature": null,
359
+
360
+ "driveFileId": "1a2b3c4d5e6f7g8h9i0j",
361
+ "driveFileUrl": "https://drive.google.com/uc?export=download&id=...",
362
+ "driveFolderId": "folder123abc",
363
+
364
+ "releaseNotes": "Bug fixes and performance improvements",
365
+ "releaseType": "patch",
366
+ "isPreRelease": false,
367
+
368
+ "uploadedAt": {"seconds": 1703808000, "nanoseconds": 0},
369
+ "uploadedBy": "user@example.com",
370
+ "uploadDuration": 12500,
371
+
372
+ "status": "active",
373
+ "error": null,
374
+
375
+ "downloads": 523,
376
+ "installs": 498,
377
+ "rollbacks": 2,
378
+ "errors": 1,
379
+
380
+ "updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
381
+ }
382
+ ```
383
+
384
+ ### Indexes Required
385
+
386
+ ```javascript
387
+ // Composite indexes
388
+ builds: {
389
+ fields: ['userId', 'uploadedAt'],
390
+ fields: ['appId', 'channel', 'uploadedAt'],
391
+ fields: ['userId', 'status', 'uploadedAt'],
392
+ fields: ['appId', 'platform', 'channel', 'version']
393
+ }
394
+ ```
395
+
396
+ ### Security Rules
397
+
398
+ ```javascript
399
+ match /builds/{buildId} {
400
+ allow read: if request.auth.uid == resource.data.userId;
401
+ allow create: if request.auth.uid == request.resource.data.userId
402
+ && exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
403
+ && get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
404
+ allow update: if request.auth.uid == resource.data.userId;
405
+ allow delete: if request.auth.uid == resource.data.userId;
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## 📁 Collection 4: `drive_tokens`
412
+
413
+ ### Purpose
414
+ Store encrypted Google Drive OAuth tokens
415
+
416
+ ### Document ID
417
+ `{userId}` - Firebase Auth UID
418
+
419
+ ### Schema
420
+
421
+ ```typescript
422
+ interface DriveTokenDocument {
423
+ // Identity
424
+ userId: string; // Firebase Auth UID
425
+
426
+ // OAuth Tokens (ENCRYPTED)
427
+ accessToken: string; // Encrypted access token
428
+ refreshToken: string; // Encrypted refresh token
429
+
430
+ // Token Metadata
431
+ tokenType: string; // Usually "Bearer"
432
+ scope: string[]; // Granted scopes
433
+ expiresAt: Timestamp; // Token expiration
434
+
435
+ // Encryption Info
436
+ encryptionMethod: 'AES-256-GCM';
437
+ iv: string; // Initialization vector (base64)
438
+ authTag: string; // Authentication tag (base64)
439
+
440
+ // Metadata
441
+ createdAt: Timestamp; // First connection
442
+ updatedAt: Timestamp; // Last token refresh
443
+ }
444
+ ```
445
+
446
+ ### Example Document (Encrypted)
447
+
448
+ ```json
449
+ {
450
+ "userId": "abc123xyz789",
451
+
452
+ "accessToken": "encrypted:gAAAAABl...",
453
+ "refreshToken": "encrypted:gAAAAABl...",
454
+
455
+ "tokenType": "Bearer",
456
+ "scope": [
457
+ "https://www.googleapis.com/auth/drive.file"
458
+ ],
459
+ "expiresAt": {"seconds": 1703811600, "nanoseconds": 0},
460
+
461
+ "encryptionMethod": "AES-256-GCM",
462
+ "iv": "base64_encoded_iv_here",
463
+ "authTag": "base64_encoded_tag_here",
464
+
465
+ "createdAt": {"seconds": 1703722000, "nanoseconds": 0},
466
+ "updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
467
+ }
468
+ ```
469
+
470
+ ### Security Rules (VERY RESTRICTIVE)
471
+
472
+ ```javascript
473
+ match /drive_tokens/{userId} {
474
+ // Only backend functions can write
475
+ allow read, write: if false;
476
+
477
+ // Exception: Cloud Functions have admin access
478
+ // Client-side code should NEVER access this collection directly
479
+ }
480
+ ```
481
+
482
+ ### Encryption Implementation
483
+
484
+ ```typescript
485
+ // Backend only - NEVER expose to client
486
+ import * as crypto from 'crypto';
487
+
488
+ const ENCRYPTION_KEY = process.env.DRIVE_TOKEN_ENCRYPTION_KEY; // 32 bytes
489
+
490
+ function encryptToken(token: string): {
491
+ encrypted: string;
492
+ iv: string;
493
+ authTag: string;
494
+ } {
495
+ const iv = crypto.randomBytes(16);
496
+ const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
497
+
498
+ let encrypted = cipher.update(token, 'utf8', 'base64');
499
+ encrypted += cipher.final('base64');
500
+
501
+ const authTag = cipher.getAuthTag().toString('base64');
502
+
503
+ return {
504
+ encrypted: `encrypted:${encrypted}`,
505
+ iv: iv.toString('base64'),
506
+ authTag
507
+ };
508
+ }
509
+
510
+ function decryptToken(encrypted: string, iv: string, authTag: string): string {
511
+ const decipher = crypto.createDecipheriv(
512
+ 'aes-256-gcm',
513
+ Buffer.from(ENCRYPTION_KEY, 'hex'),
514
+ Buffer.from(iv, 'base64')
515
+ );
516
+
517
+ decipher.setAuthTag(Buffer.from(authTag, 'base64'));
518
+
519
+ let decrypted = decipher.update(encrypted.replace('encrypted:', ''), 'base64', 'utf8');
520
+ decrypted += decipher.final('utf8');
521
+
522
+ return decrypted;
523
+ }
524
+ ```
525
+
526
+ ---
527
+
528
+ ## 📁 Collection 5: `analytics` (Future)
529
+
530
+ ### Purpose
531
+ Track build downloads, installations, and errors
532
+
533
+ ### Document ID
534
+ Auto-generated
535
+
536
+ ### Schema
537
+
538
+ ```typescript
539
+ interface AnalyticsDocument {
540
+ // Identity
541
+ userId: string;
542
+ appId: string;
543
+ buildId: string;
544
+
545
+ // Event Info
546
+ eventType: 'download' | 'install' | 'rollback' | 'error' | 'check';
547
+ eventData: Record<string, any>; // Event-specific data
548
+
549
+ // Device Info
550
+ deviceId: string; // Hashed device ID
551
+ platform: 'ios' | 'android' | 'web';
552
+ osVersion: string;
553
+ appVersion: string;
554
+
555
+ // Location (optional)
556
+ country: string | null;
557
+ region: string | null;
558
+
559
+ // Timestamp
560
+ timestamp: Timestamp;
561
+ }
562
+ ```
563
+
564
+ ---
565
+
566
+ ## 🔐 Firestore Security Rules (Complete)
567
+
568
+ ```javascript
569
+ rules_version = '2';
570
+ service cloud.firestore {
571
+ match /databases/{database}/documents {
572
+
573
+ // Helper functions
574
+ function isAuthenticated() {
575
+ return request.auth != null;
576
+ }
577
+
578
+ function isOwner(userId) {
579
+ return isAuthenticated() && request.auth.uid == userId;
580
+ }
581
+
582
+ function emailVerified() {
583
+ return request.auth.token.email_verified == true;
584
+ }
585
+
586
+ // Users collection
587
+ match /users/{userId} {
588
+ allow read: if isOwner(userId);
589
+ allow create: if isOwner(userId) && emailVerified();
590
+ allow update: if isOwner(userId)
591
+ && !request.resource.data.diff(resource.data).affectedKeys()
592
+ .hasAny(['uid', 'email', 'createdAt', 'provider']);
593
+ allow delete: if isOwner(userId);
594
+ }
595
+
596
+ // Apps collection
597
+ match /apps/{appId} {
598
+ allow read: if isOwner(resource.data.userId);
599
+ allow create: if isAuthenticated() && emailVerified()
600
+ && request.resource.data.userId == request.auth.uid;
601
+ allow update, delete: if isOwner(resource.data.userId);
602
+ }
603
+
604
+ // Builds collection
605
+ match /builds/{buildId} {
606
+ allow read: if isOwner(resource.data.userId);
607
+ allow create: if isAuthenticated() && emailVerified()
608
+ && request.resource.data.userId == request.auth.uid
609
+ && exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
610
+ && get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
611
+ allow update, delete: if isOwner(resource.data.userId);
612
+ }
613
+
614
+ // Drive tokens (backend only)
615
+ match /drive_tokens/{userId} {
616
+ allow read, write: if false; // Client can NEVER access
617
+ }
618
+
619
+ // Analytics (write only for authenticated users)
620
+ match /analytics/{eventId} {
621
+ allow read: if false; // No client reads
622
+ allow create: if isAuthenticated();
623
+ }
624
+ }
625
+ }
626
+ ```
627
+
628
+ ---
629
+
630
+ ## 📊 Firestore Indexes
631
+
632
+ ### Required Composite Indexes
633
+
634
+ ```javascript
635
+ // firestore.indexes.json
636
+ {
637
+ "indexes": [
638
+ {
639
+ "collectionGroup": "users",
640
+ "queryScope": "COLLECTION",
641
+ "fields": [
642
+ { "fieldPath": "email", "order": "ASCENDING" },
643
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
644
+ ]
645
+ },
646
+ {
647
+ "collectionGroup": "apps",
648
+ "queryScope": "COLLECTION",
649
+ "fields": [
650
+ { "fieldPath": "userId", "order": "ASCENDING" },
651
+ { "fieldPath": "createdAt", "order": "DESCENDING" }
652
+ ]
653
+ },
654
+ {
655
+ "collectionGroup": "builds",
656
+ "queryScope": "COLLECTION",
657
+ "fields": [
658
+ { "fieldPath": "userId", "order": "ASCENDING" },
659
+ { "fieldPath": "uploadedAt", "order": "DESCENDING" }
660
+ ]
661
+ },
662
+ {
663
+ "collectionGroup": "builds",
664
+ "queryScope": "COLLECTION",
665
+ "fields": [
666
+ { "fieldPath": "appId", "order": "ASCENDING" },
667
+ { "fieldPath": "channel", "order": "ASCENDING" },
668
+ { "fieldPath": "uploadedAt", "order": "DESCENDING" }
669
+ ]
670
+ },
671
+ {
672
+ "collectionGroup": "builds",
673
+ "queryScope": "COLLECTION",
674
+ "fields": [
675
+ { "fieldPath": "appId", "order": "ASCENDING" },
676
+ { "fieldPath": "platform", "order": "ASCENDING" },
677
+ { "fieldPath": "channel", "order": "ASCENDING" },
678
+ { "fieldPath": "version", "order": "DESCENDING" }
679
+ ]
680
+ }
681
+ ]
682
+ }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## ✅ Implementation Checklist
688
+
689
+ - [ ] Create Firestore database
690
+ - [ ] Define all collections and schemas
691
+ - [ ] Implement security rules
692
+ - [ ] Deploy security rules: `firebase deploy --only firestore:rules`
693
+ - [ ] Create composite indexes
694
+ - [ ] Deploy indexes: `firebase deploy --only firestore:indexes`
695
+ - [ ] Test read/write permissions
696
+ - [ ] Setup encryption for drive_tokens
697
+ - [ ] Document all collections
698
+ - [ ] Create TypeScript interfaces
699
+
700
+ ---
701
+
702
+ **Plan Status:** ✅ Complete and ready for implementation
703
+ **Database Type:** Firestore (NoSQL)
704
+ **Total Collections:** 5 (4 active + 1 future)