Spec: F03 — Teams
Status: shipped foundation · agreed channel expansion · Audited: 2026-08-10
1. Problem & CEO Job
See clearly. A 30-person company is not a flat list — the CEO thinks in units: “sales is fine, ops is on fire.” Teams are the grouping dimension for pulses, tasks, risks, and commitments so the brief and filters can answer “which part of my company?” Teams are labels with an owner, not an org chart — Lumi must never become an HR tool.
2. User Stories & Acceptance Criteria
- As a CEO, I want to create a team and name a manager, so that signals roll up to someone accountable.
- Given a team with a manager, when AI extracts a risk from that team’s pulse, then the risk carries the
teamFK and is filterable by it.
- Given a team with a manager, when AI extracts a risk from that team’s pulse, then the risk carries the
- As an admin, I want to add/remove team members without ceremony.
- Given a member added to a team, when they submit a pulse, then the pulse is attributed to that team automatically.
- Given duplicate team name in the same org, when creating, then validation error (unique per org). Fixed E-r31.
- Given a member is added to or removed from a team, when the mutation commits, then their access is added to or removed from every linked team channel in the same transaction.
- As a CEO, I want team names to appear wherever work appears, so that I never have to remember who belongs where.
- As a team member, I want my team’s conversation ready without joining another thing, so
that work and discussion stay together.
- Given an authorized user creates a team, when the server returns
201, then the creator is a team admin and one private default channel already exists with every current team member joined. - Given I can view a team, when I open its mobile detail, then I see its linked channels and can open a channel thread in one tap.
- Given an authorized user creates another team channel, when the server responds, then all current team members are joined and future roster changes continue to synchronize.
- Given a linked channel, when any user calls generic join/leave or directly edits channel membership, then the server rejects it; the team roster remains the only membership truth.
- Given an authorized user creates a team, when the server returns
3. Scope Boundaries
- In: team CRUD, manager designation, team membership (admin/member), one required linked channel, additional linked channels, inherited channel membership, standard-team bootstrap keys, and team as a filter dimension across work/pulses/operations.
- Out: nested teams, channel-specific members or roles, cross-team channels, team-level permission matrices, and cross-team dashboards (the Brief is the rollup).
4. Data Model
Team—organizationFK,name(unique per org),manager→OrganizationMember, nullable immutablebootstrap_keyfor F02’s eight standard teams.TeamMember— join table toOrganizationMember,role: admin/member, unique per (team, member).- F29
Channel.teamlinks zero or more private channels to one team withon_delete=CASCADE.is_parent_default=Truemarks the protected first linked channel without overloading F29’s organization-wideis_defaultflag.ChannelMember.is_activeis the materialized access index synchronized from activeTeamMemberorganizations, including while the channel is archived; rows and unread watermarks survive suspension but are not independently editable. One team may have at most 50 linked channels.
⚠️ Dual representation: OrganizationMember.team FK (primary team, used by invites and pulse attribution) and TeamMember M2M. See §11 row 3.
5. API Contract
| Method | Path | Min role | Notes |
|---|---|---|---|
| CRUD | /api/teams/ | roles.manage (write) / member (read) | POST atomically creates creator membership + default linked channel |
| GET/POST | /api/teams/<id>/members/ | roles.manage for write | List/add team members; add synchronizes linked channels |
| PATCH/DELETE | /api/teams/<id>/members/<id>/ | roles.manage | Change role / remove; remove synchronizes linked channels |
| GET/POST | /api/teams/<id>/channels/ | team viewer / roles.manage for POST | List linked channels or create an additional inherited private channel |
Every queryset explicitly uses request.organization_id; cross-tenant → 404. Foreign-key inputs
must belong to that same organization. Roster responses and channel ACLs include active
organization members only; suspension retains the underlying TeamMember and inactive
ChannelMember rows, reactivation restores their access and unread watermarks, and explicit team
removal deletes the relationship.
6. UI / UX
/settings/teams(TeamsPage.tsx) — list of teams with manager + member count; expanded detail manages members and lists/creates linked channels. A linked row opens the canonical F29 thread. Team deletion confirmation names the linked-channel count and permanent message-history loss.- Android/iOS Workspace → Teams — shared list and detail surfaces. Owners/admins can add an
organization member as
memberoradmin, render those roles through localized product copy, and remove through an explicit confirmation; ordinary members receive a read-only roster. The detail roster, not the older list snapshot, owns the count after mutations. - After a populated mobile Teams refresh fails, the last committed count and team rows remain visible behind one compact warning with explicit Retry. Repeated failure keeps that snapshot; successful retry clears the warning. An initial empty failure remains blocking.
- After a populated mobile Team Detail or roster refresh fails, the team facts, roster, and authorized actions remain visible behind one compact warning with explicit Retry. Team and roster recovery remain independent; an initial empty team failure remains blocking.
- The mobile team detail places Channels directly below the roster. Each row shows channel name
and unread count and opens the existing F29 thread. One compact New channel action is shown
only when the server returns
can_create_channel=true; clients never infer it from role text. - States: existing organizations may be empty (“Tạo team đầu tiên” with one-field create); a newly bootstrapped organization starts with eight standard rows but still exposes Create team for custom teams. Loading uses skeletons; member pickers come from org members only.
- No-training rule: creating a team asks for exactly one thing — a name. Creator membership and the first channel are automatic; manager and additional members come after.
- i18n:
teams.
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | CRUD + unique name per org | backend | tests/test_api.py::TestTeam |
| 2 | Membership add/remove/role, cross-tenant blocked | backend | tests/test_multitenancy.py::TestTeamMembers |
| 3 | Teams page renders, create flow | e2e | describe("Teams") |
| 4 | Mobile add/admin-role, cancel/confirm remove, re-entry persistence, member denial | shared JVM + iOS Maestro policy/lifecycle | TeamMemberWriteScreenModelTest, TeamsListSourceTest, TeamDetailScreenLogicTest, flows 108/109 |
| 5 | Team creation is atomic with creator membership and default linked channel | backend | backend/tests/teams/test_team_channels.py |
| 6 | Add/remove member synchronizes all linked channels; generic join/leave is denied | backend | backend/tests/teams/test_team_channels.py |
| 7 | Team detail lists linked channels and server capabilities; foreign-tenant inputs return 404 | backend | backend/tests/teams/test_team_channels.py |
| 8 | Mobile team detail loads channels, handles partial failure, and routes to the selected thread | shared JVM + Maestro | TeamChannelsScreenModelTest, P60 mobile flow |
| 9 | Web team detail/list handles seeded teams, linked channels, safe deletion consequence, and no optimistic success | frontend + Chromium | P60 W03/W05–W07 |
| 10 | Populated mobile Teams refresh retains count and team rows through repeated failure, then explicit Retry recovers | shared JVM + Android Maestro policy/runtime | TeamsScreenModelTest, flow 160, run-android-teams-recovery-proof.sh |
| 11 | Populated mobile Team Detail and roster retain exact context through independent repeated failures, then explicit Retry recovers each surface | shared JVM + Android Maestro policy/runtime | TeamDetailScreenModelTest, flow 160, run-android-teams-recovery-proof.sh |
8. Current Behavior
Shipped. Teams are tenant-scoped, membership assignment is enforced through the organizations/work graph, and web plus shared native surfaces support ordinary CRUD and roster management. Mobile role choices use the backend’s literal member/admin vocabulary, destructive roster changes are visible and confirmation-gated, and a refreshed detail roster owns its displayed count. Duplicate team name fix (E-r31, 2026-06-13) added serializer-level validation returning 400 instead of a DB IntegrityError 500.
The P60 backend boundary creates each team, creator membership, and protected default linked channel atomically. Team detail lists linked channels and server-derived capabilities; roster add/remove operations synchronize every active or archived linked channel through explicit services. Cross-organization manager and member inputs fail closed, generic channel join/leave cannot mutate inherited membership, and suspension retains inactive membership rows so reactivation restores access and unread watermarks. The 50-channel limit returns a stable 409 conflict. Shared Android/iOS team detail now lists and creates linked channels and routes the exact channel id into the canonical Messages thread; E01–E08 passed on the approved emulator and simulator. Web, macOS, named-device, native screen-reader-service, and store proof remain later P60 slices.
The shared Teams list now treats a refresh failure as degraded state rather than a destructive replacement when a committed directory exists. The page count and row details read from the same current-or-stale snapshot, while a compact warning exposes one explicit Retry. Guarded Android flow 160 retained two server-backed teams through two bounded list failures and recovered on the forwarded retry; iOS and named-device evidence remain open in the Contract Graph.
Shared Team Detail applies the same current-or-stale boundary independently to team facts and its roster. A populated failure keeps the exact team, member row, and authorized actions visible behind one compact Retry warning; successful retry removes only that warning. Guarded Android flow 160 proved exact team-detail and roster outages sequentially on the same server fixture; iOS and named-device evidence remain open in the Contract Graph.
9. Error States
| # | Error | Response | Notes |
|---|---|---|---|
| ES1 | Missing required name | 400 {"name": ["This field is required."]} | Covered by CRUD tests |
| ES2 | Duplicate team name in same org | 400 {"name": ["A team with this name already exists in your organization."]} | Fixed E-r31; test_create_team_duplicate_name_rejected |
| ES3 | Name > 255 chars | 400 {"name": ["Ensure this field has no more than 255 characters."]} | DRF auto-validator; test_create_team_name_too_long_rejected |
| ES4 | Cross-tenant team access | 404 | test_team_tenant_isolation |
| ES5 | Viewer creates/updates/deletes team | 403 | Requires IsOrgOwnerOrAdmin; test_viewer_cannot_add_team_member |
| ES6 | Duplicate team member added | 400 {"organization_member": "This member is already on the team."} | Fixed; test_add_duplicate_team_member_rejected |
| ES7 | Cross-org manager_id on PATCH | 200 — silently succeeds; cross-tenant FK accepted | test_patch_team_cross_org_manager_accepted; no serializer validation |
| ES8 | Cross-org organization_member_id on member add | 200 — silently succeeds | test_add_team_member_cross_org_accepted; no serializer validation |
| ES9 | Team detail with no members | 200 {members: [], admin_count: 0} | Valid behavior |
| ES10 | Frontend load failure | toast.error(t('error_load')) | No E2E test |
| ES11 | Frontend create failure (duplicate name) | toast.error(t('error_create')) + structured 400 | Fixed E-r31 |
| ES12 | Deleting team used by active projects/tasks and linked channels | Explicit irreversible confirmation names the linked-channel count; projects/tasks get team_id=NULL, while linked channels/messages cascade so no frozen-ACL orphan remains | Audit log + P60 C09 |
| ES13 | Default linked channel archive/delete | 400 default_channel_required | The team must retain one ready conversation |
| ES14 | Join/leave linked channel directly | 400 membership_inherited | Change the team roster instead |
| ES15 | Channel list fails on mobile while team/roster loaded | Keep team and roster visible; inline retry in Channels section | Never replace the whole detail with an error |
| ES16 | Create 51st linked channel | 409 linked_channel_limit | Existing channels and memberships unchanged |
| ES17 | Populated mobile Teams refresh fails | Keep last committed count and team rows visible; compact Retry above the directory | Initial empty failure remains blocking |
| ES18 | Populated mobile Team Detail refresh fails | Keep team facts, roster, and authorized actions visible; compact Retry above the detail | Initial empty team failure remains blocking |
| ES19 | Populated mobile team roster refresh fails | Keep team facts and last committed roster visible; compact Retry above the roster | Team-detail recovery remains independent |
10. Diagrams & Docs Touched
backend/DIAGRAMS.md ER diagram.
11. Implementation Audit (2026-06-13)
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | Team CRUD, unique name per org | ✅ match | TeamSerializer.validate_name(); test_create_team_duplicate_name_rejected |
| 2 | Membership add/remove/role change; cross-tenant 404 | ✅ match | TeamMemberViewSet; test_team_tenant_isolation |
| 3 | Dual representation: OrganizationMember.team FK (attribution) vs TeamMember M2M (display) — no code enforces consistency | ⚠️ spec drift / debt | test_team_member_add_does_not_sync_org_member_primary_team; by design: FK is authoritative for attribution, M2M for display — documented in §4 |
| 4 | Last admin removed from team — no guard prevents it | ⚠️ gap | test_remove_last_admin_team_member_succeeds; a team can be left with zero admins |
| 5 | Team deletion cascade: Project.team and Task.team SET_NULL | ✅ match | Django on_delete=SET_NULL; test_org_member_primary_team_set_null_on_team_delete |
| 6 | Cross-org manager_id on PATCH is rejected | ✅ match | P60 same-organization serializer validation; test_patch_team_cross_org_manager_rejected |
| 7 | Cross-org organization_member on member add returns 404 | ✅ match | P60 tenant-scoped roster service; test_add_team_member_cross_org_rejected |
| 8 | Duplicate team name → 400 via serializer (not IntegrityError 500) | ✅ match | Fixed E-r31 2026-06-13; test_create_team_duplicate_name_rejected |
| 9 | Authenticated creator is atomically added as team admin with the default linked channel | ✅ match | create_team_with_channel; test_create_team_atomically_creates_admin_and_default_channel |
| 10 | Pulse attribution loss on team delete: OrganizationMember.team SET_NULL | ✅ match | test_org_member_primary_team_set_null_on_team_delete; expected behavior |
| 11 | Visual QC: team management uses localized role badges and icon-led CTAs instead of duplicated + labels / raw English role keys | ✅ match | 2026-06-18 visual QC: TeamsPage.tsx strips stale + prefixes, adds icon-led team CTAs, and localizes owner/admin/manager/member badges through roles + teams namespaces; frontend/screenshots/13-teams/01-teams-list.png |
| 12 | Visual QC: member rows and team cards surface structure before controls | ✅ match | 2026-06-18 visual QC: /settings/teams now opens with a short page summary + count chips, team cards use calm manager/member metadata pills, and member rows group org role plus team memberships beside the person instead of scattering status/action elements across the row. |
| 13 | Native roster management uses valid roles, explicit confirmed removal, refreshed count, and a member capability gate | ✅ match | TeamDetailScreenModel, TeamDetailScreen, LumiDirectoryRow; model/source/logic tests; iOS flows 108/109 and isolated lifecycle runner. Named Android and signed-iPhone proof remain Contract Graph gap F03.GAP002. |
| 14 | Populated native Teams refresh preserves the last committed directory and exposes compact explicit recovery | ✅ match | TeamsScreenModel.dataOrStale(), TeamsScreen, TeamsScreenModelTest, policy test, and guarded Android flow 160. iOS and named-device proof remain F03.GAP004. |
| 15 | Populated native Team Detail preserves team facts and roster through independent refresh failures | ✅ match | TeamDetailScreenModel.dataOrStale(), TeamDetailScreen, TeamDetailScreenModelTest, policy test, and guarded Android flow 160. iOS and named-device proof remain F03.GAP005. |
Summary: 14/15 match; last-admin removal remains an explicit gap.
Edge Cases Catalog (formerly §10, 2026-06-12; E1–E14): Key items integrated above. Remaining items: E1 empty org → empty list (not explicitly tested); E2 dual representation no-sync (row 3); E3 last admin removal (row 4); E4 team deletion CASCADE (row 5); E7 duplicate name (row 8); E8 name max_length (ES3); E9 blank name (ES1); E11 role transitions no validation; E12 pulse attribution (row 10); E13 concurrent creation (fixed — validate_name guard); E14 pagination (DRF default). P60 closed E5, E6, and E10 with tenant-scoped services and atomic creation. Full catalog in git history (2026-06-12 changelog entry).
12. Open Questions
- None blocking. Resist any request for team hierarchies.
13. Bug / Change Log
2026-08-10 — Preserve populated mobile Team Detail and roster through refresh failure
Type: P1 reliability and recovery fix (shared KMP + managed Android proof)
Summary:
- Kept the last committed team facts, roster, and authorized actions visible through repeated team-detail or roster refresh failure.
- Added one compact warning with explicit Retry for each independently recoverable surface; an initial empty team failure retains the existing blocking recovery state.
- Added shared model proof, source/runtime policy proof, and guarded flow 160 with exact APK, sequential bounded outages, team/member custody, retained frames and trace, fixture cleanup, and runtime shutdown.
Proof boundary:
- The retained Android result is current-source Pixel 7 API 35 emulator mechanics evidence. It does not prove iOS behavior, named-device fidelity, screen-reader-service behavior, store binaries, deployed releases, or production behavior.
2026-08-10 — Preserve populated mobile Teams directory through refresh failure
Type: P1 reliability and recovery fix (shared KMP + managed Android proof)
Summary:
- Kept the last committed team count and row details visible when a populated refresh fails, including across repeated failure.
- Added one compact warning with explicit Retry; successful retry clears the warning, while an initial empty failure retains the existing blocking recovery state.
- Added shared model proof, source/runtime policy proof, and guarded flow 160 with exact APK, bounded outage, server custody, retained frames and trace, fixture cleanup, and runtime shutdown.
Proof boundary:
- The retained Android result is current-source Pixel 7 API 35 emulator mechanics evidence. It does not prove iOS behavior, named-device fidelity, screen-reader-service behavior, store binaries, deployed releases, or production behavior.
2026-07-30 — Specify container-owned team conversations
Type: Behavior Change
Summary:
- Every new team receives a private default channel; additional linked channels are allowed.
- The team roster is the only membership truth and synchronizes every linked channel transactionally.
- Team detail lists and opens related channels on Android and iOS.
- F02’s eight standard teams use stable bootstrap keys without changing user-created teams.
Reason:
- Requiring users to recreate the same group in Teams and Messages is setup tax and creates stale access when the two rosters drift.
Impact:
- F03 owns team lifecycle; F29 owns channel custody; ADR-050 defines the boundary.
Tests:
- Planned in
docs/test-plan/project-team-channels.mdcases T01–T11, C09/C14–C18, W03–W07, and M11–M20.
2026-07-20 — Mobile team membership role, interaction, and roster-truth repair
Type: P1 bug fix (shared KMP + iOS deterministic proof)
Summary:
- Replaced the invalid mobile
managerTeamMember choice with the backend-supportedadminrole, reject unknown role keys in the shared screen model, and localize persisted roster badges instead of title-casing raw API keys. - Replaced long-press-only removal with a visible, localized action and explicit cancel/confirm dialog; ordinary members see neither add nor remove controls.
- Published
LumiDirectoryRowactivation on its merged accessibility node so iOS VoiceOver/XCTest can activate the same full-row callback as a physical pointer. - Made the loaded/refreshed detail roster authoritative for the member-count chip, preventing an accepted add/remove from leaving stale summary copy.
- Added flows 108/109 and
run-ios-team-membership-proof.sh: one reused simulator, exact current app, disposable tenant, owner/member lanes, database postconditions, and exact cleanup.
Proof boundary:
- The iOS 26.4 simulator lifecycle is current-source UI mechanics evidence. Android runtime and a signed named-iPhone execution remain open in F03.GAP002. Maestro screenshots are local ignored artifacts and are not committed to git history.
2026-07-14 — Mobile: restore team manager/member signal in list rows (WRK-10)
Type: UX fix (shared KMP list anatomy)
Summary:
- Team rows now include the API-provided manager name alongside the description when available, so the list carries the ownership signal promised by F03 instead of discarding it before the detail screen.
- Member counts now use the existing English/Vietnamese singular/plural resources;
the previous hard-coded
"members"badge could leak English in a Vietnamese UI. - Added stable
team_row_<id>semantics tags andTeamsListSourceTestfor device-proof targeting and regression protection.
Scope boundary:
- This improves source-level row anatomy. Populated multi-team named-device evidence and WRK-10 scoring remain PM-owned audit work.
Tests:
git diff --checkPYENV_VERSION=system python3 scripts/check_docs_truth.pybash scripts/check-platform-parity.sh
2026-07-13 — Contract Graph migration slice
Type: Contract and proof traceability
Summary:
- Added the canonical F03 CUE contract for tenant-scoped team CRUD and roster management, with stable proof IDs attached to the existing backend tests.
- Regenerated the Contract Graph reports. F03 is intentionally
partial: the audit’s cross-org manager/member foreign-key acceptance and last-admin behavior remain open, as do browser and named Android, iOS, and macOS runtime proofs.
Impact: No product behavior change. The contract makes 12 previously unowned team endpoints traceable and preserves F02 as the tenant-membership dependency.
Tests: The Contract Graph verifier executes the selected F03 pytest proof records; remaining security
and runtime gaps are explicit in contracts/features/F03.cue.
2026-06-21 — Edit team member role in-place
Type: Feature (CRUD completion)
Summary: TeamMemberViewSet (ModelViewSet) and teamMembersApi.update existed, but TeamsPage only allowed add/remove — a member’s team role (member/admin) couldn’t be changed without removing and re-adding. Added owner/admin-gated role Select per member row (handleUpdateTeamMemberRole → PATCH), with a read-only badge for non-admins. i18n keys added to en/vi (then propagated). testid team-member-role-{memberId}.
2026-06-15 — Frontend route namespaced
Frontend route namespaced: /teams -> /settings/teams (app-wide IA restructure; API paths unchanged).
2026-06-14 — Spec restructured to TEMPLATE.md canonical order (§1–§13)
Type: Documentation Change
Summary:
- §3 Competitive Positioning (Appendix) and §4 Success Metrics (Appendix) moved to §14/§15 appendix positions.
- §5 Scope → §3, §6 Data Model → §4, §7 API → §5, §8 UI → §6, §9 Test Plan → §7, §10 Current Behavior → §8, §11 Error States → §9, §12 Diagrams → §10, §13 Implementation Audit → §11, §14 OQ → §12, §15 BL → §13.
- Updated internal §-references: §13 row 3 → §11 row 3; EC note “formerly §12” → “formerly §10”; documented in §6 → §4.
Reason:
- Align with TEMPLATE.md canonical section order per SP fleet restructuring pass.
Impact:
- Spec authors and future implementation agents.
2026-06-13 — E-r31: add serializer-level duplicate team name validation + test
Type: Bug Fix
Summary:
- Added
TeamSerializer.validate_name()to catch duplicate names within the same org at the serializer level — returns 400 instead of unhandled 500 IntegrityError. - Tests:
test_create_team_duplicate_name_rejected(same name same org → 400; same name different org → 201).
2026-06-18 — Visual QC: clean up team CTAs and member-role labels
Type: UX Fix
Summary:
- The visual audit showed
/settings/teamsleaking two unfinished details: the main action rendered as+ + Tạo nhóm, and organization-role badges stayed raw lowercase English on a Vietnamese page. - Team cards also looked passive even though the page supports expansion to manage members.
- The surface now uses icon-led action buttons, strips stale
+prefixes from translated labels, localizes org/team role badges, and adds a chevron affordance on team cards so expandability is legible from the screenshot alone.
Verification:
frontend/screenshots/13-teams/01-teams-list.png
2026-06-18 — Visual QC: member hierarchy and calmer team metadata
Type: UX Fix
Summary:
- The screenshot audit showed the Teams page had the right information but weak hierarchy: role badges sat far from the member identity, and delete actions were louder than the team context.
- Added a short page summary with team/member counts, converted team-card metadata into pill-style labels, and moved organization role + team memberships into the member identity block.
- The destructive remove controls are now quieter outline buttons, which keeps the roster readable without hiding the action.
Verification:
frontend/screenshots/13-teams/01-teams-list.png
2026-06-12 — Add competitive positioning and success metrics to F03 spec
Type: Documentation Change
Summary:
- Added §3 Competitive Positioning and §4 Success Metrics; renumbered §3–§13 to §5–§15. (Superseded by 2026-06-14 restructuring which moves these to §14–§15 appendix.)
2026-06-12 — Add Edge Cases (E1–E14) and Error States (ES1–ES12) to F03 spec
Type: Documentation Change
14. Competitive Positioning (Appendix)
vs Jira (project-hierarchy → people-first)
Jira organizes by project hierarchy: teams are labels on issues within a project board. A CEO cannot see “is sales healthy?” without navigating into a board and reading ticket statuses. Lumi teams are people-first: the team is the grouping dimension for pulses, tasks, risks, and commitments. The CEO sees the org through who is doing what — not which project board has which tickets.
vs Asana (workspaces → role-aware SME context)
Asana splits organizations into workspaces per team but has no SME role awareness — a “Sales workspace” has no concept of a manager accountable for signals. Lumi teams carry explicit admin/member semantics and map directly to the org structure.
vs Monday.com (boards → CEO-visibility)
Monday.com surfaces team work through boards that require active setup. Team health is invisible unless someone builds a dashboard for it. Lumi surfaces team health automatically: pulse completion rates, risk concentration, commitment tracking — all visible in the Brief and Today without configuring a single board.
Vietnamese SME advantage
Lumi teams are labels with an owner, not an org chart. Creating a team asks for exactly one thing — a name — and is complete in under two minutes. No hierarchy configuration, no permission matrix setup, no onboarding wizard.
15. Success Metrics (Appendix)
| Metric | Target | Why it matters |
|---|---|---|
| Team creation time | <2 min from first visit | One field (name); manager and members added after — zero ceremony matches SME pace |
| Member invitation acceptance rate | >80% | Org members are already known; adding them to a team removes the friction of re-inviting |
| Cross-team visibility usage | >60% of CEOs use the team filter within first week | Team filter must be discoverable and useful without training |
| Permission change error rate | <1% of role-change operations | Role transitions must never accidentally demote the last admin |
| Pulse-team attribution accuracy | 100% of pulses attributed to correct team | OrganizationMember.team FK is authoritative; divergence with TeamMember M2M is documented debt |
| Zero-support team management | <1% of support tickets about team CRUD or membership | Operations must be self-explanatory |