---
title: Permissions
description: Capability-based authorization - permission strings, roles, resolution, and the guards server and client code use.
---

Permissions answer "what may this caller do?". Africa OS uses **capability-based authorization**: a permission is a branded string in the shape `application.resource.action`, and the platform resolves the set of capabilities a user holds in an organization. Server functions check capabilities with guards; the UI shapes itself around the same set.

### The permission model

A permission looks like this:

```
retail.products.manage
platform.applications.access
dashboard.apps.read
```

Three segments, each lowercase letters, digits, or hyphens, joined by dots. The shape is enforced by a regex and a Zod schema (`platform/permissions/src/permission.ts`):

```ts
const permissionPattern = /^[a-z0-9-]+\.[a-z0-9-]+\.[a-z0-9-]+$/;

export const permissionSchema = z
  .string()
  .regex(permissionPattern, "permission must look like application.resource.action");
```

The parsed value is a branded type, so capabilities are distinguishable from plain strings at the type level:

```ts
export type Permission = z.infer<typeof permissionSchema> & {
  readonly __permission: unique symbol;
};

export const parsePermission = (value: string): Permission => { ... };
```

`parsePermission` is the single place permission strings enter the typed world. It fails with `InvalidPermission` on malformed input, and every guard **fails closed**: an unparseable string is never granted.

### The resolution chain

A user's capability set in an organization is resolved through a five-hop chain:

```mermaid
flowchart LR
    Member["organization_members (active)"] --> MemberRoles["member_roles"]
    MemberRoles --> Roles["roles"]
    Roles --> RolePermissions["role_permissions"]
    RolePermissions --> Permissions["permissions"]
```

`AuthorizationService.permissionsFor(userId, organizationId)` joins across all five tables and returns a distinct `Set<Permission>` (`platform/permissions/src/service.ts`). Only **active** memberships count. A malformed seed row is skipped defensively rather than taking down request assembly.

### Where roles come from

When an organization is created, `OrganizationService.create` inserts an **owner** role wired to every registered permission, so the founder can do anything. Organizations that manage their own access model create additional roles and assign them to memberships via `member_roles`.

The permission set is resolved **once per request**, in the request runtime, scoped to the active organization. It is provided to programs as `CurrentPermissions`.

### The guards

`platform/permissions/src/guard.ts` provides the two functions application code uses:

**`can` - the client-side mirror, used to shape UI:**

```ts
const canManageProducts = yield* can("retail.products.manage");
// use it to show or hide a button
```

`can` returns an `Effect.Effect<boolean>` reading `CurrentPermissions`. It fails closed on malformed strings.

**`requirePermission` - the server-side enforcement point:**

```ts
yield* requirePermission("retail.products.manage");
// safe to proceed: the caller holds the capability
```

`requirePermission` fails with a typed `Forbidden` when the capability is absent (or unparseable). Server functions call it **before acting**, and the request runtime is the only source of the permission set.

### Server vs client - the rule

The client check is never the sole guard. The pattern is:

1. The client renders controls conditionally with `can` (better UX, less noise).
2. The server function enforces with `requirePermission` (the real gate).

A guarded server function must call `requirePermission` regardless of what the UI shows. Client-side hiding is a convenience; server-side guarding is the security boundary.

### The application access gate

There is one special permission: `platform.applications.access`. The request runtime checks it before scoping a request to an application:

- The application must exist in the registry.
- It must be enabled for the active organization.
- The user must hold `platform.applications.access`.

Only after all three pass does any domain permission inside the application matter. This is the platform-level gate, separate from domain capabilities.

### Error handling

Authorization failures are typed, Effect-native values:

- `Forbidden({ permission })` - thrown by `requirePermission` and the application gate.
- `InvalidPermission({ value })` - thrown by `parsePermission` on malformed input.

Server functions catch `Forbidden` and fold it into a serializable result (typically a 403-style response), so callers never see an unhandled exception.

### Next steps

- [Request context](/architecture/request-context) - how the permission set gets resolved per request.
- [Adding a feature guide](/guides/adding-a-feature) - how to guard a new feature's server functions.