---
title: Organizations
description: 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](/architecture/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](/architecture/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](/architecture/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`, and `RegistryService.Live` wire the implementations to whatever `Database` and `Logger` provide.
- Each also exposes a `Test` layer backed by `Database.Test`, so service tests run without Postgres.
- Implementations are factored into `makeOrganizationService(sql, logger)`, `makeMembershipService(sql)`, and `makeRegistryService(sql, logger)`.

### Next steps

- [Request context](/architecture/request-context) - how these services get provided per request.
- [Database](/platform/database) - the gateway these services use.