Multi-tenancy
How AlphaSwarm turns a Microsoft Entra ID tid claim into an
Organization → Team → User → Membership chain — and what
keeps a B2B guest from another tenant from leaking into the wrong
org.
Identity flow
Schema
| Table | Purpose |
|---|---|
organizations | Top of the AlphaSwarm tenancy tree (multi-tenant) |
teams | Subgroup within an org |
workspaces | Visibility-scoped container of projects + labs |
projects / labs | The user-facing buckets where strategies / RAG corpora live |
users | Authenticated identities (one row per Entra oid) |
memberships | Polymorphic (user, scope_kind, scope_id, role) grants |
entra_tenant_links | Multi-tenant Entra tid → AlphaSwarm organization_id index (NEW) |
broker_credentials | Multi-tenant BYOK credentials (envelope-encrypted) |
Schema migrations:
0017_tenancy_foundation.py— originaldefault-*seed.0050_terraform_iac_plus_entra.py— addsentra_tenant_links+ the Terraform tables.0051_seed_wiley_tech.py— seeds the canonical "Wiley Tech" org + user "Julian" + transfers every legacydefault-*-owned row.
Runtime Context & Scoped Operations
AlphaSwarm uses alphaswarm.tenancy.runtime_context.get_runtime_context() as the single source of truth for the active tenant during a request or task. This context carries the tenant_id, org_id, and project_id.
Scoped Data Access
Database models utilize ProjectScopedMixin (and OrganizationScopedMixin) to enforce tenant isolation at the ORM level. The alphaswarm.persistence.db.get_session helper automatically applies tenant filters to queries when a runtime_context is present.
Multi-Tenant Trading (BYOK)
The Account Management System (AMS) uses the BrokerCredentialStore to manage per-tenant brokerage credentials. Credentials are envelope-encrypted using a master key (or AWS KMS/Vault) and stored in the broker_credentials table. The build_brokerage factory resolves these credentials based on the active tenant_id.
EntraTenantLink lifecycle
Statuses (see :data:ENTRA_TENANT_STATUSES):
| Status | Behaviour |
|---|---|
pending | Created by first-login of an unknown tid. User signs in but lands on an "awaiting org admin" surface (no Memberships granted). |
active | New logins from the tenant auto-provision into the linked org + workspaces. |
suspended | Sign-ins from the tenant still resolve, but no new Memberships are granted. |
revoked | Sign-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 for creating (and
optionally immediately activating) a link. Links auto-created as
pending by a first sign-in from an unrecognized tenant are instead
promoted via the frontend
EntraTenantLinkWizard,
a 3-step wizard that calls POST /tenancy/entra-links/{link_id}/promote.
The alphaswarm_client SPA's AuthProvider has standardised on
Microsoft Entra ID via MSAL (ADR-013): the "Microsoft" login button
(loginWithMicrosoft) is a direct MSAL redirect, not an Auth0-federated
one — alphaswarm_client no longer ships an Auth0 SDK/provider at all.
The backend's Auth0Provider and the Terraform-provisioned
connection=azure-ad-myorg Enterprise Connection (see
scim-provisioning.md) still exist for non-SPA /
legacy consumers, but the current customer-facing SPA does not route
Microsoft sign-in through them, so _apply_entra_tenant_link (which is
a no-op unless claims_provider(claims) == "msal_entra", i.e. the
token's iss is login.microsoftonline.com) is not reached via that
path today.
MsalEntraProvider remains registered through IdentityProviderMeta
and activates when ALPHASWARM_AUTH_PROVIDER=msal_entra — this is the
path the SPA's direct-MSAL login and provision_user_from_claims
actually exercise. Super-admin promotion of the resulting pending
EntraTenantLink rows is 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), keyed by
the raw Entra app-role string with the target AlphaSwarm role as the value.
The seeded Wiley Tech link (alembic/versions/0051_seed_wiley_tech.py::_seed_entra_tenant_link)
actually seeds an empty role_mapping ({}), so its logins fall through
to the _fold_entra_role_to_tenancy defaults above rather than a custom
per-link mapping; an admin can populate an override like this via the
onboarding wizard or the POST /tenancy/entra-links /
POST /tenancy/entra-links/{link_id}/promote APIs:
{
"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:
- 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). - EntraTenantLinkWizard (3 steps) — promotes an already-
pendingEntraTenantLink(auto-created on first sign-in from an unrecognized Entra tenant): (1) pick the pending link, (2) choose the org + bootstrap default role, (3) confirm promotion (POST /tenancy/entra-links/{link_id}/promote). It does not take a manually-typedtid— creating a link ahead of time with a primary domain / allowed email domains / full app-role mapping is done via thePOST /tenancy/entra-linksAPI instead (see msal-entra-setup.md). - 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 ondata.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:
| Slug | Type | Notes |
|---|---|---|
default | Organization | Legacy 0017 seed (preserved for FK chains) |
wiley-tech | Organization | New canonical seed (Wiley Tech) |
core | Team | Default team under wiley-tech |
main | Workspace | Default workspace under wiley-tech |
main | Project | Default project under main workspace |
main | Lab | Default lab under main workspace |
julian@wiley.tech | User | Owner 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.