Skip to main content

Multi-tenancy

How AlphaSwarm turns a Microsoft Entra ID tid claim into an OrganizationTeamUserMembership chain — and what keeps a B2B guest from another tenant from leaking into the wrong org.

Identity flow

Schema

TablePurpose
organizationsTop of the AlphaSwarm tenancy tree (multi-tenant)
teamsSubgroup within an org
workspacesVisibility-scoped container of projects + labs
projects / labsThe user-facing buckets where strategies / RAG corpora live
usersAuthenticated identities (one row per Entra oid)
membershipsPolymorphic (user, scope_kind, scope_id, role) grants
entra_tenant_linksMulti-tenant Entra tid → AlphaSwarm organization_id index (NEW)

Schema migrations:

  • 0017_tenancy_foundation.py — original default-* seed.
  • 0050_terraform_iac_plus_entra.py — adds entra_tenant_links + the Terraform tables.
  • 0051_seed_wiley_tech.py — seeds the canonical "Wiley Tech" org + user "Julian" + transfers every legacy default-*-owned row.

Statuses (see :data:ENTRA_TENANT_STATUSES):

StatusBehaviour
pendingCreated by first-login of an unknown tid. User signs in but lands on an "awaiting org admin" surface (no Memberships granted).
activeNew logins from the tenant auto-provision into the linked org + workspaces.
suspendedSign-ins from the tenant still resolve, but no new Memberships are granted.
revokedSign-ins from the tenant are blocked at provision time.

AGENTS rule 44: organization provisioning from Entra ID claims goes through EntraTenantLink. Don't auto-create org rows from raw tid claims. The data.tenancy.link_org_to_entra_tenant MCP tool (REST: POST /tenancy/entra-links) is the only sanctioned ingress. The frontend EntraTenantLinkWizard drives this flow with a 5-step wizard.

On the Auth0-federated path, the Microsoft button on the SPA login screen uses the Auth0 Enterprise Connection connection=azure-ad-myorg, which federates users to their home Entra tenant. The Entra tid claim returned through Auth0 is forwarded into the AlphaSwarm access-token claim set by the Auth0 Action, and provision_user_from_claims runs _apply_entra_tenant_link exactly as it does in the direct-MSAL path.

For regulated deployments that bypass Auth0 and hit Entra directly, MsalEntraProvider remains registered through IdentityProviderMeta and activates when ALPHASWARM_AUTH_PROVIDER=msal_entra. Both authentication paths converge on the same backend EntraTenantLink lookup chain, and super-admin promotion remains managed in alphaswarm_client/src/components/onboarding/EntraTenantLinkWizard.tsx.

App role mapping

Entra ships app roles in a top-level roles claim array (e.g. ["alphaswarm.admin", "alphaswarm.terraform.operator"]). The provisioning logic maps them onto the AlphaSwarm role lattice (viewer < editor < admin < owner):

# alphaswarm/auth/user.py::_apply_entra_tenant_link
# Multi-word roles fold to the tail token:
# alphaswarm.terraform.operator -> "operator" -> editor
# alphaswarm.terraform.approver -> "approver" -> admin

Per-link overrides live in EntraTenantLink.role_mapping (JSON). Example for the seeded Wiley Tech link:

{
"alphaswarm.admin": "owner",
"alphaswarm.editor": "editor",
"alphaswarm.viewer": "viewer",
"alphaswarm.terraform.operator": "editor",
"alphaswarm.terraform.approver": "admin"
}

Onboarding wizards (frontend)

/admin/onboarding hosts three wizards behind tabs:

  1. OrgCreateWizard (4 steps) — name / billing / default structure / review. Seeds the canonical Core team + Main workspace + Main project + Main lab (from configs/tenants/tenant_default_template.yaml).
  2. EntraTenantLinkWizard (5 steps) — choose org / Entra tid + primary domain / allowed email domains / app-role mapping / activate.
  3. UserInviteWizard (3 steps) — email + display name / scope + role / review + send (Entra B2B invitation when MSAL is configured).

Tenant template files

configs/tenants/ hosts three YAMLs:

  • tenant_default_template.yaml — default org structure created on data.tenancy.create_organization.
  • roles_default_template.yaml — canonical app-role → AlphaSwarm-role mapping.
  • user_invite_template.yaml — Entra B2B invite email body + custom claims payload.

Seeded state

After running alembic upgrade head against a fresh DB:

SlugTypeNotes
defaultOrganizationLegacy 0017 seed (preserved for FK chains)
wiley-techOrganizationNew canonical seed (Wiley Tech)
coreTeamDefault team under wiley-tech
mainWorkspaceDefault workspace under wiley-tech
mainProjectDefault project under main workspace
mainLabDefault lab under main workspace
julian@wiley.techUserOwner on every Wiley Tech scope

Every legacy *_runs / bots / agent_runs_v2 / analysis_runs / ... row that previously pointed at default-org / default-user is re-stamped to point at wiley-tech / julian@wiley.tech (see _restamp_legacy_rows in alembic/versions/0051_seed_wiley_tech.py). The legacy default-* rows stay in place so any orphan FK still resolves.