Firestore Data Model
Quick links: Frontend β’ Backend
Backendβ
Collections:
-
users/{uid}email: stringstripeCustomerId: stringisStaff?: boolean(staff member flag for admin features)role?: 'admin' | 'support'(staff role for access control)- Timestamps:
createdAt,updatedAt
-
users/{uid}/subscription/currentcustomerId: stringsubscriptionId: stringstatus: 'trialing' | 'active' | 'canceled' | 'past_due' | 'unpaid'planId: 'starter' | 'pro'priceId: string(Stripe price ID)currentPeriodStart: timestampcurrentPeriodEnd: timestamptrialEnd: timestamp | nullcancelAtPeriodEnd: booleanpendingPlan?: { priceId: string, planId: string, creditsPerPeriod: number, appliesAt: timestamp }(scheduled plan change)createdAt,updatedAt
-
users/{uid}/integrations/{provider}defaultAccountId: string | nullconnectedCount: numberfeatures: { repostEnabled: boolean, templatesEnabled: boolean }createdAt,updatedAt- Subcollection
accounts/{accountId}platform: 'facebook' | 'instagram' | ...displayName,username,avatarUrlaccessToken,refreshToken?,tokenType?expiresIn?,expiresAt?enabled_for_repost,can_be_sourceenable_text_repurpose_target,enable_image_repurpose_targetprofile: provider-specific metadatapoll_cursor: { last_seen_id: string | null, last_seen_time: number | null }last_polled_at: timestamp | null- Provider-specific operational fields (e.g.,
last_video_extraction_timefor TikTok,pageAccessTokenfor Facebook) createdAt,updatedAt
-
oauthStates/{state}ephemeral state docs\{ uid, provider, returnTo, createdAt \} -
tempSubscriptions/{customerId}temp storage during checkout flow -
processedSessions/{sessionId}idempotency marker for verify-session -
postSubmissions/{postSubmissionId}userId: stringkeyHash: string(hash of API key that created submission)request: object(original request body)status: 'queued' | 'orchestrating' | 'dispatched' | 'dispatch_failed' | 'scheduled'targets: { platform: string, accountId: string, targetId: string }[]scheduledTime: string | null(ISO 8601 datetime)scheduledTask?: { queue: string, taskName: string, scheduledTime: string }(Cloud Tasks metadata for scheduled posts)normalization?: { url: string, contentType: string }(media normalization results)createdAt: timestampupdatedAt: timestamp(set during orchestration)dispatchedAt: timestamp(set when all tasks enqueued)failedTargetDetails?: { platform: string, accountId?: string, targetId: string, error: string }[]failedTargetsLegacy?: string[](deprecated: list of failed platforms)
-
postSubmissions/{postSubmissionId}/runs/{targetId}platform: stringaccountId: string | nulltargetId: string(format:{platform}:{accountId}or just{platform})status: 'queued' | 'processing' | 'succeeded' | 'failed'attemptCount: numberenqueuedAt?: timestampstartedAt?: timestampcompletedAt?: timestampscheduledAt?: timestamp | nullupdatedAt: timestamptaskName?: string(Cloud Tasks task name for tracking)result?: object(provider-specific success data, e.g., post ID, URL)lastError?: string(error message if status is 'failed')
-
users/{uid}/posts/{postId}(user-visible published posts)postSubmissionId: string | nullplatform: 'facebook' | 'instagram' | 'threads' | 'x' | 'tiktok' | 'youtube' | 'linkedin'accountId: string | nullcaption: string | nullvideoURL: string | nullpostURL: string | nullpostId: string | null(platform's native post ID)platformUsername: string | nullaccountDisplayName: string | nullaccountAvatarUrl: string | nullmediaType: 'text' | 'image' | 'video' | nullthumbnails: string[]status: 'published'createdAt: timestamp- Doc IDs follow
<platform>_<accountId>_<platformPostId>(e.g.,instagram_acct42_179002837) to keep runs and metrics per account.
-
users/{uid}/repurposeLinks/{linkId}platform: stringaccountId: string | nulltype: 'text' | 'image' | 'video'templateId: stringsettings: { delayHours?: number }createdAt,updatedAt- When users connect multiple accounts on the same platform, all writes must include
accountIdso automations stay scoped to the intended account.
-
users/{uid}/automations_workflows/{workflowId}(new for multi-source reposting)source: { platform: string, accountId: string, targetId: string }triggers: { kind: 'organic_post_created', filters?: { mediaType?: string[], hashtags?: string[] } }[]repostTargets: { platform: string, accountId: string, targetId: string, enabled: boolean }[]repurposeActions: { templateId: string, platform: string, accountId: string | null, type: 'image' | 'video', delayHours?: number, aiPrompts?: { overlay?: string, caption?: string } }[]status: 'active' | 'paused'createdAt,updatedAt,lastRunAt?: timestamp- Workflows encapsulate the relationship between a specific source account and one or more repost/repurpose automations. Each workflow owns its own target list, enabling βTikTok A1 β Instagram A1β and βTikTok A1 β LinkedIn A2β to coexist.
-
users/{uid}/organicPosts/{postId}.workflows/{workflowId}workflowId: stringstatus: 'pending' | 'running' | 'succeeded' | 'failed'repostSubmissionId?: stringlastError?: stringupdatedAt: timestamp- This per-workflow status document records whether a workflow has been executed for a given organic post, allowing retries or additional workflows without touching already-completed ones.
-
media/\{id\}(uploaded media metadata)userId: stringkeyHash: string(API key hash that uploaded this media)url: string(public URL via media proxy)contentType: string(MIME type, e.g., "video/mp4", "image/jpeg")objectPath: string(Cloud Storage path: "media/{id}.{ext}")downloadToken: string(Firebase Storage download token)ext: string | null(file extension)originalUrl: string(original remote URL)finalUrl: string(resolved URL after redirects)createdAt: timestamp- Subcollection
transcripts/{jobId}(AI transcription results)text: string(transcribed text)model: string(e.g., "whisper-1")language: string | null(ISO 639-1 code)durationSeconds: number | nullprovider: 'openai' | stringlikelyLyrics: boolean(music detection flag)createdAt: timestamp
-
users/{uid}/organicPosts/{postId}(detected organic posts from social platforms)source_platform: 'tiktok' | 'instagram'source_account_id: string | nullexternal_post_id: string(platform's native post ID)created_at_source: timestamp(when post was published on platform)caption: stringshare_url: string(public URL to view post)source_video_url: string | nullcover_image_url: string | null(thumbnail/cover image)duration: number | null(video duration in seconds)width: number | null(video width in pixels)height: number | null(video height in pixels)embed_link: string | nullembed_html: string | nullstatus: 'detected' | 'reposting_initiated' | 'reposting_failed'transcription?: string(AI-generated transcript)created_at: timestamp(when detected by system)updated_at: string (ISO)reposting_initiated_at?: string (ISO)- Subcollection
workflows/{workflowId}(per-workflow execution status)workflowId: stringstatus: 'pending' | 'running' | 'succeeded' | 'failed'repostSubmissionId?: stringlastError?: stringupdatedAt: timestamp
Analytics & Metricsβ
-
users/{uid}/integrations/tiktokmetrics_tracking_enabled_since: string (ISO)when metrics collection is enabled
-
users/{uid}/postMetrics/{postId}(current metrics for a post)platform: 'tiktok' | ...external_post_id: stringaccountId: string | nulllike_count: numbercomment_count: numberview_count: numbershare_count: numberupdated_at: string (ISO)
-
users/{uid}/postMetrics/{postId}/snapshots/{YYYY-MM-DDTHH}(hourly snapshots)like_count: numbercomment_count: numberview_count: numbershare_count: numbercaptured_at: timestamp
-
users/{uid}/aggregateMetrics/overalltotal_likes: numbertotal_comments: numbertotal_views: numbertotal_shares: numberupdated_at: string (ISO)
-
users/{uid}/aggregateMetrics/{platform}(e.g.,tiktok)platform: stringtotal_likes: numbertotal_comments: numbertotal_views: numbertotal_shares: numberupdated_at: string (ISO)
-
apiKeys/{hash}(root collection, SHA-256 hash as doc ID)userId: stringname: string | null(user-provided key name)createdAt: timestamprevokedAt: timestamp | null(null = active, timestamp = revoked)- Security: Admin SDK only, never exposed to client
-
users/{uid}/apiKeys/{keyId}(user-scoped API key metadata)name: string | nulltokenPreview: string(last 4 characters for UI display)createdAt: timestamprevokedAt: timestamp | null- Security: User can read/create, Admin SDK handles revocation
-
users/\{uid\}/apiRequests/\{id\}(API request audit log)method: string(HTTP method: GET, POST, etc.)route: string(API route: "/v1/posts", "/v1/media")status: number(HTTP status code: 200, 201, 400, 500, etc.)durationMs: number(request duration in milliseconds)info: any[](request metadata, e.g., target platforms)postSubmissionId?: string(linked submission ID if applicable)createdAt: timestamp
-
users/{uid}/aiJobs/{jobId}feature: 'transcription' | stringmediaId?: string(for media-based jobs)input?: object(for URL-based jobs, e.g.,\{ url \})provider: 'openai' | stringstatus: 'pending' | 'running' | 'succeeded' | 'failed'estimatedCredits: number | nullfinalCredits: number | nulltranscript?: string(for URL-based jobs)idempotencyKey: stringcreatedAt: timestampupdatedAt: timestamp
-
rendered_media/\{id\}(template-rendered OG images)templateSlug: string(template identifier, e.g., "tweetImage")config: object(template-specific configuration)url: string(raw Firebase Storage URL with download token)friendlyUrl: string(public proxied URL via media function)objectPath: string(Cloud Storage path)contentType: string(usually "image/png")createdAt: timestamp
Observability & Error Trackingβ
-
logs/{eventId}(structured event logs with distributed tracing)level: 'debug' | 'info' | 'warn' | 'error'message: string(human-readable log message)source: 'api' | 'integration' | 'orchestrator' | string(system component)subsystem: string(more specific component, e.g., "publishing", "auth")userId: string | null(associated user if applicable)platform: string | null(social platform if applicable)traceId: string | null(distributed trace ID)spanId: string | null(span ID within trace)flowId: string | null(business flow ID, e.g., postSubmissionId)depth: number(span nesting depth for trace hierarchy)spanStatus: 'ok' | 'error'(span completion status)operationType: string(operation category: "http_request", "function", "database", "media_processing", "task")context: object(structured context data)error: object | null({ name, message, stack, code })createdAt: timestampbucketHour: string(format: "YYYY-MM-DD-HH" for time-series queries)- Security: Staff-only read (requires isStaff or role == admin/support)
- Indexes: 16 composite indexes for querying by time, level, source, user, platform, trace, etc.
-
errorLogs/\{id\}(detailed API error tracking)timestamp: string (ISO)error: object{ name, message, code, statusCode, stack, details }request: object{ method, url, headers (redacted), body (truncated), query, params, ip }user: object{ userId, keyHash }createdAt: timestamp- Note: Critical errors are also logged to
logscollection with level: "error"
Rate Limitingβ
-
rateLimits/{key}(rate limit tracking with sliding window)requests: number[](array of request timestamps within current window)updatedAt: timestamptier: 'default' | 'authenticated' | 'premium' | 'admin'key: string(rate limit key, e.g., "user:abc123" or "ip:192.168.1.1")endpoint?: string(for endpoint-specific rate limiting)- Cleanup: Scheduled function removes docs older than 24 hours
- Security: Admin SDK only
-
users/{uid}/credits/currentbalance: numberperiodStart: timestampperiodEnd: timestampplanId: string | nullpriceId: string | nullupdatedAt: timestamp
-
users/{uid}/creditLedger/{entryId}(immutable)type: 'topup' | 'debit' | 'adjustment' | 'refund'amount: number(top-ups positive, debits negative)source:\{ event: string, id: string \} | nullidempotencyKey: stringcreatedAt: timestamp
Additional Root Collectionsβ
-
oauthStates/{state}(ephemeral OAuth state tracking)uid: string(user ID initiating OAuth)provider: string(platform name: "facebook", "instagram", etc.)returnTo: string(URL to redirect after completion)createdAt: timestamp- TTL: Should be auto-deleted after 10 minutes (recommended TTL policy)
-
tempSubscriptions/{customerId}(temporary subscription data during checkout)- Used during Stripe checkout flow
- Auto-cleaned after session completion
- Security: Admin SDK only
-
processedSessions/{sessionId}(idempotency tracking for Stripe sessions)- Prevents duplicate subscription processing
- Security: Admin SDK only
Notesβ
Credits Systemβ
- The
balanceinusers/{uid}/credits/currentis a cached value derived from the sum of ledger entries within the current period. - All credit operations are logged to
creditLedgerfor audit trail and dispute resolution. - Ledger entries are immutable and use
idempotencyKeyto prevent duplicate charges.
Subscription Managementβ
- Subscription doc may include
pendingPlanfor next-period plan changes. - Status transitions:
trialingβactiveβpast_dueorcanceled - Subscription enforcement checks both status and
currentPeriodEnd > now
Multi-Account Architectureβ
- All platform integrations support multiple accounts per platform via
accounts/{accountId}subcollection. defaultAccountIdis used when API requests don't specify an account.- Post submissions require explicit
accountIdwhen multiple accounts exist on a platform. targetIdformat:{platform}:{accountId}for unique identification.
Composite Indexes (Multi-Source Reposting)β
users/{uid}/automations_workflowsonsource.targetIdfor "find workflows for this source account"users/{uid}/automations_workflowsonrepostTargets.targetIdfor auditing/visualizing destinations- Collection group index on
users/{uid}/organicPosts/{postId}/workflowsfor monitoring workflow execution states - Collection group index on
integrations.can_be_source + enabled_for_repostfor repost source detection - Collection group index on
posts.platform + accountId + createdAtfor user post history queries - Collection group index on
organicPosts.source_platform + source_account_id + created_at_sourcefor organic post polling - Collection group index on
accounts.platformfor platform-wide account queries
Observability Indexesβ
- 16 composite indexes on
logscollection for complex queries:- Time-series queries:
bucketHour DESC + createdAt DESC - Trace reconstruction:
traceId ASC + depth ASC + createdAt ASC - User activity:
userId ASC + level ASC + createdAt DESC - Platform errors:
platform ASC + bucketHour DESC + createdAt DESC - Operation types:
operationType ASC + bucketHour DESC + createdAt DESC
- Time-series queries:
Security Rulesβ
Client Permissionsβ
- Users can read/update their own documents:
request.auth.uid == uid - Users can read their own posts, subscriptions, integrations (read-only)
- Users can create/read API keys (revocation via Admin SDK)
- Users can read/write their own posts
- OAuth states: Users can read/create/delete their own states
Admin SDK Only (Server-Side)β
- All writes to subscription, integration accounts, credits
- API key revocation
- Post submission status updates
- Run document creation/updates
- Error logging
- Rate limit tracking
- OAuth token storage
Staff-Only Accessβ
logscollection: RequiresisStaff == trueorrole == 'admin' | 'support'- Used for debugging, monitoring, and support operations
Related frontend: see Frontend
Frontendβ
- UI reads
users/{uid}/integrations/*to render connected profiles inPlatformPicker. - UI reads
users/{uid}/subscription/currentto gate features.
Related backend: see Backend