native-update
Version:
Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.
705 lines (558 loc) • 18.1 kB
Markdown
# Task 2: Database Schema Design
**Created:** 2025-12-27
**Status:** 📝 Planning
**Database:** Firebase Firestore
## 🎯 Objectives
Design a complete, scalable Firestore database schema that supports:
- User management and authentication
- Multi-app management per user
- Build versioning and distribution
- Google Drive token storage (encrypted)
- Analytics tracking (future)
## 📊 Collections Overview
```
firestore/
├── users/ # User profiles
├── apps/ # User's applications
├── builds/ # Build versions
├── drive_tokens/ # Encrypted Drive credentials
└── analytics/ # Usage analytics (future)
```
## 📁 Collection 1: `users`
### Purpose
Store user profile information and preferences
### Document ID
`{userId}` - Firebase Auth UID
### Schema
```typescript
interface UserDocument {
// Identity
uid: string; // Firebase Auth UID (redundant but helpful)
email: string; // User email
displayName: string | null; // Full name
photoURL: string | null; // Profile picture URL
// Authentication
provider: 'email' | 'google.com'; // Auth provider
emailVerified: boolean; // Email verification status
createdAt: Timestamp; // Account creation
lastLogin: Timestamp; // Last login time
// Google Drive Integration
driveConnected: boolean; // Drive connected?
driveEmail: string | null; // Drive account email
driveConnectedAt: Timestamp | null; // When connected
// Subscription (future)
plan: 'free' | 'pro' | 'enterprise';
planStartDate: Timestamp | null;
planEndDate: Timestamp | null;
// Usage Stats
appsCount: number; // Total apps created
buildsCount: number; // Total builds uploaded
storageUsed: number; // Bytes used (in Drive)
// Preferences
preferences: {
emailNotifications: boolean; // Send email notifications
updateNotifications: boolean; // Notify on updates
theme: 'light' | 'dark' | 'auto';
language: 'en' | 'es' | 'fr'; // i18n support
};
// Metadata
updatedAt: Timestamp; // Last profile update
}
```
### Example Document
```json
{
"uid": "abc123xyz789",
"email": "user@example.com",
"displayName": "John Doe",
"photoURL": "https://lh3.googleusercontent.com/...",
"provider": "google.com",
"emailVerified": true,
"createdAt": {"seconds": 1703721600, "nanoseconds": 0},
"lastLogin": {"seconds": 1703808000, "nanoseconds": 0},
"driveConnected": true,
"driveEmail": "user@gmail.com",
"driveConnectedAt": {"seconds": 1703722000, "nanoseconds": 0},
"plan": "free",
"planStartDate": null,
"planEndDate": null,
"appsCount": 2,
"buildsCount": 5,
"storageUsed": 157286400,
"preferences": {
"emailNotifications": true,
"updateNotifications": true,
"theme": "auto",
"language": "en"
},
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
}
```
### Indexes Required
```javascript
// Composite indexes
users: {
fields: ['email', 'createdAt'],
fields: ['plan', 'createdAt']
}
```
### Security Rules
```javascript
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow create: if request.auth.uid == userId;
allow update: if request.auth.uid == userId
&& !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['uid', 'email', 'createdAt', 'provider']);
allow delete: if request.auth.uid == userId;
}
```
## 📁 Collection 2: `apps`
### Purpose
Store user's application configurations
### Document ID
Auto-generated
### Schema
```typescript
interface AppDocument {
// Identity
id: string; // Auto-generated doc ID
userId: string; // Owner (Firebase Auth UID)
// App Info
name: string; // App display name
packageId: string; // com.example.app
icon: string | null; // App icon URL
description: string; // App description
// Platform Support
platforms: ('ios' | 'android' | 'web')[];
// Update Channels
channels: {
production: ChannelConfig;
staging: ChannelConfig;
development: ChannelConfig;
};
// Stats
totalBuilds: number; // Total builds uploaded
activeUsers: number; // Active installs (future)
lastBuildDate: Timestamp | null; // Last build upload
// Metadata
createdAt: Timestamp;
updatedAt: Timestamp;
}
interface ChannelConfig {
enabled: boolean; // Channel active?
autoUpdate: boolean; // Auto-update enabled?
updateStrategy: 'immediate' | 'background' | 'manual';
requireUserConsent: boolean; // Ask before update?
minVersion: string | null; // Minimum app version (semver)
}
```
### Example Document
```json
{
"id": "app_abc123",
"userId": "abc123xyz789",
"name": "My Awesome App",
"packageId": "com.example.awesome",
"icon": "https://storage.googleapis.com/.../icon.png",
"description": "A great mobile application",
"platforms": ["ios", "android"],
"channels": {
"production": {
"enabled": true,
"autoUpdate": true,
"updateStrategy": "background",
"requireUserConsent": false,
"minVersion": "1.0.0"
},
"staging": {
"enabled": true,
"autoUpdate": false,
"updateStrategy": "manual",
"requireUserConsent": true,
"minVersion": null
},
"development": {
"enabled": true,
"autoUpdate": true,
"updateStrategy": "immediate",
"requireUserConsent": false,
"minVersion": null
}
},
"totalBuilds": 5,
"activeUsers": 1234,
"lastBuildDate": {"seconds": 1703808000, "nanoseconds": 0},
"createdAt": {"seconds": 1703721600, "nanoseconds": 0},
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
}
```
### Indexes Required
```javascript
// Composite indexes
apps: {
fields: ['userId', 'createdAt'],
fields: ['userId', 'platforms', 'createdAt']
}
```
### Security Rules
```javascript
match /apps/{appId} {
allow read: if request.auth.uid == resource.data.userId;
allow create: if request.auth.uid == request.resource.data.userId
&& request.resource.data.keys().hasAll(['name', 'packageId', 'userId']);
allow update: if request.auth.uid == resource.data.userId;
allow delete: if request.auth.uid == resource.data.userId;
}
```
## 📁 Collection 3: `builds`
### Purpose
Store build metadata and distribution info
### Document ID
Auto-generated
### Schema
```typescript
interface BuildDocument {
// Identity
id: string; // Auto-generated doc ID
userId: string; // Owner
appId: string; // Parent app
// Version Info
version: string; // Semantic version (1.0.0)
buildNumber: number; // Integer build number
versionCode: number; // Android version code
bundleVersion: string; // iOS bundle version
// Distribution
channel: 'production' | 'staging' | 'development';
platform: 'ios' | 'android' | 'web';
// File Info
fileName: string; // Original filename
fileSize: number; // Size in bytes
fileType: 'zip' | 'apk' | 'ipa';
mimeType: string; // MIME type
// Security
checksum: string; // SHA-256 hash
signature: string | null; // Digital signature (future)
// Google Drive
driveFileId: string; // Google Drive file ID
driveFileUrl: string; // Direct download URL (expiring)
driveFolderId: string; // Parent folder ID
// Release Info
releaseNotes: string; // What's new
releaseType: 'major' | 'minor' | 'patch' | 'hotfix';
isPreRelease: boolean; // Beta/alpha?
// Upload Info
uploadedAt: Timestamp; // Upload timestamp
uploadedBy: string; // User email
uploadDuration: number; // Upload time (ms)
// Status
status: 'uploading' | 'processing' | 'active' | 'archived' | 'failed';
error: string | null; // Error message if failed
// Analytics (aggregated)
downloads: number; // Total downloads
installs: number; // Successful installs
rollbacks: number; // Rollback count
errors: number; // Installation errors
// Metadata
updatedAt: Timestamp;
}
```
### Example Document
```json
{
"id": "build_xyz456",
"userId": "abc123xyz789",
"appId": "app_abc123",
"version": "1.0.1",
"buildNumber": 2,
"versionCode": 101,
"bundleVersion": "1.0.1",
"channel": "production",
"platform": "android",
"fileName": "app-release-1.0.1.zip",
"fileSize": 31457280,
"fileType": "zip",
"mimeType": "application/zip",
"checksum": "sha256:a1b2c3d4e5f6...",
"signature": null,
"driveFileId": "1a2b3c4d5e6f7g8h9i0j",
"driveFileUrl": "https://drive.google.com/uc?export=download&id=...",
"driveFolderId": "folder123abc",
"releaseNotes": "Bug fixes and performance improvements",
"releaseType": "patch",
"isPreRelease": false,
"uploadedAt": {"seconds": 1703808000, "nanoseconds": 0},
"uploadedBy": "user@example.com",
"uploadDuration": 12500,
"status": "active",
"error": null,
"downloads": 523,
"installs": 498,
"rollbacks": 2,
"errors": 1,
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
}
```
### Indexes Required
```javascript
// Composite indexes
builds: {
fields: ['userId', 'uploadedAt'],
fields: ['appId', 'channel', 'uploadedAt'],
fields: ['userId', 'status', 'uploadedAt'],
fields: ['appId', 'platform', 'channel', 'version']
}
```
### Security Rules
```javascript
match /builds/{buildId} {
allow read: if request.auth.uid == resource.data.userId;
allow create: if request.auth.uid == request.resource.data.userId
&& exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
&& get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
allow update: if request.auth.uid == resource.data.userId;
allow delete: if request.auth.uid == resource.data.userId;
}
```
## 📁 Collection 4: `drive_tokens`
### Purpose
Store encrypted Google Drive OAuth tokens
### Document ID
`{userId}` - Firebase Auth UID
### Schema
```typescript
interface DriveTokenDocument {
// Identity
userId: string; // Firebase Auth UID
// OAuth Tokens (ENCRYPTED)
accessToken: string; // Encrypted access token
refreshToken: string; // Encrypted refresh token
// Token Metadata
tokenType: string; // Usually "Bearer"
scope: string[]; // Granted scopes
expiresAt: Timestamp; // Token expiration
// Encryption Info
encryptionMethod: 'AES-256-GCM';
iv: string; // Initialization vector (base64)
authTag: string; // Authentication tag (base64)
// Metadata
createdAt: Timestamp; // First connection
updatedAt: Timestamp; // Last token refresh
}
```
### Example Document (Encrypted)
```json
{
"userId": "abc123xyz789",
"accessToken": "encrypted:gAAAAABl...",
"refreshToken": "encrypted:gAAAAABl...",
"tokenType": "Bearer",
"scope": [
"https://www.googleapis.com/auth/drive.file"
],
"expiresAt": {"seconds": 1703811600, "nanoseconds": 0},
"encryptionMethod": "AES-256-GCM",
"iv": "base64_encoded_iv_here",
"authTag": "base64_encoded_tag_here",
"createdAt": {"seconds": 1703722000, "nanoseconds": 0},
"updatedAt": {"seconds": 1703808000, "nanoseconds": 0}
}
```
### Security Rules (VERY RESTRICTIVE)
```javascript
match /drive_tokens/{userId} {
// Only backend functions can write
allow read, write: if false;
// Exception: Cloud Functions have admin access
// Client-side code should NEVER access this collection directly
}
```
### Encryption Implementation
```typescript
// Backend only - NEVER expose to client
import * as crypto from 'crypto';
const ENCRYPTION_KEY = process.env.DRIVE_TOKEN_ENCRYPTION_KEY; // 32 bytes
function encryptToken(token: string): {
encrypted: string;
iv: string;
authTag: string;
} {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
let encrypted = cipher.update(token, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag().toString('base64');
return {
encrypted: `encrypted:${encrypted}`,
iv: iv.toString('base64'),
authTag
};
}
function decryptToken(encrypted: string, iv: string, authTag: string): string {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(ENCRYPTION_KEY, 'hex'),
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(authTag, 'base64'));
let decrypted = decipher.update(encrypted.replace('encrypted:', ''), 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
```
## 📁 Collection 5: `analytics` (Future)
### Purpose
Track build downloads, installations, and errors
### Document ID
Auto-generated
### Schema
```typescript
interface AnalyticsDocument {
// Identity
userId: string;
appId: string;
buildId: string;
// Event Info
eventType: 'download' | 'install' | 'rollback' | 'error' | 'check';
eventData: Record<string, any>; // Event-specific data
// Device Info
deviceId: string; // Hashed device ID
platform: 'ios' | 'android' | 'web';
osVersion: string;
appVersion: string;
// Location (optional)
country: string | null;
region: string | null;
// Timestamp
timestamp: Timestamp;
}
```
## 🔐 Firestore Security Rules (Complete)
```javascript
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
function emailVerified() {
return request.auth.token.email_verified == true;
}
// Users collection
match /users/{userId} {
allow read: if isOwner(userId);
allow create: if isOwner(userId) && emailVerified();
allow update: if isOwner(userId)
&& !request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['uid', 'email', 'createdAt', 'provider']);
allow delete: if isOwner(userId);
}
// Apps collection
match /apps/{appId} {
allow read: if isOwner(resource.data.userId);
allow create: if isAuthenticated() && emailVerified()
&& request.resource.data.userId == request.auth.uid;
allow update, delete: if isOwner(resource.data.userId);
}
// Builds collection
match /builds/{buildId} {
allow read: if isOwner(resource.data.userId);
allow create: if isAuthenticated() && emailVerified()
&& request.resource.data.userId == request.auth.uid
&& exists(/databases/$(database)/documents/apps/$(request.resource.data.appId))
&& get(/databases/$(database)/documents/apps/$(request.resource.data.appId)).data.userId == request.auth.uid;
allow update, delete: if isOwner(resource.data.userId);
}
// Drive tokens (backend only)
match /drive_tokens/{userId} {
allow read, write: if false; // Client can NEVER access
}
// Analytics (write only for authenticated users)
match /analytics/{eventId} {
allow read: if false; // No client reads
allow create: if isAuthenticated();
}
}
}
```
## 📊 Firestore Indexes
### Required Composite Indexes
```javascript
// firestore.indexes.json
{
"indexes": [
{
"collectionGroup": "users",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "email", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "apps",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "userId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "builds",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "userId", "order": "ASCENDING" },
{ "fieldPath": "uploadedAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "builds",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "appId", "order": "ASCENDING" },
{ "fieldPath": "channel", "order": "ASCENDING" },
{ "fieldPath": "uploadedAt", "order": "DESCENDING" }
]
},
{
"collectionGroup": "builds",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "appId", "order": "ASCENDING" },
{ "fieldPath": "platform", "order": "ASCENDING" },
{ "fieldPath": "channel", "order": "ASCENDING" },
{ "fieldPath": "version", "order": "DESCENDING" }
]
}
]
}
```
## ✅ Implementation Checklist
- [ ] Create Firestore database
- [ ] Define all collections and schemas
- [ ] Implement security rules
- [ ] Deploy security rules: `firebase deploy --only firestore:rules`
- [ ] Create composite indexes
- [ ] Deploy indexes: `firebase deploy --only firestore:indexes`
- [ ] Test read/write permissions
- [ ] Setup encryption for drive_tokens
- [ ] Document all collections
- [ ] Create TypeScript interfaces
**Plan Status:** ✅ Complete and ready for implementation
**Database Type:** Firestore (NoSQL)
**Total Collections:** 5 (4 active + 1 future)