alepha@docs:~/docs/guides/server$
cat 4-authentication.md | pretty
4 min read
Last commit:

#Authentication

Alepha provides JWT-based authentication through $issuer for token management and $realm for full user management.

#Token Management with $issuer

$issuer is the low-level primitive for creating and verifying JWT tokens. Use it when you manage users yourself or integrate with an external identity provider.

typescript
 1import { $issuer } from "alepha/security"; 2import { $action } from "alepha/server"; 3import { t } from "alepha"; 4  5class AuthController { 6  issuer = $issuer({ 7    secret: "your-secret-key", 8  }); 9 10  login = $action({11    method: "POST",12    path: "/auth/login",13    schema: {14      body: t.object({15        email: t.email(),16        password: t.text(),17      }),18    },19    handler: async ({ body }) => {20      const user = await this.authenticate(body.email, body.password);21      return this.issuer.createToken(user);22    },23  });24}

#Internal vs External Issuers

An internal issuer signs and verifies tokens with a shared secret:

typescript
1issuer = $issuer({2  secret: "my-secret",3});

An external issuer verifies tokens from an external provider (Auth0, Keycloak, etc.) using JWKS:

typescript
1issuer = $issuer({2  jwks: () => process.env.AUTH0_JWKS_URL,3  profile: (payload) => ({4    id: payload.sub,5    email: payload.email,6    name: payload.name,7  }),8});

#Token Lifecycle

$issuer manages access tokens and refresh tokens:

Setting Default
Access token expiration 15 minutes
Refresh token expiration 30 days

Override via the settings option:

typescript
1issuer = $issuer({2  secret: "...",3  settings: {4    accessToken: { expiration: [1, "hours"] },5    refreshToken: { expiration: [90, "days"] },6  },7});

#User Management with $realm

$realm is a higher-level primitive that wraps $issuer with built-in user management: registration, login, sessions, password handling, and identity providers.

typescript
1import { $realm } from "alepha/api/users";2 3class App {4  realm = $realm();5}

$realm ships with two default roles:

  • admin -- Full access to all resources and permissions.
  • user -- Access to owned resources only.

#Identity Providers

Enable login methods through the identities option:

