Skip to main content

Firestore Data Model

Quick links: Frontend β€’ Backend

Backend​

Collections:

  • users/{uid}

    • email: string
    • stripeCustomerId: string
    • isStaff?: boolean (staff member flag for admin features)
    • role?: 'admin' | 'support' (staff role for access control)
    • Timestamps: createdAt, updatedAt
  • users/{uid}/subscription/current

    • customerId: string
    • subscriptionId: string
    • status: 'trialing' | 'active' | 'canceled' | 'past_due' | 'unpaid'
    • planId: 'starter' | 'pro'
    • priceId: string (Stripe price ID)
    • currentPeriodStart: timestamp
    • currentPeriodEnd: timestamp
    • trialEnd: timestamp | null
    • cancelAtPeriodEnd: boolean
    • pendingPlan?: { priceId: string, planId: string, creditsPerPeriod: number, appliesAt: timestamp } (scheduled plan change)
    • createdAt, updatedAt
  • users/{uid}/integrations/{provider}

    • defaultAccountId: string | null
    • connectedCount: number
    • features: { repostEnabled: boolean, templatesEnabled: boolean }
    • createdAt, updatedAt
    • Subcollection accounts/{accountId}
      • platform: 'facebook' | 'instagram' | ...
      • displayName, username, avatarUrl
      • accessToken, refreshToken?, tokenType?
      • expiresIn?, expiresAt?
      • enabled_for_repost, can_be_source
      • enable_text_repurpose_target, enable_image_repurpose_target
      • profile: provider-specific metadata
      • poll_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_time for TikTok, pageAccessToken for 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: string
    • keyHash: 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: timestamp
    • updatedAt: 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: string
    • accountId: string | null
    • targetId: string (format: {platform}:{accountId} or just {platform})
    • status: 'queued' | 'processing' | 'succeeded' | 'failed'
    • attemptCount: number
    • enqueuedAt?: timestamp
    • startedAt?: timestamp
    • completedAt?: timestamp
    • scheduledAt?: timestamp | null
    • updatedAt: timestamp
    • taskName?: 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 | null
    • platform: 'facebook' | 'instagram' | 'threads' | 'x' | 'tiktok' | 'youtube' | 'linkedin'
    • accountId: string | null
    • caption: string | null
    • videoURL: string | null
    • postURL: string | null
    • postId: string | null (platform's native post ID)
    • platformUsername: string | null
    • accountDisplayName: string | null
    • accountAvatarUrl: string | null
    • mediaType: 'text' | 'image' | 'video' | null
    • thumbnails: 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: string
    • accountId: string | null
    • type: 'text' | 'image' | 'video'
    • templateId: string
    • settings: { delayHours?: number }
    • createdAt, updatedAt
    • When users connect multiple accounts on the same platform, all writes must include accountId so 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: string
    • status: 'pending' | 'running' | 'succeeded' | 'failed'
    • repostSubmissionId?: string
    • lastError?: string
    • updatedAt: 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: string
    • keyHash: 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 | null
      • provider: 'openai' | string
      • likelyLyrics: boolean (music detection flag)
      • createdAt: timestamp
  • users/{uid}/organicPosts/{postId} (detected organic posts from social platforms)

    • source_platform: 'tiktok' | 'instagram'
    • source_account_id: string | null
    • external_post_id: string (platform's native post ID)
    • created_at_source: timestamp (when post was published on platform)
    • caption: string
    • share_url: string (public URL to view post)
    • source_video_url: string | null
    • cover_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 | null
    • embed_html: string | null
    • status: '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: string
      • status: 'pending' | 'running' | 'succeeded' | 'failed'
      • repostSubmissionId?: string
      • lastError?: string
      • updatedAt: timestamp

Analytics & Metrics​

  • users/{uid}/integrations/tiktok

    • metrics_tracking_enabled_since: string (ISO) when metrics collection is enabled
  • users/{uid}/postMetrics/{postId} (current metrics for a post)

    • platform: 'tiktok' | ...
    • external_post_id: string
    • accountId: string | null
    • like_count: number
    • comment_count: number
    • view_count: number
    • share_count: number
    • updated_at: string (ISO)
  • users/{uid}/postMetrics/{postId}/snapshots/{YYYY-MM-DDTHH} (hourly snapshots)

    • like_count: number
    • comment_count: number
    • view_count: number
    • share_count: number
    • captured_at: timestamp
  • users/{uid}/aggregateMetrics/overall

    • total_likes: number
    • total_comments: number
    • total_views: number
    • total_shares: number
    • updated_at: string (ISO)
  • users/{uid}/aggregateMetrics/{platform} (e.g., tiktok)

    • platform: string
    • total_likes: number
    • total_comments: number
    • total_views: number
    • total_shares: number
    • updated_at: string (ISO)
  • apiKeys/{hash} (root collection, SHA-256 hash as doc ID)

    • userId: string
    • name: string | null (user-provided key name)
    • createdAt: timestamp
    • revokedAt: 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 | null
    • tokenPreview: string (last 4 characters for UI display)
    • createdAt: timestamp
    • revokedAt: 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' | string
    • mediaId?: string (for media-based jobs)
    • input?: object (for URL-based jobs, e.g., \{ url \})
    • provider: 'openai' | string
    • status: 'pending' | 'running' | 'succeeded' | 'failed'
    • estimatedCredits: number | null
    • finalCredits: number | null
    • transcript?: string (for URL-based jobs)
    • idempotencyKey: string
    • createdAt: timestamp
    • updatedAt: 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: timestamp
    • bucketHour: 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 logs collection with level: "error"

Rate Limiting​

  • rateLimits/{key} (rate limit tracking with sliding window)

    • requests: number[] (array of request timestamps within current window)
    • updatedAt: timestamp
    • tier: '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/current

    • balance: number
    • periodStart: timestamp
    • periodEnd: timestamp
    • planId: string | null
    • priceId: string | null
    • updatedAt: timestamp
  • users/{uid}/creditLedger/{entryId} (immutable)

    • type: 'topup' | 'debit' | 'adjustment' | 'refund'
    • amount: number (top-ups positive, debits negative)
    • source: \{ event: string, id: string \} | null
    • idempotencyKey: string
    • createdAt: 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 balance in users/{uid}/credits/current is a cached value derived from the sum of ledger entries within the current period.
  • All credit operations are logged to creditLedger for audit trail and dispute resolution.
  • Ledger entries are immutable and use idempotencyKey to prevent duplicate charges.

Subscription Management​

  • Subscription doc may include pendingPlan for next-period plan changes.
  • Status transitions: trialing β†’ active β†’ past_due or canceled
  • Subscription enforcement checks both status and currentPeriodEnd > now

Multi-Account Architecture​

  • All platform integrations support multiple accounts per platform via accounts/{accountId} subcollection.
  • defaultAccountId is used when API requests don't specify an account.
  • Post submissions require explicit accountId when multiple accounts exist on a platform.
  • targetId format: {platform}:{accountId} for unique identification.

Composite Indexes (Multi-Source Reposting)​

  • users/{uid}/automations_workflows on source.targetId for "find workflows for this source account"
  • users/{uid}/automations_workflows on repostTargets.targetId for auditing/visualizing destinations
  • Collection group index on users/{uid}/organicPosts/{postId}/workflows for monitoring workflow execution states
  • Collection group index on integrations.can_be_source + enabled_for_repost for repost source detection
  • Collection group index on posts.platform + accountId + createdAt for user post history queries
  • Collection group index on organicPosts.source_platform + source_account_id + created_at_source for organic post polling
  • Collection group index on accounts.platform for platform-wide account queries

Observability Indexes​

  • 16 composite indexes on logs collection 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

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​

  • logs collection: Requires isStaff == true or role == 'admin' | 'support'
  • Used for debugging, monitoring, and support operations

Related frontend: see Frontend

Frontend​

  • UI reads users/{uid}/integrations/* to render connected profiles in PlatformPicker.
  • UI reads users/{uid}/subscription/current to gate features.

Related backend: see Backend