Skip to main content

RFC Review: Qontak One Team Migration — Phase 1: Legacy Team Migration

Companion review for rfc-qontak-one-team-migration.md, produced by the rfc-reviewer skill. Cycle R3 (2026-07-01) — a delta re-review confirming the 3 Bifrost-owned spec fixes (REV-2/9/10) landed and that REV-1, the cross-squad Chat/CRM trigger + TEAM_MIGRATED contract, is now signed off by both Chat and CRM (OQ-1/OQ-9 closed). Supersedes R2 (which superseded R1). Findings carry forward by stable REV-n id.

Executive Summary

  • Overall Score: 7.5/10 (R3 — the REV-2 cap is lifted; the last hard dependency REV-1 is resolved)
  • Rating: Strong
  • RFC Type: full-stack · Sub-Type: FE enhancement · BE new-feature
  • Assessment Confidence: High (both repos re-read across R1–R3; the 3 fixes verified in-spec, the contract sign-off confirmed with Chat + CRM)
  • Applied Caps/Gates: None binding. The R2 cap (REV-2 invite-field casing) is lifted — casing corrected to teamIds and verified tagless. No category below 5.0.
  • Implementation Readiness Verdict: PROCEED — all blockers cleared: the 3 Bifrost-owned spec fixes are applied (REV-2/9/10) and the cross-squad trigger/event contract is signed off (REV-1). The full initiative, including Task 5 (end-to-end trigger), is now agent-executable. Remaining open items (REV-3/4/5/6/7/8) are implementation-time decisions, not gates.
  • Report Path: bifrost/qontak-one-team/rfcs/rfc-qontak-one-team-migration-review.md
  • RFC Author: Grehasta (BE) / Syafrizal M. (FE) | Reviewed: 2026-07-01 (R3)

The push-model rework is a clear improvement: migration now reuses the existing POST /teams/bulk async path (is_migrate/app, Kafka-emitting), and the new TEAM_MIGRATED event is a well-specified, purpose-built back-mapping channel (§2.4 event contract). That moves R1's biggest blocker — "Chat/CRM read APIs don't exist" — into a smaller, well-scoped dependency (the trigger contract). But the re-review surfaced two new code-grounded gaps the push model exposes: the existing CreateTeam.Validate() requires member_ids (clashes with the structure-only non-goal, REV-10), and the idempotency index is partial on a now-optional app_identifier_id (replay can duplicate, REV-9). The one thing that still caps the score is unchanged from R1: the invite field casing (REV-2). Fix REV-2 (one line), REV-10 (relax validation for migrate-mode), and REV-9 (require app_identifier_id when is_migrate=true), and this clears to Strong.


Quick Verdict

Why this RFC can be implemented agentically (more of it now):

  • Migration is now an extension of existing, working code (bulk_create.go, createSingleTeam, events.go) — chunks 1, 2, 3 (Launchpad side), 4, 5 are specified with real paths + repo-sourced commands + verifiable criteria.
  • The TEAM_MIGRATED contract (§2.4) is concrete: event_type, aggregate_id=team_id, app, app_identifier_id, same topic/envelope as today — an agent can implement the publisher and a consumer team can implement the mapping.

Why it will still cause guessing or rework:

  • REV-2: §2.4 specifies the invite field snake_case (team_ids) against a camelCase endpoint — agent would re-create the dropped-teams bug.
  • REV-10: an agent pushing a structure-only team (no members) hits CreateTeam.Validate()'s member_ids requirement and either fails or wrongly invents members.
  • REV-9: with app_identifier_id optional, the agent can't tell whether replay-safety is guaranteed.

Findings Ledger (carry-forward)

