---
title: Authentication
description: How identity works - Better Auth behind an Effect service, sessions, Google OAuth, WorkOS enterprise SSO, and the session helpers server code uses.
---

Authentication answers the question "who is calling?". Africa OS uses **Better Auth** for identity under the hood, but application code never touches it. Everything goes through `AuthService`, an Effect service in `@africaos/auth`.

### The layering

```mermaid
flowchart LR
    ServerFunction["Server function"]
    SessionHelper["requireCurrentUser / resolveCurrentUser"]
    AuthService["AuthService (Effect service)"]
    Provider["provider.ts - the Better Auth boundary"]
    BetterAuth["better-auth"]
    Database[("PostgreSQL - public schema")]

    ServerFunction --> SessionHelper
    SessionHelper --> AuthService
    AuthService --> Provider
    Provider --> BetterAuth
    BetterAuth --> Database
```

The key rule: **`provider.ts` is the only file in the repository that imports `better-auth`.** Everything else programs against the small, typed `AuthService` surface. This keeps the identity provider swappable and gives server functions Effect-native errors.

### What AuthService offers

`AuthService` is an Effect `Context.Service` (`platform/auth/src/service.ts`). Its methods take the incoming request `Headers` and return the parsed result plus the auth `Response` (so the caller can propagate the `Set-Cookie` headers):

| Method | Purpose |
| --- | --- |
| `signIn` | Email + password sign-in. |
| `signUp` | Email + password sign-up. |
| `signOut` | End the session, returning the response carrying the clearing cookies. |
| `getSession` | Resolve the active session, as `Option.none()` when unauthenticated. |
| `signInWithGoogle` | Start a Google OAuth handshake, returning the consent URL. |
| `completeGoogleSignIn` | Exchange the OAuth callback code/state for a session. |
| `signInEnterpriseMember` | Issue a session for a WorkOS-verified enterprise member. |

Every method returns typed errors instead of throwing ad hoc: `InvalidCredentials`, `EmailAlreadyInUse`, `OAuthUnavailable`, and `AuthFailure`.

### Sign-in errors are typed

`AuthService` maps Better Auth responses onto typed failures:

- A 401 from the provider becomes `InvalidCredentials`.
- A 422 with an `EMAIL_ALREADY_IN_USE` or `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` code becomes `EmailAlreadyInUse`.
- OAuth handshakes succeed as redirects (3xx with a `Location` header), so success is tested by status range, not `response.ok`.
- Anything else becomes `AuthFailure` wrapping the cause.

This is why server functions can fold failures into serializable results without string-matching error messages.

### Google OAuth

Google sign-in has two halves:

1. **Start** - `signInWithGoogle(headers, callbackURL)` returns the consent URL to send the browser to, alongside the auth `Response` whose cookies carry the OAuth state across the redirect.
2. **Complete** - `completeGoogleSignIn(headers, { code, state })` exchanges the callback parameters for a session and returns the auth `Response`.

Both fail with `OAuthUnavailable` when Google is not configured (no `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`), so the feature degrades gracefully instead of throwing at runtime.

### WorkOS enterprise SSO

WorkOS handles enterprise single sign-on. `signInEnterpriseMember` issues a Better Auth session for a member who has already been verified by WorkOS - the service takes the verified `profileId` and `connectionId` rather than re-authenticating. The WorkOS provider lives in `platform/auth/src/workos/service.ts`.

### The session helpers

`platform/auth/src/session.ts` offers the two helpers server code uses most:

```ts
// Optionally-resolved user; None when unauthenticated.
const user = yield* resolveCurrentUser(headers);

// Hard requirement; fails with Unauthenticated.
const user = yield* requireCurrentUser(headers);
```

Both run as Effect programs against `AuthService`, so they work anywhere a service is available. The request runtime uses `requireCurrentUser` during assembly - a request without a session fails before any handler logic runs.

`sessionCookies(response)` extracts the `Set-Cookie` values from an auth response, which callers attach to their own response to persist the session.

### Where the sessions live

Better Auth manages its own tables in the **public schema** (`user`, `session`, `account`, `verification`), created by migration `0001_identity.sql`. The platform never queries these directly - it goes through `AuthService`.

### Authentication is not authorization

Authentication only establishes *who* the caller is. Whether the caller may perform an action is the job of [permissions](/architecture/permissions), resolved per organization by the [request context](/architecture/request-context).

### Next steps

- [Request context](/architecture/request-context) - how the identity becomes a full runtime.
- [Authentication](/architecture/authentication) - this guide, the full service surface and the provider boundary.