Skip to main content

Multi-Account Integrations

Status​

Accepted β€” implementation staged behind feature flags per rollout plan below.

Context​

The current integrations layer allows exactly one connected account per provider (users/{uid}/integrations/{provider}). Every part of the systemβ€”auth flows, orchestrators, publish functions, repost automation, templates, UIβ€”depends on that assumption. Customers are requesting support for multiple Instagram/TikTok/YouTube accounts under a single Soku workspace, so we need a ground-up redesign that:

  • Stores credentials, profile metadata, and feature toggles per account instead of per provider.
  • Lets publish/repost orchestrators target specific accounts while maintaining idempotency and observability.
  • Preserves backwards compatibility for existing API clients and the two current users until the frontend migrates.
  • Keeps auditability and rate-limit protection at least as strong as today.

Decision​

  1. Introduce account-scoped documents. Each provider keeps a lightweight summary doc at users/{uid}/integrations/{provider} and nests per-account docs under accounts/{accountId}. Account IDs are provider-native identifiers (e.g., IG user ID, TikTok open_id, YouTube channel ID).
  2. Encode targets as (platform, accountId) tuples everywhere. request.post.content.platform becomes an array of objects { platform: 'instagram', accountId: '178414...' }. All orchestrators, Cloud Tasks, and run documents use a composite key targetId = ${platform}:${accountId}.
  3. Backwards compatibility shim. When a payload supplies only platform, the orchestrator resolves it to the provider’s defaultAccountId until legacy clients are upgraded.
  4. Observation & logging. Trace/span metadata, postSubmissions.targets, and runs/{targetId} store both platform and account ID so we can debug multi-account fan-out.
  5. Security rules & admin APIs. Firestore rules gate account docs by request.auth.uid == uid. Admin helpers (token refresh, pollers, organic ingestion) iterate accounts under each provider.
  6. Orderly rollout. Ship schema + API changes behind a MULTI_ACCOUNT_INTEGRATIONS feature flag. Migrate the two active users manually, validate end-to-end, then enable new UI/flows and finally drop the shim.

Data Model​

users/{uid}/integrations/{provider}
defaultAccountId: string | null
connectedCount: number
features:
repostEnabled: boolean (provider-wide toggle)
templatesEnabled: boolean
createdAt: timestamp
updatedAt: timestamp

users/{uid}/integrations/{provider}/accounts/{accountId}
platform: 'instagram' | 'tiktok' | ...
displayName: string
username: string
avatarUrl: string
accessToken: string
refreshToken?: string
tokenType?: string
expiresAt?: number | timestamp
enabled_for_repost: boolean
can_be_source: boolean
enable_text_repurpose_target: boolean
enable_image_repurpose_target: boolean
profile: { platformId, accountType, ... }
metrics: { lastPolledAt, pollCursor }
createdAt: timestamp
updatedAt: timestamp

Indexes:

  • integrations/{provider}/accounts composite on enabled_for_repost, can_be_source for repost/automation lookups.
  • Collection group integrations remains for provider-level queries, plus a new collection group integrationAccounts for cross-provider sweeps.

Orchestrators & Tasks​

  • extractTargets() accepts both legacy strings and { platform, accountId }. It returns tuples normalized to lowercase platform keys and canonical account IDs.
  • postSubmissions/{id} stores:
    • targets: [{ platform, accountId, targetId }]
    • runs/{targetId} documents instead of runs/{platform}.
  • Cloud Task names follow publishInstagram-${postSubmissionId}-${accountId} to remain deterministic per target.
  • Repost orchestrator queries accounts subcollections, filters by enabled_for_repost, and builds per-account targets. Idempotency includes targetId so the same organic post can fan out to multiple accounts safely.

API & Compatibility​

  • REST /v1/posts and internal /publishPost accept the new shape immediately. Clients sending legacy arrays continue to work until we enforce the new contract.
  • Webhook payloads and admin APIs emit account IDs when referencing integrations.
  • recordPublishedPost now stores { platform, accountId } and caches account profile details on each post.

Security & Compliance​

  • Firestore rules restrict both provider docs and nested account docs to the owning user.
  • Token refresh routines operate per account doc; sensitive fields stay in Firestore/Secrets Manager as today.

Rollout Plan​

  1. Land schema, rules, and API changes guarded by feature flag.
  2. Ship orchestrator/provider updates plus automated tests that cover multi-account fan-out.
  3. Update frontend (settings, automations, publish UX) to surface multiple accounts and send account-aware payloads.
  4. Manually migrate the two current users, verify publish + repost flows.
  5. Enable the feature flag for everyone, monitor queues/rate limits, then remove the compatibility shim.

Alternatives Considered​

  • Separate top-level collection integrations_v2 β€” rejected to avoid duplicating auth flows and security rules.
  • Multiple Firestore docs per provider (provider+counter IDs) β€” harder to query and reason about than an explicit accounts subcollection, and complicates security rules.

This ADR locks in the data model and execution strategy so implementation can proceed without re-litigating the fundamentals.