IDSeverityFinding (one line)RFC locationStatusFirst seenResolved inEvidence / fix
REV-1major(Resolved R3.) Migration's source path: receive path POST /teams/bulk already exists; the Chat/CRM trigger endpoint contract + TEAM_MIGRATED event are now signed off by both squads§1 Deps, §2.4, §5 OQ-1resolvedR1R3 2026-07-01Was blocker (pull/read-APIs) → major (push model). Chat + CRM both signed off the trigger/event contract (OQ-1/OQ-9 closed) — Task 5 unblocked, full end-to-end migration now buildable
REV-2blocker/sso_invite team field specified snake_case (team_ids) but live endpoint takes camelCase → teamIds§2.4 invite row; §2.G; §2.AfixedR1RFC 2026-06-30 (post-R2)Verified ApiUserInviteParam tagless (sso_invite.go:23) + BindRequest case-insensitive json.Decode (util.go:39) ⇒ camelCase. Spec corrected to teamIds (TeamIds []string); mapping instruction removed. Cap lifted. Residual: FE store wiring = §4.D chunk 6 (code, not spec).
REV-3majorFE enforcement gating signal undecided (Option A menu-visibility vs B new flag)§1.B, §5 OQ-4, §4.D ch7openR1Pick Option A; state chunk-7 trigger expression
REV-4majorGeneral-team async retry under-specified (attempts/backoff/DLQ)§2.F, §5 OQ-7openR1Read queue pkg; pin retry params
REV-5majorInvite team cardinality unresolved (single team_id vs team_ids[])§1.B, §5 OQ-2openR1Confirm; shapes write loop
REV-6minor/sso_invite success response schema only named, not pinned§2.4, §5 OQ-8openR1Inline fields FE reads
REV-7minorFE non-functional thin (no perf budget / analytics events)§3, §5 L-2openR1State "no bundle delta" + name/n/a analytics
REV-8minorOTM-S03 out of launchpad repos; PRD still Must-Have; owner unconfirmed§1 Out of Scope, §5 OQ-3openR1Confirm legacy-CRM owner
REV-9majorIdempotency index partial on reference_id; app_identifier_id was optional ⇒ absent ⇒ NULL ⇒ no dedup ⇒ replay duplicates§2.3 DDL, §2.D, §2.4, §5 OQ-9fixedR2RFC 2026-06-30 (post-R2)Decided: app_identifier_id required when is_migrate=true (§1.B, §2.3 note, §4.D ch3) → migrated rows stay in-index, replay-safe. Residual: Chat/CRM topic sign-off (OQ-9).
REV-10majorCreateTeam.Validate() requires member_ids but PRD §5.1 migrates structures only → memberless push fails validation§2.4, §2.I, OTM-S05fixedR2RFC 2026-06-30 (post-R2)Decided: relax validation to allow empty member_ids when is_migrate=true (§1.B, §4.D ch3); native creates still require members.

Ledger summary (R3, 2026-07-01): 10 tracked — 4 resolved (REV-2, REV-9, REV-10 fixed in spec; REV-1 signed off by Chat + CRM), 6 open: 0 blocker, 0 hard dependency, 3 major (REV-3, REV-4, REV-5 — implementation-time decisions), 3 minor (REV-6, REV-7, REV-8). No cap binds and no external dependency remains. Formal R3 score: 7.5 (Strong) — PROCEED. The scorecard below is the as-reviewed R2 snapshot (pre-fix); the R3 delta is the four resolutions above, which lift the cap and the last dependency.


PRD → RFC Traceability Matrix

PRD ElementRFC SectionCoverage
OTM-S01 mandatory team on invite§1.A, §2.A, §2.4Full (casing wrong — REV-2; cardinality open — REV-5)
OTM-S02 auto-create General team§2.3, §2.4, §2.FFull (retry thin — REV-4)
OTM-S03 sidebar URL change§1 Out of Scope, §5 OQ-3Partial — out of repo (REV-8)
OTM-S04 source identifiers§2.3 DDL + CHECKFull
OTM-S05 migrate legacy teams (push)§2.4 (+event), §2.F.1, §4.D ch3Full on Launchpad side; trigger contract is the dependency (REV-1); member_ids gap (REV-10)
PRD §9 #1/#5 behaviors (push + trigger)§2.4, §2.F.1Full
PRD §12 team_migrated (now TEAM_MIGRATED)§2.4 event contract, §3Full — aligned across PRD v1.3 + RFC