typescript
1realm = $realm({2  identities: {3    credentials: true,  // email/password (default)4    google: true,       // Google OAuth5    github: true,       // GitHub OAuth6  },7});

#Securing Actions

Actions are public by default. To require authentication, add the $secure() middleware:

typescript
 1import { $secure } from "alepha/security"; 2  3publicEndpoint = $action({ 4  handler: () => "anyone can access this", 5}); 6  7protectedEndpoint = $action({ 8  use: [$secure()], 9  handler: () => "only authenticated users",10});

The authenticated user is available on the request object:

typescript
1profile = $action({2  path: "/me",3  use: [$secure()],4  handler: async ({ user }) => {5    return user;6  },7});

You can also restrict access to a specific issuer or role:

typescript
1adminOnly = $action({2  use: [$secure({ issuers: ["admin"] })],3  handler: () => "admin issuer only",4});5 6managersOnly = $action({7  use: [$secure({ roles: ["manager", "admin"] })],8  handler: () => "managers and admins only",9});

#User Resolution

$secure() resolves the authenticated user using atom-first resolution, which works across all transports:

  1. currentUserAtom — checked first. Set by $action.run() fork, MCP transports, pipelines, and jobs.
  2. request.user — HTTP request user set by previous middleware.
  3. HTTP headers — JWT or API key resolved from Authorization header.

#Local Action Calls

When calling an action locally via .run(), pass the user in options:

typescript
1// Pass a specific user2await controller.action.run({}, { user: { id: "user-1", roles: ["admin"] } });3 4// Use the system user5await controller.action.run({}, { user: "system" });6 7// Use the user from the current HTTP request8await controller.action.run({}, { user: "context" });

The user is scoped to the action call using ALS fork isolation — it does not leak to subsequent calls.

In test mode, .fetch() automatically creates a JWT token from the user option:

typescript
1// Automatic test token creation2const res = await controller.action.fetch({}, { user: { id: "test-user" } });

#Roles and Permissions

Define roles with permission sets in the issuer:

typescript
 1issuer = $issuer({ 2  secret: "...", 3  roles: [ 4    { 5      name: "admin", 6      permissions: [{ name: "*" }], 7    }, 8    { 9      name: "editor",10      permissions: [11        { name: "articles:*" },12        { name: "media:upload" },13        { name: "admin:articles:*" },14      ],15    },16    {17      name: "viewer",18      permissions: [19        { name: "articles:list" },20        { name: "articles:get" },21      ],22    },23  ],24});

#Wildcard Permissions

Permissions use a colon-separated hierarchy. The * wildcard matches everything at and below its level:

Pattern Matches Does not match
* Everything (admin access)
articles:* articles:list, articles:get, articles:delete media:upload
admin:articles:* admin:articles:list, admin:articles:update admin:users:list

Permissions declared in $secure({ permissions: [...] }) are auto-created in the permission registry at definition time — no separate registration step is needed.

#Ownership

The ownership flag restricts a permission to resources owned by the user:

typescript
 1{ 2  name: "user", 3  permissions: [ 4    { 5      name: "*", 6      ownership: true, 7      exclude: ["admin:*"], 8    }, 9  ],10}

This grants access to all actions, but only for the user's own resources. The exclude array removes specific permission patterns — here, all admin-namespaced actions are excluded entirely.

#$secure Options

$secure() accepts four options. All are optional — when none are provided, it only checks authentication.

typescript
1$secure({2  issuers?: string[],3  roles?: string[],4  permissions?: (string | Permission)[],5  guard?: (user: UserAccountToken) => boolean,6})

#Check Order

When multiple options are provided, checks run in this fixed order. Each check must pass before the next runs:

  1. Authentication — Is there a valid user? → UnauthorizedError (401) if not.
  2. Issuers — Does the user's realm match one of the listed issuers? → ForbiddenError (403) if not.
  3. Roles — Does the user have at least one of the listed roles? → ForbiddenError (403) if not.
  4. Permissions — Does the user's role grant all listed permissions? → ForbiddenError (403) if not.
  5. Guard — Does the custom function return true? → ForbiddenError (403) if not.

#AND vs OR Logic

  • Issuers — OR: user must match at least one of the listed issuers.
  • Roles — OR: user must have at least one of the listed roles.
  • Permissions — AND: user must have all listed permissions.
  • Options — AND: all provided options must pass.

#Examples

typescript
 1// Auth only — any authenticated user 2profile = $action({ 3  use: [$secure()], 4  handler: ({ user }) => user, 5}); 6  7// Role check (OR) — admin or manager 8dashboard = $action({ 9  use: [$secure({ roles: ["admin", "manager"] })],10  handler: () => { /* ... */ },11});12 13// Permission check (AND) — must have both14publish = $action({15  use: [$secure({ permissions: ["articles:create", "articles:publish"] })],16  handler: () => { /* ... */ },17});18 19// Issuer restriction20adminPanel = $action({21  use: [$secure({ issuers: ["admin"] })],22  handler: () => { /* ... */ },23});24 25// Custom guard — runs after all other checks26ownProfile = $action({27  use: [$secure({ guard: (user) => user.id === params.id })],28  handler: () => { /* ... */ },29});30 31// Combining options — all must pass32adminManage = $action({33  use: [$secure({34    issuers: ["main"],35    roles: ["admin"],36    permissions: ["admin:manage"],37    guard: (user) => !!user.email,38  })],39  handler: () => { /* ... */ },40});

#Browser Behavior

On the server, $secure throws errors (401/403). In the browser, it returns undefined instead — the handler is never called. Use action.can() to conditionally render UI:

typescript
1// Browser: returns undefined if unauthorized, "ok" if authorized2const result = await action();3 4// Use action.can() to check without calling5if (action.can()) {6  // render the button7}

#HTTP Basic Auth

$basicAuth provides HTTP Basic Authentication for simple use cases (webhooks, internal tools):

typescript
1import { $basicAuth } from "alepha/security";2 3webhook = $action({4  use: [$basicAuth({ username: "stripe", password: process.env.WEBHOOK_SECRET })],5  handler: ({ body }) => { /* ... */ },6});

Uses timing-safe comparison to prevent timing attacks. Returns 401 with WWW-Authenticate header on failure.

#Service Accounts

$serviceAccount manages tokens for service-to-service communication:

typescript
 1import { $serviceAccount } from "alepha/security"; 2  3// OAuth2 client credentials 4external = $serviceAccount({ 5  oauth2: { 6    url: "https://provider.com/oauth2/token", 7    clientId: process.env.CLIENT_ID, 8    clientSecret: process.env.CLIENT_SECRET, 9  },10});11 12// JWT-based (internal issuer)13internal = $serviceAccount({14  issuer: myIssuer,15  user: { id: "batch-worker" },16});17 18// Usage: tokens are cached and auto-refreshed19const token = await external.token();