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β
- Introduce account-scoped documents. Each provider keeps a lightweight summary doc at
users/{uid}/integrations/{provider}and nests per-account docs underaccounts/{accountId}. Account IDs are provider-native identifiers (e.g., IG user ID, TikTok open_id, YouTube channel ID). - Encode targets as
(platform, accountId)tuples everywhere.request.post.content.platformbecomes an array of objects{ platform: 'instagram', accountId: '178414...' }. All orchestrators, Cloud Tasks, and run documents use a composite keytargetId = ${platform}:${accountId}. - Backwards compatibility shim. When a payload supplies only
platform, the orchestrator resolves it to the providerβsdefaultAccountIduntil legacy clients are upgraded. - Observation & logging. Trace/span metadata,
postSubmissions.targets, andruns/{targetId}store both platform and account ID so we can debug multi-account fan-out. - 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. - Orderly rollout. Ship schema + API changes behind a
MULTI_ACCOUNT_INTEGRATIONSfeature 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}/accountscomposite onenabled_for_repost,can_be_sourcefor repost/automation lookups.- Collection group
integrationsremains for provider-level queries, plus a new collection groupintegrationAccountsfor 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 ofruns/{platform}.
- Cloud Task names follow
publishInstagram-${postSubmissionId}-${accountId}to remain deterministic per target. - Repost orchestrator queries
accountssubcollections, filters byenabled_for_repost, and builds per-account targets. Idempotency includestargetIdso the same organic post can fan out to multiple accounts safely.
API & Compatibilityβ
- REST
/v1/postsand internal/publishPostaccept 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.
recordPublishedPostnow 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β
- Land schema, rules, and API changes guarded by feature flag.
- Ship orchestrator/provider updates plus automated tests that cover multi-account fan-out.
- Update frontend (settings, automations, publish UX) to surface multiple accounts and send account-aware payloads.
- Manually migrate the two current users, verify publish + repost flows.
- 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
accountssubcollection, and complicates security rules.
This ADR locks in the data model and execution strategy so implementation can proceed without re-litigating the fundamentals.