Summary: 6 of 7 full, 1 partial (OTM-S03). Forward+reverse present. PRD↔RFC now consistent on the event (TEAM_MIGRATED) and field (app_identifier_idreference_id) after the v1.2/v1.3 edits. The only contradiction is the §2.4 invite casing (REV-2).


Scorecard (Full-Stack, 18 categories)

#CategorySourceR1R2Evidence-Based Rationale (Δ)
1PRT — PRD TraceabilityMerged9.09.0OTM-S05 reframed to push, still fully traced; PRD↔RFC event/field aligned
2TDC — Technical DecisionsMerged7.07.5↑ push-vs-pull, event, and field-naming decisions closed (§1.B); REV-10 interaction still unclosed
3CNT — Contract Specificity (FE)FE7.07.0unchanged (invite FE)
4SCB — Scope Boundaries (FE)FE8.58.5§2.I updated for push, still crisp
5DEP — Dependencies (FE)FE8.08.0deps clearer (trigger + consume)
6NFS — Non-Functional (FE)FE6.06.0unchanged (REV-7)
7TPS — Test PlanFE7.57.5migrate test now targets the bulk path
8DMS — Data Model & SchemaBE8.58.5DDL solid; lifecycle table present
9ACV — API Contract & VersioningBE6.06.0new TEAM_MIGRATED contract + bulk extend are strong, but offset by REV-2 (invite casing) + REV-6 + REV-10 validation gap
10DIC — Data Integrity & ConsistencyBE8.07.5↓ idempotency hole when app_identifier_id absent (REV-9)
11FMC — Failure Mode CoverageMerged8.08.0bulk per-item results + squad-side retry well modelled
12CSS — Concurrency & ScalingBE7.07.0collision map updated for re-push
13SAS — Security & AuthorizationBE7.57.5service-to-service auth for /teams/bulk now explicit (§3)
14ROL — Rollout & RollbackMerged8.08.0deploy order + compat matrix intact; step 5 now "trigger squads"
15OBS — ObservabilityMerged7.07.5TEAM_MIGRATED Kafka signal specified (topic, payload)
16SBC — Service Boundary & CouplingBE7.58.0↑ push boundaries + the event-mapping step (§2.F.1) are explicit
17CPA — Pattern AlignmentMerged6.56.5reuse improved, but cross-layer naming still fails on invite (REV-2)
18CDG — Compliance & Data GovernanceBE7.07.0unchanged; membership-not-migrated still reduces exposure (modulo REV-10)

Overall: 6.5 — the cross-layer mismatch cap (REV-2) binds. Without it the document would sit ~7.4 (Strong); the cap is the honest gate until the casing is corrected.


Decision Closure Assessment (delta)

#DecisionStatusNote
Push vs pull migrationResolved (NEW)§1.B — clean rationale; reuses existing bulk path
app/app_identifier_idsource_identifier/reference_id namingResolved (NEW)§1.B — request↔storage mapping is coherent
Separate TEAM_MIGRATED eventResolved (NEW)§2.4 — full contract; same topic, distinct event_type
app_identifier_id optional vs requiredDangling (NEW)drives REV-9; "optional" + partial index = no replay safety
Migrate-mode member_ids requirementNot addressed (NEW)REV-10 — existing validation requires members; non-goal says structures only
Invite team field shape/casingPartialREV-2 (casing) + REV-5 (cardinality) unchanged
FE enforcement gateDanglingREV-3 unchanged
General-team retryPartialREV-4 unchanged

Aggregate: 6 Resolved (3 net-new closed), 2 Partial, 2 Dangling, 1 not-addressed. The push refinement closed three meaningful decisions and opened two new ones (REV-9, REV-10).

