Skip to main content

Summary — RFC: Migrate Chat Divisions to USMAN Teams (Phase 1, Q3)

Digest of rfc-migrate-division-to-usman-team.md (backend RFC, status RFC, target 2026-Q3, last updated 2026-07-12). Owner: Communication Squad. Latest rfc-reviewer score: 9.5 / Agentic-Ready / PROCEED (rfc-migrate-division-to-usman-team-review-report.md). This summary is a reading aid; the draft is the source of truth.


TL;DR

Qontak One introduces a unified Team identity managed in Launchpad (Bifrost squad). This RFC covers the chat backend work (hub_core / hub_service / hub_worker) to make every Chat Division reference a Launchpad Team, migrate all existing divisions, and move member/supervisor management to Launchpad (chat becomes read-only for membership) — without changing any routing/SLA/broadcast/reporting behavior. The trick: keep divisions and add a team_id reference (rather than replacing divisions with teams), so division_id stays the internal key and every downstream consumer keeps working with zero query rewrites.

Blast radius is small by design: all migration and sync is async (Sidekiq + Karafka), gated per-org by a flag; rollback is a flag flip with all data preserved.


Problem & Goal

  • Today: Chat groups agents, scopes SLA, filters reports, and segments broadcasts using a local Division entity, with membership managed in chat.
  • Goal: Every Chat Division links to a Launchpad Team; membership is owned by Launchpad and mirrored into chat; chat exposes read-only member management. Preserve division_id as the routing key so nothing downstream changes.
  • Success criteria: 100% of active CIDs mapped (team_id NOT NULL); 0% routing regression; idempotent/resumable migration; member-sync p95 ≤ 30 s; read-only enforced in Team mode; dormant (channel-less) divisions behave correctly; every division change recaches Redis.

Out of scope: team hierarchy logic (columns added, no behavior), historical SLA/report backfill (chat keeps division_id), bot/channel routing changes, CRM team migration, and all frontend work (separate FE RFC — OQ-9).


What gets built

Schema (hub_core) — additive, nullable, safe for non-Team orgs

  • divisions: +team_id, +parent_team_id, +parent_id (all uuid NULL) + index (organization_id, team_id) + inline ES put_mapping. (No parent_id index — data-only.)
  • organizations.settings (existing jsonb): +use_qontak_one_team (per-org Team-mode gate), +team_migration_status (pending/processing/completed/failed) via store_accessor.

APIs

EndpointChangePurpose
GET /core/v2/divisionsextendedlist + per-division team_id, has_channels, channel_count; members read-only in Team mode (incl. Admin/Owner with role)
PUT /core/v2/divisions/:idnewrename division + background name-sync to Launchpad
PUT /core/v2/divisions/channelsnewassign channels to a team_id; auto-creates the division if none exists; empty set keeps it (dormant)
POST /internal/v1/teams/migratenewper-CID migration trigger (static API key X-Chat-Api-Key)
GET /core/v1/organizations/settingsextendedexposes team_migration_status to the FE (banner) — the canonical org-flag surface
POST/PUT /core/v1/divisions/usersextendedreject member/supervisor writes in Team mode (422 managed_by_team)

Consumer, worker, and outbound clients

  • KafkaConsumers::Teams::TeamEvents (hub_core, wired in hub_worker/karafka.rb bifrost_launchpad group) — consumes bifrost.team.events.v1; maps team_id, syncs the full roster, renames, and cascades deletes. event_id dedup via Redis SET NX (7-day TTL).
  • Divisions::TeamMigrationWorker (Sidekiq) — per-CID, idempotent/resumable (skips team_id-set divisions), calls Launchpad bulk-create.
  • Launchpad HTTP clients (BulkCreateTeam, GetTeam, UpdateTeamName) — reuse the existing Basic-auth /private/teams client pattern; wrapped in a new launchpad_circuit_breaker (Circuitbox, thresholds 5 / 50% / 60s / 60s, 30 s timeout).

Key design decisions (D1–D16)

