RFC Review: Qontak One Team Migration — Phase 1: Legacy Team Migration
Companion review for
rfc-qontak-one-team-migration.md, produced by therfc-reviewerskill. 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_MIGRATEDcontract, is now signed off by both Chat and CRM (OQ-1/OQ-9 closed). Supersedes R2 (which superseded R1). Findings carry forward by stableREV-nid.
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: FEenhancement· BEnew-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
teamIdsand 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_MIGRATEDcontract (§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()'smember_idsrequirement and either fails or wrongly invents members. - REV-9: with
app_identifier_idoptional, the agent can't tell whether replay-safety is guaranteed.
Findings Ledger (carry-forward)
| ID | Severity | Finding (one line) | RFC location | Status | First seen | Resolved in | Evidence / fix |
|---|---|---|---|---|---|---|---|
REV-1 | major | (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-1 | resolved | R1 | R3 2026-07-01 | Was 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-2 | blocker | /sso_invite team field specified snake_case (team_ids) but live endpoint takes camelCase → teamIds | §2.4 invite row; §2.G; §2.A | fixed | R1 | RFC 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-3 | major | FE enforcement gating signal undecided (Option A menu-visibility vs B new flag) | §1.B, §5 OQ-4, §4.D ch7 | open | R1 | — | Pick Option A; state chunk-7 trigger expression |
REV-4 | major | General-team async retry under-specified (attempts/backoff/DLQ) | §2.F, §5 OQ-7 | open | R1 | — | Read queue pkg; pin retry params |
REV-5 | major | Invite team cardinality unresolved (single team_id vs team_ids[]) | §1.B, §5 OQ-2 | open | R1 | — | Confirm; shapes write loop |
REV-6 | minor | /sso_invite success response schema only named, not pinned | §2.4, §5 OQ-8 | open | R1 | — | Inline fields FE reads |
REV-7 | minor | FE non-functional thin (no perf budget / analytics events) | §3, §5 L-2 | open | R1 | — | State "no bundle delta" + name/n/a analytics |
REV-8 | minor | OTM-S03 out of launchpad repos; PRD still Must-Have; owner unconfirmed | §1 Out of Scope, §5 OQ-3 | open | R1 | — | Confirm legacy-CRM owner |
REV-9 | major | Idempotency 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-9 | fixed | R2 | RFC 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-10 | major | CreateTeam.Validate() requires member_ids but PRD §5.1 migrates structures only → memberless push fails validation | §2.4, §2.I, OTM-S05 | fixed | R2 | RFC 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 Element | RFC Section | Coverage |
|---|---|---|
| OTM-S01 mandatory team on invite | §1.A, §2.A, §2.4 | Full (casing wrong — REV-2; cardinality open — REV-5) |
| OTM-S02 auto-create General team | §2.3, §2.4, §2.F | Full (retry thin — REV-4) |
| OTM-S03 sidebar URL change | §1 Out of Scope, §5 OQ-3 | Partial — out of repo (REV-8) |
| OTM-S04 source identifiers | §2.3 DDL + CHECK | Full |
| OTM-S05 migrate legacy teams (push) | §2.4 (+event), §2.F.1, §4.D ch3 | Full 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.1 | Full |
PRD §12 team_migrated (now TEAM_MIGRATED) | §2.4 event contract, §3 | Full — 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_id→reference_id) after the v1.2/v1.3 edits. The only contradiction is the §2.4 invite casing (REV-2).
Scorecard (Full-Stack, 18 categories)
| # | Category | Source | R1 | R2 | Evidence-Based Rationale (Δ) |
|---|---|---|---|---|---|
| 1 | PRT — PRD Traceability | Merged | 9.0 | 9.0 | OTM-S05 reframed to push, still fully traced; PRD↔RFC event/field aligned |
| 2 | TDC — Technical Decisions | Merged | 7.0 | 7.5 | ↑ push-vs-pull, event, and field-naming decisions closed (§1.B); REV-10 interaction still unclosed |
| 3 | CNT — Contract Specificity (FE) | FE | 7.0 | 7.0 | unchanged (invite FE) |
| 4 | SCB — Scope Boundaries (FE) | FE | 8.5 | 8.5 | §2.I updated for push, still crisp |
| 5 | DEP — Dependencies (FE) | FE | 8.0 | 8.0 | deps clearer (trigger + consume) |
| 6 | NFS — Non-Functional (FE) | FE | 6.0 | 6.0 | unchanged (REV-7) |
| 7 | TPS — Test Plan | FE | 7.5 | 7.5 | migrate test now targets the bulk path |
| 8 | DMS — Data Model & Schema | BE | 8.5 | 8.5 | DDL solid; lifecycle table present |
| 9 | ACV — API Contract & Versioning | BE | 6.0 | 6.0 | new TEAM_MIGRATED contract + bulk extend are strong, but offset by REV-2 (invite casing) + REV-6 + REV-10 validation gap |
| 10 | DIC — Data Integrity & Consistency | BE | 8.0 | 7.5 | ↓ idempotency hole when app_identifier_id absent (REV-9) |
| 11 | FMC — Failure Mode Coverage | Merged | 8.0 | 8.0 | bulk per-item results + squad-side retry well modelled |
| 12 | CSS — Concurrency & Scaling | BE | 7.0 | 7.0 | collision map updated for re-push |
| 13 | SAS — Security & Authorization | BE | 7.5 | 7.5 | service-to-service auth for /teams/bulk now explicit (§3) |
| 14 | ROL — Rollout & Rollback | Merged | 8.0 | 8.0 | deploy order + compat matrix intact; step 5 now "trigger squads" |
| 15 | OBS — Observability | Merged | 7.0 | 7.5 | ↑ TEAM_MIGRATED Kafka signal specified (topic, payload) |
| 16 | SBC — Service Boundary & Coupling | BE | 7.5 | 8.0 | ↑ push boundaries + the event-mapping step (§2.F.1) are explicit |
| 17 | CPA — Pattern Alignment | Merged | 6.5 | 6.5 | reuse improved, but cross-layer naming still fails on invite (REV-2) |
| 18 | CDG — Compliance & Data Governance | BE | 7.0 | 7.0 | unchanged; 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)
| # | Decision | Status | Note |
|---|---|---|---|
| Push vs pull migration | Resolved (NEW) | §1.B — clean rationale; reuses existing bulk path | |
app/app_identifier_id → source_identifier/reference_id naming | Resolved (NEW) | §1.B — request↔storage mapping is coherent | |
Separate TEAM_MIGRATED event | Resolved (NEW) | §2.4 — full contract; same topic, distinct event_type | |
app_identifier_id optional vs required | Dangling (NEW) | drives REV-9; "optional" + partial index = no replay safety | |
Migrate-mode member_ids requirement | Not addressed (NEW) | REV-10 — existing validation requires members; non-goal says structures only | |
| Invite team field shape/casing | Partial | REV-2 (casing) + REV-5 (cardinality) unchanged | |
| FE enforcement gate | Dangling | REV-3 unchanged | |
| General-team retry | Partial | REV-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 / event | BE side | FE / consumer side | Match? | Gaps |
|---|---|---|---|---|
POST /sso_invite | snake_case team_ids (RFC) | FE sends camelCase teamIds | No | REV-2 (casing) — cap driver |
POST /teams/bulk (migrate) | accepts app_identifier_id, persists reference_id, requires member_ids | Chat/CRM push | Partial | REV-10 (member_ids required vs structure-only); REV-9 (optional id → no dedup) |
TEAM_MIGRATED event | {team_id, app, app_identifier_id, …} on TopicTeamEventsV1 | Chat/CRM consume to map | Yes (pending squad sign-off, OQ-9) | clean; new |
GET /teams/menu-visibility | {is_visible} | isTeamMenuVisible | Yes | should 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_MIGRATEDcontract is concrete and minimal (distinctevent_type, same topic/envelope, carries theapp_identifier_id↔team_idjoin key); a consumer squad can build its mapping table directly from it. - Coherent request↔storage vocabulary —
app/app_identifier_idon the wire,source_identifier/reference_idin 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_idsrequired 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
- §2.4 (REV-2) — verify
/sso_inviteJSON tags; specifyteamIds; remove the snake_case mapping instruction. Clears the cap. - §2.4 / OTM-S05 (REV-10) — relax
CreateTeam.Validate()to allow emptymember_idswhenis_migrate=true(or decide migration carries members). Unblocks the actual push. - §2.3/§2.4 (REV-9) — make
app_identifier_idrequired whenis_migrate=true(mirror the existingIsMigrate && App==""guard); update OQ-9. - §5 OQ-1/OQ-4 (REV-1/REV-3) — land the Chat/CRM trigger +
TEAM_MIGRATEDcontract sign-off, and pick the FE gating option.
Implementation Readiness Checklist (delta)
Unblocked
- Migration reuses existing async bulk path + Kafka (push model)
-
TEAM_MIGRATEDcontract 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_idfor 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
| # | Question | Category | Severity |
|---|---|---|---|
| 1 | /sso_invite field casing — confirm teamIds (REV-2) | ACV/CPA | Blocking |
| 2 | Allow empty member_ids for is_migrate=true? (REV-10) | ACV/DIC | Blocking |
| 3 | Make app_identifier_id required for migrate-mode? (REV-9) | DIC | Important |
| 4 | Chat/CRM trigger endpoint + TEAM_MIGRATED sign-off (REV-1, OQ-9) | DEP/OBS | Important |
| 5 | FE enforcement gate Option A vs B (REV-3) | TDC/ROL | Important |
| 6 | Invite cardinality single vs multi (REV-5) | TDC | Important |
Review History
| Cycle | Date | Reviewed RFC revision | Score | Verdict | Findings open → fixed | Notes |
|---|---|---|---|---|---|---|
R1 | 2026-06-30 | last_updated 2026-06-30 (pull model) | 6.5 | HOLD | 8 open (2B/3M/3m), 0 fixed | First review. Capped by REV-2 casing; blocked by REV-1 read-APIs. |
R2 | 2026-06-30 | last_updated 2026-06-30 (push model + TEAM_MIGRATED) | 6.5 | HOLD | 10 open (1B/6M/3m); REV-1 eased blocker→major; REV-9/REV-10 new | Push 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. |
R3 | 2026-07-01 | last_updated 2026-07-01 (3 fixes applied + contract signed) | 7.5 | PROCEED | REV-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. |