New Decision detail — app_identifier_id optional (REV-9)

The DDL idempotency key is UNIQUE (company_id, source_identifier, reference_id) WHERE reference_id IS NOT NULL (§2.3). app_identifier_id (→ reference_id) is declared optional (§2.4, OQ-9). If a squad omits it, the row's reference_id is NULL, the partial index excludes it, and a replay creates a duplicate team — defeating OTM-S05/AC-3. The existing code already has the precedent to fix this: team_request.go:34 rejects IsMigrate && App=="". Mirror it: reject IsMigrate && app_identifier_id=="" (i.e. required for migrate-mode, optional otherwise). Resolves REV-9 and tightens OQ-9.

New Decision detail — migrate-mode member_ids (REV-10)

CreateTeam.Validate() (team_request.go:20) returns an error when len(MemberIDs)==0; CreateTeamItem.Validate() chains it (:89). The PRD migrates team structures, not memberships (§5.1), so Chat/CRM would push teams with empty member_ids → every migrate item fails validation. The RFC must either (a) relax validation to allow empty member_ids when is_migrate=true, or (b) explicitly decide migration carries members (contradicting the non-goal). This is unaddressed in the current draft and would block the push path in practice.


Cross-Layer Contract Verification

Endpoint / eventBE sideFE / consumer sideMatch?Gaps
POST /sso_invitesnake_case team_ids (RFC)FE sends camelCase teamIdsNoREV-2 (casing) — cap driver
POST /teams/bulk (migrate)accepts app_identifier_id, persists reference_id, requires member_idsChat/CRM pushPartialREV-10 (member_ids required vs structure-only); REV-9 (optional id → no dedup)
TEAM_MIGRATED event{team_id, app, app_identifier_id, …} on TopicTeamEventsV1Chat/CRM consume to mapYes (pending squad sign-off, OQ-9)clean; new
GET /teams/menu-visibility{is_visible}isTeamMenuVisibleYesshould encode migration flag (REV-3)

Mismatches: 1 hard (REV-2) → caps at 6.5; 1 partial (bulk: REV-9/REV-10).


End-to-End Data Flow (migration, push)

Launchpad trigger client → Chat/CRM (HTTPS trigger)
→ squad maps its OWN active Divisions/Teams (active filter owned by squad)
→ POST /teams/bulk { is_migrate, app, app_identifier_id, name, member_ids?, company_sso_id }
⚠ member_ids currently REQUIRED (REV-10) — memberless structure push fails today
→ EnqueueBulkCreateTeam (202 + upload job) → worker → ProcessBulkCreateTeam → createSingleTeam
→ INSERT teams (name prefix, source_identifier=app, reference_id=app_identifier_id)
⚠ idempotent only if reference_id present (REV-9)
→ emit TEAM_MIGRATED { team_id, app, app_identifier_id } on TopicTeamEventsV1
→ Chat/CRM consume TEAM_MIGRATED → map their division/team → centralized team_id

Traceable end-to-end from the RFC alone; the two ⚠ are the new findings.


Mermaid validity

11 / 11 blocks parse (RFC 10 + PRD 1) — re-validated with @mermaid-js/mermaid-cli mmdc after the push + event edits (exit 0, no parse errors). No diagram findings.


Strengths

  • Maximal reuse of existing code — the push model maps onto bulk_create.go / createSingleTeam / events.go, which already do async creation, app-prefixing, and Kafka emission. The RFC correctly identifies the small delta (persist source cols, idempotency, TEAM_MIGRATED).
  • Purpose-built mapping event — §2.4's TEAM_MIGRATED contract is concrete and minimal (distinct event_type, same topic/envelope, carries the app_identifier_idteam_id join key); a consumer squad can build its mapping table directly from it.
  • Coherent request↔storage vocabularyapp/app_identifier_id on the wire, source_identifier/reference_id in storage, documented as a deliberate mapping (§1.B) rather than accidental drift.

