---
title: Request context
description: How Africa OS turns one HTTP request into a fully provisioned Effect runtime - user, organization, permissions, and a run function for server programs.
---

The request context is the heart of the platform. Instead of each server function hand-assembling its own session, organization, database, and logger, Africa OS builds one **request runtime** that bundles everything together. Server functions call `runRequest` and receive a fully provisioned context.

### The problem it solves

In a naive design, every server function would:

1. Resolve the session from cookies.
2. Look up the platform profile.
3. Figure out the active organization.
4. Compute the permission set.
5. Construct a database handle.

That is error-prone, repeated everywhere, and a place where tenant bugs hide. Africa OS centralizes it in `@africaos/request-context`.

### The request pipeline

`buildRuntime(headers, options)` assembles the runtime for a single request:

```mermaid
flowchart LR
    A["Authenticate<br/>requireCurrentUser"] --> B["Sync platform profile<br/>upsert platform.users"]
    B --> C["List organizations<br/>listForUser"]
    C --> D["Resolve active org<br/>slug > cookie > first"]
    D --> E["Resolve permissions<br/>permissionsFor(user, org)"]
    E --> F["Resolve application<br/>registry + access gate"]
    F --> G["RequestRuntime"]
```

Each step is an Effect program run against a `ManagedRuntime` sharing a process-wide memo map, so the Postgres pool and the Better Auth instance are built **once per process**, not once per request.

### What the runtime carries

```ts
export interface RequestRuntime {
  readonly user: CurrentUserValue;                 // who is calling
  readonly organization: Option.Option<Organization>; // active tenant
  readonly application: Option.Option<CurrentApplicationValue>; // scoped application
  readonly permissions: ReadonlySet<Permission>;   // capabilities
  readonly run: (program) => Promise<Success>;     // execute an Effect program
}
```

The `run` function executes an Effect program inside the request's runtime, with every request-scoped service already provided. Programs can require `CurrentUser`, `CurrentOrganization`, `CurrentApplication`, `CurrentPermissions`, `RequestContext`, and all the platform services.

### Resolution rules

**Active organization.** The URL slug (`:organization`) wins when it resolves to a real organization that is one of the user's active memberships. Otherwise the `africaos.active_org` cookie wins when it names an active membership. Otherwise the first active membership. No memberships means `None`.

**Application.** Only relevant for organization-scoped requests (`:organization/:application/*`). The application must exist in the registry (`ApplicationNotFound`), be enabled for the active organization (`ApplicationDisabled`), and the user must hold `platform.applications.access` (`Forbidden`). This is the platform-level gate before any domain permission matters.

**Permissions.** Resolved once per request, scoped to the active organization, via `AuthorizationService.permissionsFor(userId, organizationId)`. A user with no active organization holds an empty set.

### The URL scope

Server functions that live inside an application receive the URL scope so the runtime verifies the segments server-side:

```ts
export interface RequestScope {
  readonly organizationSlug?: string;
  readonly applicationSlug?: string;
}
```

The runtime never trusts the URL on its own: an unknown slug fails with `OrganizationNotFound`, a real organization the user does not belong to fails with `NotOrganizationMember`, and a disabled application fails with `ApplicationDisabled`.

### Using it in server functions

```ts
import { runRequest } from "@africaos/request-context";
import { CurrentOrganization } from "@africaos/request-context";

const listProducts = createServerFn({ method: "GET" })
  .validator((data: { organizationSlug: string }) => data)
  .handler(async ({ data }) => {
    const organization = await runRequest(request.headers, Effect.gen(function* () {
      const org = yield* CurrentOrganization;
      return org;
    }), { scope: { organizationSlug: data.organizationSlug } });
    // ...
  });
```

The `scope` option tells the runtime to verify the slug server-side rather than falling back to the cookie default.

### runRequestFolded - handling stale sessions

`runRequest` authenticates *inside* `buildRuntime`, so a missing or expired session rejects with `Unauthenticated` before the program can fold its own failures. `runRequestFolded` catches exactly that one rejection and maps it to a caller-chosen neutral result:

```ts
const products = await runRequestFolded(
  headers,
  program,
  { scope },
  () => [] // unauthenticated → empty list, never a 500
);
```

Every other failure still rejects. This keeps direct calls and stale sessions from surfacing as server errors.

### Request-scoped services

Programs may require any of:

| Tag | Holds |
| --- | --- |
| `CurrentUser` | The resolved platform user. |
| `CurrentOrganization` | The active organization (`Option`). |
| `CurrentApplication` | The scoped application (`Option`). |
| `CurrentPermissions` | The capability set (`Set<Permission>`). |
| `RequestContext` | All of the above in one value. |
| Platform services | `Database`, `Logger`, `AuthService`, `OrganizationService`, `MembershipService`, `AuthorizationService`, `RegistryService`. |

### Why this matters for tenancy

Because `CurrentOrganization` comes from the runtime, application code cannot accidentally use a client-supplied organization id. The tenant boundary is enforced by construction: the runtime resolved it, and the database layer provides `orgWhere`/`orgScope` to keep queries scoped to it.

### Next steps

- [Permissions](/architecture/permissions) - what the permission set contains and how guards use it.
- [Database](/platform/database) - how queries are kept tenant-scoped.