Organizations
The organizations package - OrganizationService, MembershipService, RegistryService, schemas, and the errors they raise.
@africaos/organizations is the tenant package. It owns the organization model, memberships, and the application registry. It is one of only four locked platform packages, and its services appear in every request runtime.
The architecture page Multi-tenancy explains the model; this page documents the code.
The services
The package exposes three Effect services, plus a context tag:
| Service | Owns |
|---|---|
OrganizationService |
The organization itself: create, lookup, lifecycle, list, applications. |
MembershipService |
Who belongs: invite, status transitions, removal. |
RegistryService |
The application registry and per-organization enablement. |
OrganizationContext |
The active-organization context value ({ organizationId }). |
OrganizationService
platform/organizations/src/service.ts:
| Method | Signature | Fails with |
|---|---|---|
create |
{ name, ownerUserId } |
SlugTaken |
listForUser |
userId |
- |
listAll |
- (admin only) | - |
count |
- | - |
get / getBySlug |
id / slug |
OrganizationNotFound |
setStatus |
id, status |
IllegalOrganizationTransition |
getContext |
- | - |
listEnabledApplications |
organizationId |
- |
enableApplication |
organizationId, applicationSlug |
- |
create runs inside withTransaction: it slugifies the name (deduplicating name, name-2, …), inserts the org as pending, inserts the creator as an active member, creates an owner role wired to every registered permission, and grants it to the creator. The whole sequence is atomic.
setStatus enforces the lifecycle state machine from lifecycle.ts (see Multi-tenancy). Setting verified also stamps verified_at.
listForUser returns only the user’s active memberships. This is what feeds the organization switcher and active-organization resolution.
enableApplication and listEnabledApplications manage organization_applications. The richer registry surface (with full metadata and typed failures) lives on RegistryService.
MembershipService
platform/organizations/src/membership.ts:
| Method | Signature | Fails with |
|---|---|---|
invite |
organizationId, email |
UserNotFound, AlreadyMember |
setStatus |
membershipId, status |
MembershipNotFound, IllegalMembershipTransition |
remove |
membershipId |
MembershipNotFound, IllegalMembershipTransition |
invite looks the user up by email on public.user (joining through the platform profile), refuses duplicate memberships, and inserts the membership as invited. setStatus enforces the membership state machine from the same file; remove is a transition to removed, which is terminal (the member must be re-invited).
RegistryService
platform/organizations/src/registry.ts (documented in detail on the Application registry page):
| Method | Purpose |
|---|---|
listForOrganization |
Enabled applications for an org, as full metadata. |
listAll |
Every registered application, no org context. |
register |
Upsert an application definition from app.config.ts. |
getForOrganization |
One enabled application by slug, with typed failures. |
setEnabled |
Toggle enablement for an org. |
Schemas
The shared schemas live in platform/organizations/src/schemas/organization.ts:
organizationStatusSchema-pending | reviewing | verified | suspended.membershipStatusSchema-invited | active | suspended | removed.OrganizationStatus/MembershipStatus- the derived types.
These are the single source of truth for the status strings; both lifecycle state machines parse rows through them.
Errors
The package’s typed errors (platform/organizations/src/errors.ts) include:
OrganizationNotFound({ id } | { slug })SlugTaken({ slug })IllegalOrganizationTransition({ from, to })NotOrganizationMember()MembershipNotFound({ id })AlreadyMember({ email })UserNotFound({ email })IllegalMembershipTransition({ from, to })ApplicationNotFound({ slug })ApplicationDisabled({ slug })InvalidApplicationMetadata({ errors })
Server functions catch these on the Effect failure channel and fold them into serializable results.
Layers and testing
OrganizationService.Live,MembershipService.Live, andRegistryService.Livewire the implementations to whateverDatabaseandLoggerprovide.- Each also exposes a
Testlayer backed byDatabase.Test, so service tests run without Postgres. - Implementations are factored into
makeOrganizationService(sql, logger),makeMembershipService(sql), andmakeRegistryService(sql, logger).
Next steps
- Request context - how these services get provided per request.
- Database - the gateway these services use.