# Adding Authentication

Add user authentication with Agentuity Auth

Protect your agents and routes with user authentication. Agentuity provides a first-party auth solution powered by [BetterAuth](https://better-auth.com).

> [!NOTE]
> **Platform Auth Overview**
> This page covers React frontend auth setup. For a platform overview covering API keys, bearer tokens, and session auth configuration, see [Authentication Services](/services/authentication).

## Full-Stack Auth in Seconds

### Frontend

```tsx
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) return <LoginForm />;
return <div>Welcome, {user.name}</div>;
```
### API Route

```typescript
router.use('/api/*', authMiddleware);

router.get('/api/me', async (c) => {
  const user = await c.var.auth.getUser();
  return c.json(user);
});
```
### Agent

```typescript
handler: async (ctx, input) => {
  const user = await ctx.auth?.getUser();
  return `Hello, ${user?.name ?? 'anonymous'}!`;
}
```
## What You Get

- **Email/password authentication** out of the box
- **Session and API key middleware** for routes
- **Native `ctx.auth` support** in agents
- **Organizations, teams, and roles** via BetterAuth plugins
- **JWT tokens** for external service integration

## Quick Start

Use the `agentuity-auth` template, or add auth to any project manually:

```bash
# Start from the auth template
bunx agentuity create my-app --template agentuity-auth

# Or add auth to an existing project
bun add @agentuity/auth better-auth drizzle-orm
```

Then create your auth configuration (see [Server Setup](#server-setup) below), set `DATABASE_URL` and `AGENTUITY_AUTH_SECRET` in your environment, and run the BetterAuth CLI to generate and apply the schema (use `bunx` or `npx`):

```bash
# With Bun
bunx @better-auth/cli generate
bunx @better-auth/cli migrate

# Or with npm
npx @better-auth/cli generate
npx @better-auth/cli migrate
```

## Server Setup

### The Basics

Create an auth instance with just a connection string:

```typescript title="src/auth.ts"
import { createAuth } from '@agentuity/auth';

export const auth = createAuth({
  connectionString: process.env.DATABASE_URL,
});
```

That's it! This gives you:
- Email/password authentication
- Session management
- All default plugins (see below)

### Default Plugins

Agentuity Auth includes these plugins automatically:

| Plugin | Purpose |
|--------|---------|
| `organization` | Multi-tenancy with teams, roles, and invitations |
| `jwt` | JWT token generation with JWKS endpoint |
| `bearer` | Bearer token auth via `Authorization` header |
| `apiKey` | API key authentication for programmatic access |

> [!NOTE]
> **Skip Default Plugins**
> If you need full control over plugins, use `skipDefaultPlugins: true` and add only what you need.

### Mounting Auth Routes

Mount the auth handler to expose sign-in, sign-up, session, and other endpoints:

```typescript title="src/api/index.ts"
import { Hono } from 'hono';
import type { Env } from '@agentuity/runtime';
import { mountAuthRoutes } from '@agentuity/auth';
import { auth } from '../auth';

const router = new Hono<Env>();

// Mount auth routes at /api/auth/*
router.on(['GET', 'POST'], '/api/auth/*', mountAuthRoutes(auth));

export default router;
```

### Advanced Configuration

```typescript title="src/auth.ts"
import { createAuth } from '@agentuity/auth';

export const auth = createAuth({
  connectionString: process.env.DATABASE_URL,
  // Or: database: drizzleAdapter(db, { provider: 'pg', schema: authSchema }),

  skipDefaultPlugins: false, // Set true for full control over plugins
  apiKey: { enabled: true, defaultPrefix: 'ag_', defaultKeyLength: 64 },
  trustedOrigins: ['https://your-domain.com'], // Auto-resolved from env by default
  plugins: [], // Add custom BetterAuth plugins
});
```

## Middleware

### Session Middleware

Protect routes with session-based authentication:

```typescript
import { createSessionMiddleware } from '@agentuity/auth';
import { auth } from '../auth';

// Required authentication (returns 401 if not authenticated)
const authMiddleware = createSessionMiddleware(auth);

// Optional authentication (continues without auth context if not authenticated)
const optionalAuth = createSessionMiddleware(auth, { optional: true });

// Role-based access (returns 403 if user lacks required role)
const adminOnly = createSessionMiddleware(auth, { hasOrgRole: ['admin', 'owner'] });
```

**Usage examples:**

```typescript
// Protect all API routes
router.use('/api/*', authMiddleware);

// Allow both authenticated and anonymous access
router.get('/api/content', optionalAuth, async (c) => {
  const user = await c.var.auth.getUser().catch(() => null);
  return c.json({ premium: !!user });
});

// Admin-only route
router.get('/api/admin', adminOnly, async (c) => {
  return c.json({ message: 'Welcome, admin!' });
});
```

### API Key Middleware

For programmatic access via API keys:

```typescript
import { createApiKeyMiddleware } from '@agentuity/auth';
import { auth } from '../auth';

const apiKeyAuth = createApiKeyMiddleware(auth);
const optionalApiKey = createApiKeyMiddleware(auth, { optional: true });
const writeAccess = createApiKeyMiddleware(auth, { hasPermission: { project: 'write' } });
const fullAccess = createApiKeyMiddleware(auth, { hasPermission: { project: ['read', 'write'], admin: '*' } });
```

**API keys are sent via headers:** `x-agentuity-auth-api-key: your_key` or `Authorization: ApiKey your_key`

### The Auth Context

When middleware authenticates a request, `c.var.auth` provides these methods:

| Method | Returns | Description |
|--------|---------|-------------|
| `getUser()` | `Promise<AuthUser>` | Get the authenticated user |
| `getOrg()` | `Promise<AuthOrgContext \| null>` | Get active organization with full details |
| `getOrgRole()` | `Promise<string \| null>` | Get user's role in active org |
| `hasOrgRole(...roles)` | `Promise<boolean>` | Check if user has one of the specified roles |
| `hasPermission(resource, ...actions)` | `boolean` | Check API key permissions |
| `getToken()` | `Promise<string \| null>` | Get the bearer token from request |
| `authMethod` | `'session' \| 'api-key' \| 'bearer'` | How the request was authenticated |
| `apiKey` | `AuthApiKeyContext \| null` | API key details (if authenticated via API key) |

**Example:**

```typescript
router.get('/api/profile', authMiddleware, async (c) => {
  const user = await c.var.auth.getUser();
  const org = await c.var.auth.getOrg();
  const isAdmin = await c.var.auth.hasOrgRole('admin', 'owner');

  return c.json({
    user: { id: user.id, email: user.email, name: user.name },
    organization: org ? { id: org.id, name: org.name, role: org.role } : null,
    isAdmin,
  });
});
```

> [!NOTE]
> **Full Type Reference**
> See the complete type definitions in the [auth package types](https://github.com/agentuity/sdk/tree/main/packages/auth/src/agentuity/types.ts).

## Client Setup

### Creating the Auth Client

```typescript title="src/web/auth-client.ts"
import { createAuthClient } from '@agentuity/auth/react';

export const authClient = createAuthClient();

// Export methods for use in components
export const { signIn, signUp, signOut, useSession, getSession } = authClient;
```

### AuthProvider

Wrap your app with `AuthProvider`. Add `AgentuityProvider` only if you also use `@agentuity/react` transport hooks and need `AuthProvider` to mirror the bearer token into them:

```tsx title="src/web/frontend.tsx"
import { useState } from 'react';
import { AgentuityProvider } from '@agentuity/react';
import { AuthProvider, createAuthClient } from '@agentuity/auth/react';
import { App } from './App';

const authClient = createAuthClient();

export function Root() {
  const [authHeader, setAuthHeader] = useState<string | null>(null);

  return (
    <AgentuityProvider authHeader={authHeader}>
      <AuthProvider
        authClient={authClient}
        onAuthHeaderChange={setAuthHeader}
      >
        <App />
      </AuthProvider>
    </AgentuityProvider>
  );
}
```

### useAuth Hook

Access auth state in your components:

```tsx
import { useAuth } from '@agentuity/auth/react';

function Profile() {
  const { user, isAuthenticated, isPending, error } = useAuth();

  if (isPending) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!isAuthenticated) return <div>Please sign in</div>;

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}
```

### Sign In / Sign Up / Sign Out

```tsx
import { signIn, signUp, signOut } from './auth-client';

// Email/password sign in
await signIn.email({ email, password });

// Email/password sign up
await signUp.email({ email, password, name });

// Sign out
await signOut();
```

## Using Auth in Agents

### The ctx.auth Interface

Auth is available natively on `ctx.auth` in agent handlers:

```typescript title="src/agent/protected/agent.ts"
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';

export default createAgent('protected', {
  schema: {
    input: s.object({ query: s.string() }),
    output: s.object({ result: s.string(), userId: s.string() }),
  },
  handler: async (ctx, { query }) => {
    if (!ctx.auth) {
      return { result: 'Please sign in', userId: '' };
    }

    const user = await ctx.auth.getUser();
    const org = await ctx.auth.getOrg();

    // Check organization roles
    if (org && await ctx.auth.hasOrgRole('admin')) {
      // Admin-only logic
    }

    return { result: `Hello ${user.name}`, userId: user.id };
  },
});
```

### Agent-to-Agent Auth Propagation

When one agent calls another, auth context propagates automatically:

```typescript title="src/agent/hello/agent.ts"
import { createAgent } from '@agentuity/runtime';
import poemAgent from '../poem/agent';

export default createAgent('hello', {
  handler: async (ctx, { name }) => {
    const user = ctx.auth ? await ctx.auth.getUser() : null;

    // Auth context passes through to poem agent automatically
    const poem = await poemAgent.run({
      userEmail: user?.email,
      userName: name,
    });

    return `Hello ${name}!\n\n${poem}`;
  },
});
```

## Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `DATABASE_URL` | PostgreSQL connection string | **Required** |
| `AGENTUITY_AUTH_SECRET` | Secret for signing tokens | Falls back to `BETTER_AUTH_SECRET` |
| `AGENTUITY_BASE_URL` | Base URL for auth callbacks | Falls back to `BETTER_AUTH_URL` |

**Auto-resolved trusted origins:**
- `AGENTUITY_CLOUD_DOMAINS` - Platform-set domains (deployment URLs, custom domains)
- `AUTH_TRUSTED_DOMAINS` - Developer-set additional trusted domains (comma-separated)

**Generate a secure secret:**

```bash
openssl rand -hex 32
```

> [!WARNING]
> **Replace Dev Secret Before Deploying**
> If you're using a development secret, generate a new one before deploying to production. Store it securely in your environment variables.

## Database Configuration

### Connection String (Simplest)

Just provide the connection string and Agentuity handles the rest:

```typescript
import { createAuth } from '@agentuity/auth';

export const auth = createAuth({
  connectionString: process.env.DATABASE_URL,
});
```

### Bring Your Own Drizzle

If you have an existing Drizzle setup:

```typescript
import { drizzle } from 'drizzle-orm/bun-sql';
import { drizzleAdapter } from '@agentuity/drizzle';
import * as authSchema from '@agentuity/auth/schema';
import * as appSchema from './schema';

const schema = { ...authSchema, ...appSchema };
const db = drizzle(process.env.DATABASE_URL!, { schema });

export const auth = createAuth({
  database: drizzleAdapter(db, { provider: 'pg', schema: authSchema }),
});
```

The `@agentuity/auth/schema` export provides all auth-related Drizzle tables (`user`, `session`, `account`, `verification`, `organization`, `member`, `invitation`, `apiKey`).

## Built-in Features

### Organizations & Teams

Create and manage organizations:

```typescript
// Create an organization
const org = await auth.api.createOrganization({
  body: { name: 'My Team', slug: 'my-team' },
  headers: c.req.raw.headers,
});

// Get user's role in active org
const role = await c.var.auth.getOrgRole();

// Check role
if (await c.var.auth.hasOrgRole('admin', 'owner')) {
  // Admin actions
}
```

> [!NOTE]
> **Full Organization API**
> See the [BetterAuth Organization Plugin docs](https://www.better-auth.com/docs/plugins/organization) for the complete API including invitations, member management, and role configuration.

### API Keys

Create API keys for programmatic access:

```typescript
const result = await auth.api.createApiKey({
  body: {
    name: 'my-integration',
    userId: user.id,
    expiresIn: 60 * 60 * 24 * 30, // 30 days
    permissions: { project: ['read', 'write'] },
  },
});
console.log('API Key:', result.key); // Only shown once!
```

> [!NOTE]
> **Full API Key API**
> See the [BetterAuth API Key Plugin docs](https://www.better-auth.com/docs/plugins/api-key) for listing, revoking, and permission schemas.

### JWT & Bearer Tokens

```typescript
// Get token in route handler
const token = await c.var.auth.getToken();

// JWKS endpoint: GET /api/auth/.well-known/jwks.json
```

> [!NOTE]
> **Full JWT API**
> See the [BetterAuth JWT Plugin docs](https://www.better-auth.com/docs/plugins/jwt) for token customization and verification.

## Schema & Migrations

Use the BetterAuth CLI to generate and apply the auth schema (use `bunx` or `npx`):

```bash
# Generate SQL for all auth tables (useful for review or manual migrations)
bunx @better-auth/cli generate
# or: npx @better-auth/cli generate

# Apply migrations directly
bunx @better-auth/cli migrate
# or: npx @better-auth/cli migrate
```

If you use Drizzle or Prisma, prefer your ORM's own migration tooling and generate the schema with `@better-auth/cli generate`, then incorporate it into your migration files.

## Next Steps

- [Middleware & Routes](/routes/middleware): Authentication and validation patterns
- [Provider Setup](/frontend/provider-setup): `AgentuityProvider` configuration for `@agentuity/react` apps
- [React Hooks](/frontend/react-hooks): Building custom UIs
- [Authentication Services](/services/authentication): Choose between OIDC and app-owned auth