alepha@docs:~/docs/guides/server$
cat 4-authentication.md
2 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

$secure() supports authentication-only, role checks, permission checks, and custom guards:

typescript
 1// Auth only — any authenticated user 2profile = $action({ 3  use: [$secure()], 4  handler: ({ user }) => user, 5}); 6  7// Auth + role check 8dashboard = $action({ 9  use: [$secure({ roles: ["admin"] })],10  handler: () => { /* ... */ },11});12 13// Auth + explicit permissions14deleteOrder = $action({15  use: [$secure({ permissions: ["orders:delete"] })],16  handler: ({ params }) => { /* ... */ },17});18 19// Auth + permissions + issuer restriction20adminManage = $action({21  use: [$secure({ permissions: ["admin:manage"], issuers: ["admin"] })],22  handler: () => { /* ... */ },23});24 25// Auth + custom guard26ownProfile = $action({27  use: [$secure({ guard: (user) => user.id === params.id })],28  handler: () => { /* ... */ },29});

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});

#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. Admin-namespaced actions are excluded entirely.