Skip to content
Africa OS Knowledgebase
Esc
navigateopen⌘Jpreview
On this page

Authentication

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

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:

// 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, resolved per organization by the request context.

Next steps

Last updated on August 18, 2026