iterion

ADR-048 — Org → Teams two-level hierarchy

Status: Accepted (implemented — P0→P2) Date: 2026-06-27 (implemented 2026-06-28)

Context

Today iterion’s tenancy model is flat: a “team” IS the tenant — one identity.Team type (id / name / slug / role / quotas), no separate “org” concept. A user belongs to many teams and switches the active team (stored on the JWT as id.TeamID); the studio’s UserTeamChip already exposes a team switcher. Every tenant-scoped store keys on this single id: forge.Connection, boardmongo (board per team), secrets/OAuth, marketplace viewer, orgusage quotas, audit, pat, webhooks.

The flat model conflates two things operators want to separate:

The confusing left-nav chip (“SocialGouv” with SocialGouv/socialgouv on the team page) is a symptom: it’s really the tenant switcher, but there’s no level above it, so an org with several squads can’t model them without creating several unrelated top-level “teams” that don’t share SSO/billing/members.

Decision (proposed)

Introduce a two-level hierarchy: Org → Teams.

Key design decisions to lock

  1. Tenant key. Two options for the stores currently keyed by tenant=team_id:
    • (A) Keep team_id as the tenant key for resource stores (board, forge, secrets, runs); add org_id only where org-level (members, SSO, quotas, billing, marketplace). Smallest migration — existing team-scoped data is untouched; we only add an Org layer above + an org_id column on teams.
    • (B) Re-key everything on (org_id, team_id). Cleaner long-term, large migration of every collection + index. → Recommend (A): org is additive; resources stay team-keyed. Quotas/usage move to org-level (sum across its teams).
  2. Quotas/usage. Move orgusage + cost-cap + run-quota from per-team to per-org (the launch gate sums a team’s run against its org budget). Teams can optionally carry a sub-cap.

  3. SSO + members. auth/orgSSO + the member roster move to org scope; team membership becomes a grant within the org (you must be an org member to be added to its teams).

  4. JWT + active context. Add org_id alongside team_id; /api/auth/me returns the org tree (orgs → teams the user can see); two switchers (org, then team) — or one combined “Org / Team” picker.

  5. Naming/UX. Left-nav chip becomes an explicit Org picker (chevron + “Organisation”), with the Team picker nested or adjacent. Resolves the “c’est le bazar” confusion.

Migration (existing flat teams)

Each current Team becomes an Org with one default Team carrying the same id (so every team-keyed resource — board/forge/secrets/runs — keeps working unchanged under option A). The Org gets the team’s name/slug/quotas/SSO/members; the default Team inherits the resources. Idempotent backfill: for each team, create org_id=<new>, set team.org_id, move org-level fields up. Personal teams → personal orgs.

Phased rollout

Risks / open questions

Alternatives considered

As built (2026-06-28)

Implemented P0→P2 in one branch under a no-backward-compat mandate (young project — no compat shims). Key decisions as they actually landed:

Deferred follow-ups: precise org-level memory CAS (today the org quota is pushed onto each team bucket); current-month usage-doc rename in the migration; a Playwright cloud smoke (tsc/eslint/vite + Go suite are green).