Features / supported
Organizations and teams
Purpose
Provide the tenancy model: global users hold explicit memberships in organizations and teams. The team is the tenancy boundary for all business data (INV-034); authorization always resolves an explicit scope before any grant is evaluated (INV-024/025).
User outcomes
- A founder creates an organization and immediately has a working team with owner and admin grants — atomically, never a partial organization (BC-US-140, INV-033).
- Admins add teams, staff them, rename them (stable UUID/slug identity), and archive teams that are no longer used.
- Organization-level commercial accounts project onto team-local billing customers without ever granting cross-team access (BC-US-142).
Actors and permissions
- Organization roles:
organization_owner,organization_admin,organization_member. - Team roles:
team_admin,billing_admin,finance_operator,auditor,integration_client. - Team roles never derive from organization roles;
platform_adminnever bypasses membership checks.
Domain terminology
- Scope (
BillingCore.Scope) — the resolved authorization context (principal + organization + team + role grants) threaded through every context call. Built only by the auth pipeline, never from request params. - Account — organization-scoped commercial identity, projected to
team-local customers via
account_team_customers. - Team settings version — immutable settings snapshot with canonical
SHA-256 hash;
teams.settings_versionpoints at the latest.
Workflows
Orgs.create_organization/2— one transaction inserts the organization, its first team (default name “Default”, a bootstrap convenience, never a semantic marker), settings version 1, and the creator’sorganization_owner+team_adminmemberships, with audit entries.- Membership management —
add_organization_member,change_organization_roles,remove_organization_member,add_team_member,change_team_roles,remove_team_member. Removed memberships keep their row (statusremoved). - Team lifecycle —
create_team(active organizations only),rename_team,archive_team,update_team_settings(locked, version-bumping snapshot). - Accounts —
create_account(external_id unique per organization),update_account,archive_account(idempotent),project_account_to_team(upsert on(account_id, team_id); refuses cross-organization teams). - Scope resolution —
resolve_scope(user, organization_id, team_id \\ nil).
State transitions
No formal state machine. Statuses: organizations/teams active/disabled
(disabled is called “archived” for teams), memberships active/removed,
accounts active/archived.
Business rules / invariants
- INV-033: organization creation atomically creates the first team; the
final active team of an active organization cannot be archived
(
{:error, :last_active_team}, race-safe via an organization row lock). - The last active
organization_ownercan be neither removed nor demoted ({:error, :last_owner}). - An organization membership cannot be removed while the user holds active
team memberships in that organization (
{:error, :active_team_memberships}). - Team membership requires an active organization membership
(
{:error, :not_organization_member}) — INV-035. resolve_scope/3requires: active user, existing active organization, ACTIVE organization membership, and (when a team is requested) a team that belongs to that organization, is active, and holds an ACTIVE team membership. Any failure is a uniform{:error, :unauthorized}— no detail leak (INV-024/025).- Possession of an ID never grants access; queries are constrained by
scope.team.id/scope.organization.id(SPEC §13.4).
GraphQL contract
Queries: viewer (own memberships and roles), organizations,
organization, team, teams, organizationMemberships /
teamMemberships (admin directories with member emails), and
organizationInvitations (BC-US-144). Mutations: createOrganization
(returns organization + first team), createTeam, renameTeam,
archiveTeam (organization scope — the owner need not be a member of the
archived team), inviteOrganizationMember / revokeOrganizationInvitation,
addTeamMember (requires an existing organization membership, INV-024),
changeTeamRoles, removeTeamMember, and changeOrganizationRoles
(last-owner protected). This is the full SPEC §14.5
organizations-and-membership row. See schema/billing_core.graphql.
CLI surface
Not yet implemented. revryn (BC-US-157) is planned; domain commands
are reachable via GraphQL.
MCP surface
Not yet implemented (BC-US-158 planned).
UI behavior
First-run and dashboard workspace creation (#create-workspace-form,
per-organization #new-team-form-…), the team members page
(/teams/:team_id/members: role changes, removal, invitations with the
one-time accept link), and the browser accept flow (/invitations/:token).
Accounting / ERP effects
None directly. Teams carry the invoicing legal identity (legal_name,
base_currency, time_zone, locale) used downstream by billing and ERP.
Async / failure / recovery behavior
All mutations are single transactions; a failing step rolls back the whole command (verified for organization creation). No durable operations.
Observability
Audit events (orgs.organization.created, orgs.team.*,
orgs.organization_membership.*, orgs.team_membership.*,
orgs.account.*) are recorded in the same transaction as each change.
Tests
test/workflows/organization_lifecycle_test.exs— creation atomicity, last-team/last-owner protection, membership ordering, team lifecycle.test/workflows/scope_resolution_test.exs— INV-024/025 matrix: role locality, cross-organization substitution, platform_admin non-bypass, disabled/suspended targets.test/graphql/memberships_test.exs— SPEC §14.5 contract breadth: directories, role changes, INV-024 team grants, last-owner and last-team protections, typed authorization problems.test/graphql/invitations_test.exs,test/workflows/invitation_test.exs,test/billing_core_web/live/members_live_test.exs— BC-US-144 invitation lifecycle across API, mail, and browser.e2e/features/multi_membership.spec.ts— INV-032: one identity with conflicting roles across three teams in two organizations.
Security / privacy
Scope resolution fails closed and uniformly (:unauthorized). Membership
history is retained (status removed), not deleted. Audit entries carry the
acting principal.
Limitations
- Team settings and account administration remain context functions without GraphQL mutations (accounts surface via the projection flow).
- Organization suspension/closure workflows beyond the
statusfield are not implemented;removeOrganizationMemberis intentionally absent from the public contract (SPEC §14.5 table) and remains a context command.