Biggest Gaps

  • REV-2 (blocker): invite field casing — unchanged from R1, still caps the score.
  • REV-10 (major, new): member_ids required by validation contradicts the structure-only migration non-goal — would block the push path in practice.
  • REV-9 (major, new): optional app_identifier_id + partial unique index = no replay safety; breaks the idempotency AC for items without the id.

Priority Actions

  1. §2.4 (REV-2) — verify /sso_invite JSON tags; specify teamIds; remove the snake_case mapping instruction. Clears the cap.
  2. §2.4 / OTM-S05 (REV-10) — relax CreateTeam.Validate() to allow empty member_ids when is_migrate=true (or decide migration carries members). Unblocks the actual push.
  3. §2.3/§2.4 (REV-9) — make app_identifier_id required when is_migrate=true (mirror the existing IsMigrate && App=="" guard); update OQ-9.
  4. §5 OQ-1/OQ-4 (REV-1/REV-3) — land the Chat/CRM trigger + TEAM_MIGRATED contract sign-off, and pick the FE gating option.

Implementation Readiness Checklist (delta)

Unblocked

  • Migration reuses existing async bulk path + Kafka (push model)
  • TEAM_MIGRATED contract specified
  • DDL + per-status lifecycle; deploy order + rollback; mermaid 11/11
  • PRD↔RFC consistent on event + field naming

Blocked (must fix first)

  • REV-2 — invite field casing → teamIds
  • REV-10 — allow memberless migrate-mode teams (validation)
  • REV-9 — require app_identifier_id for migrate-mode (idempotency)
  • REV-1/REV-3 — Chat/CRM trigger+event contract; FE gating decision

Verdict: Fix REV-2 (one line) + REV-9 + REV-10 (small, code-local) and the Launchpad side is agent-ready; the cross-squad trigger/event sign-off is the remaining external dependency.


Open Questions

#QuestionCategorySeverity
1/sso_invite field casing — confirm teamIds (REV-2)ACV/CPABlocking
2Allow empty member_ids for is_migrate=true? (REV-10)ACV/DICBlocking
3Make app_identifier_id required for migrate-mode? (REV-9)DICImportant
4Chat/CRM trigger endpoint + TEAM_MIGRATED sign-off (REV-1, OQ-9)DEP/OBSImportant
5FE enforcement gate Option A vs B (REV-3)TDC/ROLImportant
6Invite cardinality single vs multi (REV-5)TDCImportant

Review History

CycleDateReviewed RFC revisionScoreVerdictFindings open → fixedNotes
R12026-06-30last_updated 2026-06-30 (pull model)6.5HOLD8 open (2B/3M/3m), 0 fixedFirst review. Capped by REV-2 casing; blocked by REV-1 read-APIs.
R22026-06-30last_updated 2026-06-30 (push model + TEAM_MIGRATED)6.5HOLD10 open (1B/6M/3m); REV-1 eased blocker→major; REV-9/REV-10 newPush rework improved reuse + closed 3 decisions; cap still REV-2. New gaps: member_ids-required (REV-10), optional-id idempotency (REV-9). Raw quality ↑ (~7.4) but cap binds.
R32026-07-01last_updated 2026-07-01 (3 fixes applied + contract signed)7.5PROCEEDREV-2/9/10 fixed; REV-1 resolved; 6 open (0B/3M/3m)Delta re-review. The 3 Bifrost-owned spec fixes verified in-spec (REV-2 casing→teamIds tagless; REV-9 require app_identifier_id for migrate; REV-10 relax member_ids), and Chat + CRM both signed off the trigger/TEAM_MIGRATED contract (OQ-1/OQ-9 closed → REV-1 resolved). Cap lifted, last dependency cleared. Full initiative incl. Task 5 now agent-executable. Remaining open items are implementation-time decisions, not gates.