Multi-tenancy
How organizations act as the tenant boundary - the organization model, lifecycle, memberships, and the active-organization context.
Multi-tenancy is the backbone of Africa OS. An organization is the tenant: the unit that owns data, the unit that applications get enabled for, and the boundary every permission is scoped to. This page explains the model in code.
The core model
Two separate notions of “user” exist, and it is important to keep them straight:
public.useris the identity row that Better Auth owns (login, sessions, accounts). Itsidis the identity id.platform.usersis the platform profile, keyed byidentity_id. Itsidis the platform user id that memberships, roles, and permissions reference.
The request runtime bridges the two: it authenticates the identity, then upserts the platform profile, and everything downstream works with the platform user id.
The organization lifecycle
Every organization moves through a state machine, defined in platform/organizations/src/lifecycle.ts:
The transitions table:
export const organizationTransitions = {
pending: ["reviewing", "suspended"],
reviewing: ["verified", "suspended"],
verified: ["suspended"],
suspended: ["reviewing"],
};
Verification is a manual administrative step performed in the admin console. Any transition not listed fails with IllegalOrganizationTransition. New organizations are created as pending, and the creator becomes an active member holding the owner role.
Memberships
A membership is the link between a user and an organization, and it has its own lifecycle (platform/organizations/src/membership.ts):
The transitions table:
export const membershipTransitions = {
invited: ["active", "removed"],
active: ["suspended", "removed"],
suspended: ["active", "removed"],
removed: [],
};
A removed member cannot come back through the state machine - they must be re-invited. MembershipService.invite looks the user up by email on public.user, refuses duplicate memberships with AlreadyMember, and inserts the membership as invited.
Creating an organization, transactionally
OrganizationService.create runs inside withTransaction, so the whole creation is atomic. Inside the transaction it:
- Slugifies the name, deduplicating until the slug is free (
name,name-2,name-3, …). - Inserts the organization with status
pending. - Inserts the creator as an
activemember. - Creates an
ownerrole scoped to the organization, wired to every registered permission. - Grants the owner role to the creator’s membership.
The result: the founder can do anything inside their organization until it builds its own access model.
const organization = yield* OrganizationService.create({
name: "Acme Retail",
ownerUserId: platformUserId,
});
Listing a user’s organizations
OrganizationService.listForUser(userId) returns every organization where the user has an active membership, ordered by creation. This is what the web app’s organization switcher shows, and it is the input to active-organization resolution.
The active organization
A user can belong to many organizations, but a request happens in one. The request runtime resolves the active organization (platform/request-context/src/runtime.ts):
- A
:organizationslug in the URL wins, but only if it resolves to a real organization that is one of the user’s active memberships (NotOrganizationMemberotherwise). - Otherwise, the
africaos.active_orgcookie wins if it names an active membership. - Otherwise, the first active membership.
The resolved organization is provided to programs as CurrentOrganization. A user with no memberships carries Option.none(), meaning no tenant context.
Tenancy rules that never change
- The tenant id always comes from the request runtime, never from client input. Server functions receive
CurrentOrganizationand use itsid; client-supplied organization ids are rejected or ignored. - Platform queries are scoped by the caller. The database layer provides
orgWhere/orgScopehelpers to keep queries tenant-scoped; see Database. - Application availability is an organization concern. Whether an org can use an application is decided by
organization_applications, checked byRegistryService.getForOrganization. - Permissions are resolved per organization. The same user may hold different capabilities in different organizations.
Next steps
- Application registry - how organizations enable and launch applications.
- Permissions - how memberships become capabilities.
- Organizations (platform) - the full service surface.