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. Latestrfc-reviewerscore: 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_idas 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(alluuid NULL) + index(organization_id, team_id)+ inline ESput_mapping. (Noparent_idindex — data-only.)organizations.settings(existing jsonb): +use_qontak_one_team(per-org Team-mode gate), +team_migration_status(pending/processing/completed/failed) viastore_accessor.
APIs
| Endpoint | Change | Purpose |
|---|---|---|
GET /core/v2/divisions | extended | list + per-division team_id, has_channels, channel_count; members read-only in Team mode (incl. Admin/Owner with role) |
PUT /core/v2/divisions/:id | new | rename division + background name-sync to Launchpad |
PUT /core/v2/divisions/channels | new | assign channels to a team_id; auto-creates the division if none exists; empty set keeps it (dormant) |
POST /internal/v1/teams/migrate | new | per-CID migration trigger (static API key X-Chat-Api-Key) |
GET /core/v1/organizations/settings | extended | exposes team_migration_status to the FE (banner) — the canonical org-flag surface |
POST/PUT /core/v1/divisions/users | extended | reject member/supervisor writes in Team mode (422 managed_by_team) |
Consumer, worker, and outbound clients
KafkaConsumers::Teams::TeamEvents(hub_core, wired inhub_worker/karafka.rbbifrost_launchpadgroup) — consumesbifrost.team.events.v1; mapsteam_id, syncs the full roster, renames, and cascades deletes.event_iddedup via RedisSET NX(7-day TTL).Divisions::TeamMigrationWorker(Sidekiq) — per-CID, idempotent/resumable (skipsteam_id-set divisions), calls Launchpad bulk-create.- Launchpad HTTP clients (
BulkCreateTeam,GetTeam,UpdateTeamName) — reuse the existing Basic-auth/private/teamsclient pattern; wrapped in a newlaunchpad_circuit_breaker(Circuitbox, thresholds 5 / 50% / 60s / 60s, 30 s timeout).
Key design decisions (D1–D16)
| # | Decision | Summary |
|---|---|---|
| D1 | Link storage | team_id column on divisions (not a join table) — every consumer already keys on division_id. Overrides PRD §17 (sign-off pending, OQ-5). |
| D2 | Per-org gating | unified_app capability AND use_qontak_one_team flag, via qontak_one_team_enabled? |
| D3 | Member sync | Mirror the full roster (agents + supervisors) into user_divisions; ≥1-supervisor invariant bypassed in Team mode |
| D4 | team_id → division mapping | Event-driven via TEAM_MIGRATED, matched on app_identifier_id = division_id |
| D5 | Idempotency | Skip team_id IS NOT NULL + Launchpad source-ref dedup; status in organizations.settings |
| D6 | Versioning | New endpoints are V2; V1 only gains a read-only guard |
| D7 | Channel-less division | Keep the division (don't delete) when all channels are unassigned |
| D8 | Outbound auth | Reuse existing Basic-auth /private/teams Launchpad client (no new host/token) |
| D9 | Delete | Hard cascade on TEAM_DELETED via existing Divisions::Delete |
| D10 | ES | Index team_id (keyword) for parity/debug |
| D11 | Inbound auth | Dedicated static key LAUNCHPAD_MIGRATION_API_KEY (not shared CRM_API_KEY) |
| D12 | Resilience | New launchpad_circuit_breaker (dedicated circuit, quantified thresholds) |
| D13 | Dormant division | A channel-less division is skipped by routing/assignment/notification but still listed (has_channels:false); validate_division (list access-control) left unchanged |
| D14 | Always-recache | Every 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 |
| D15 | Admin/Owner display | Synced 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 |
| D16 | Status exposure | Expose team_migration_status via GET /core/v1/organizations/settings (per-org flag surface), not the per-division divisions list |
Key flows
- Migration (per CID): Launchpad triggers
POST /internal/v1/teams/migrate→ chat enqueuesTeamMigrationWorker→ worker bulk-creates teams in Launchpad → Launchpad emitsTEAM_MIGRATED→ chat consumer setsdivisions.team_idand, once all divisions mapped, setsteam_migration_status = completed. - Ongoing sync:
TEAM_UPDATED→ sync roster (agents + supervisors + Admin/Owner display) + name;TEAM_DELETED→ cascade delete. Every write recaches Redis (D14). - Assign channel → auto-create division: admin assigns a channel to a
team_idwith no division → chat fetches the team from Launchpad and creates the division (seedinguser_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_teamon). 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_latencyhistogram 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):
| # | Item | Type | Deadline |
|---|---|---|---|
| OQ-5 | Confirm team_id column over PRD §17 join table | Stakeholder sign-off | Before AGREED |
| OQ-2 | Agree LAUNCHPAD_MIGRATION_API_KEY value + Heimdall trigger with Bifrost | Coordination | Before pilot |
| OQ-8 | QA regression across dormant-division surfaces | Verification | Before Assignment-Menu Lock |
| OQ-14 | Audit remaining GetUsersByDivision consumers for admin non-routability | Bounded verification | Before Assignment-Menu Lock |
| OQ-9 | FE RFC (banner, read-only fields, tooltips) consuming the additive BE fields | Dependency | Before 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.v1topic + payloads, andPOST /private/teams/bulk(migrate mode) — both shipped/as-built. Chat consumes/calls; does not modify. - Platform/Billing:
unified_apppackage 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).