#DecisionSummary
D1Link storageteam_id column on divisions (not a join table) — every consumer already keys on division_id. Overrides PRD §17 (sign-off pending, OQ-5).
D2Per-org gatingunified_app capability AND use_qontak_one_team flag, via qontak_one_team_enabled?
D3Member syncMirror the full roster (agents + supervisors) into user_divisions; ≥1-supervisor invariant bypassed in Team mode
D4team_id → division mappingEvent-driven via TEAM_MIGRATED, matched on app_identifier_id = division_id
D5IdempotencySkip team_id IS NOT NULL + Launchpad source-ref dedup; status in organizations.settings
D6VersioningNew endpoints are V2; V1 only gains a read-only guard
D7Channel-less divisionKeep the division (don't delete) when all channels are unassigned
D8Outbound authReuse existing Basic-auth /private/teams Launchpad client (no new host/token)
D9DeleteHard cascade on TEAM_DELETED via existing Divisions::Delete
D10ESIndex team_id (keyword) for parity/debug
D11Inbound authDedicated static key LAUNCHPAD_MIGRATION_API_KEY (not shared CRM_API_KEY)
D12ResilienceNew launchpad_circuit_breaker (dedicated circuit, quantified thresholds)
D13Dormant divisionA channel-less division is skipped by routing/assignment/notification but still listed (has_channels:false); validate_division (list access-control) left unchanged
D14Always-recacheEvery division-mutation path (consumer + V2 assign-channel) recaches Redis by delegating to existing repositories; UserDivision::Edit gains managed_by_team: to bypass the supervisor check
D15Admin/Owner displaySynced into user_divisions for read-only display but never routable — the assignment engine already role-gates candidates (role IN ('agent','member')), so no engine change; one notification pluck gets a role filter
D16Status exposureExpose team_migration_status via GET /core/v1/organizations/settings (per-org flag surface), not the per-division divisions list

Key flows

  1. Migration (per CID): Launchpad triggers POST /internal/v1/teams/migrate → chat enqueues TeamMigrationWorker → worker bulk-creates teams in Launchpad → Launchpad emits TEAM_MIGRATED → chat consumer sets divisions.team_id and, once all divisions mapped, sets team_migration_status = completed.
  2. Ongoing sync: TEAM_UPDATED → sync roster (agents + supervisors + Admin/Owner display) + name; TEAM_DELETED → cascade delete. Every write recaches Redis (D14).
  3. Assign channel → auto-create division: admin assigns a channel to a team_id with no division → chat fetches the team from Launchpad and creates the division (seeding user_divisions), attaches channels. Empty channel set keeps the division as dormant (D13).

Rollout, rollback & observability

  • Rollout: deploy migration (inert nullable columns) → hub_core → hub_worker (queue + topic) → hub_service (V2 + internal endpoints, behind flag) → per-org enablement (migrate → verify → flip use_qontak_one_team on). Stages: Internal QA → Internal Account → Shadow Write → Cutover → Assignment-Menu Lock → GA.
  • Feature flag: use_qontak_one_team (default OFF, per-org). Kill-switch = flip OFF → legacy V1 path resumes; team_id + Launchpad teams preserved. Mid-session safe.
  • Rollback trigger: assignment-failure-rate increase > 0% vs baseline unresolved in 24 h, or migration failure > 5%/batch.
  • Observability: 6 metrics via Services::Datadog::CaptureCustomMetric (incl. team_member_sync_latency histogram for the ≤30 s SLA); alerts on migration/member-sync failure and latency; consumer-lag + error-rate dashboard (co-owned Bifrost + Comm).

Status & open items

Execution plan: 15 ordered chunks (migration → model/entity → flag helper → Launchpad clients

  • breaker → consumer → worker → karafka → V1 guard → V2 interactors → V2 endpoints → internal endpoint → observability → dormant-skip/admin-non-routability → settings exposure → full regression). Ready for agent execution: yes.

Open questions (none block implementation):

#ItemTypeDeadline
OQ-5Confirm team_id column over PRD §17 join tableStakeholder sign-offBefore AGREED
OQ-2Agree LAUNCHPAD_MIGRATION_API_KEY value + Heimdall trigger with BifrostCoordinationBefore pilot
OQ-8QA regression across dormant-division surfacesVerificationBefore Assignment-Menu Lock
OQ-14Audit remaining GetUsersByDivision consumers for admin non-routabilityBounded verificationBefore Assignment-Menu Lock
OQ-9FE RFC (banner, read-only fields, tooltips) consuming the additive BE fieldsDependencyBefore GA

Two nice-to-have polish items from the latest review: pin the Entities::Settings dry-struct attribute semantics for team_migration_status (chunk 14), and expand the abbreviated Division response schema in §2.4.


Dependencies

  • Bifrost (Launchpad): bifrost.team.events.v1 topic + payloads, and POST /private/teams/bulk (migrate mode) — both shipped/as-built. Chat consumes/calls; does not modify.
  • Platform/Billing: unified_app package flag semantics per org.
  • Product/GTM: which CIDs/plans are in scope for cutover (OQ-10).

Related: PRD 51227460370 · Architecture & Flow Decision 51249054233 · Launchpad Teams RFC 51213828683 · Division docs 1–8. Revision history in the draft's §6 Comment logs (v1 → rev7). This digest reflects rev7 (2026-07-12).