Frontend

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.

Full-Stack Auth in Seconds

const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) return <LoginForm />;
return <div>Welcome, {user.name}</div>;
router.use('/api/*', authMiddleware);
 
router.get('/api/me', async (c) => {
  const user = await c.var.auth.getUser();
  return c.json(user);
});
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

Select authentication during project creation:

agentuity project create
# Select "Yes" when prompted for authentication

Initialize auth in your existing Agentuity project:

agentuity project auth init

This command:

  • Sets up a Postgres database (or uses your existing one)
  • Installs @agentuity/auth, better-auth, and drizzle-orm
  • Generates src/auth.ts with sensible defaults
  • Runs database migrations (or prompts you to run them manually)
  • Shows integration examples for routes and frontend

For non-Agentuity projects or custom setups:

bun add @agentuity/auth better-auth drizzle-orm

Then create your auth configuration manually (see Server Setup below).

Server Setup

The Basics

Create an auth instance with just a connection string:

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:

PluginPurpose
organizationMulti-tenancy with teams, roles, and invitations
jwtJWT token generation with JWKS endpoint
bearerBearer token auth via Authorization header
apiKeyAPI key authentication for programmatic access

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:

src/api/index.ts
import { createRouter } from '@agentuity/runtime';
import { mountAuthRoutes } from '@agentuity/auth';
import { auth } from '../auth';
 
const router = createRouter();
 
// Mount auth routes at /api/auth/*
router.on(['GET', 'POST'], '/api/auth/*', mountAuthRoutes(auth));
 
export default router;

Advanced Configuration

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:

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:

// 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:

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:

MethodReturnsDescription
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)booleanCheck API key permissions
getToken()Promise<string | null>Get the bearer token from request
authMethod'session' | 'api-key' | 'bearer'How the request was authenticated
apiKeyAuthApiKeyContext | nullAPI key details (if authenticated via API key)

Example:

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

Full Type Reference

See the complete type definitions in the auth package types.

Client Setup

Creating the Auth Client

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 AgentuityProvider and AuthProvider:

src/web/frontend.tsx
import { AgentuityProvider } from '@agentuity/react';
import { AuthProvider, createAuthClient } from '@agentuity/auth/react';
import { App } from './App';
 
const authClient = createAuthClient();
 
export function Root() {
  return (
    <AgentuityProvider>
      <AuthProvider authClient={authClient}>
        <App />
      </AuthProvider>
    </AgentuityProvider>
  );
}

useAuth Hook

Access auth state in your components:

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

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:

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:

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

Complete Examples

See the auth testing app for full working examples of agents with authentication.

Environment Variables

VariableDescriptionDefault
DATABASE_URLPostgreSQL connection stringRequired
AGENTUITY_AUTH_SECRETSecret for signing tokensFalls back to BETTER_AUTH_SECRET
AGENTUITY_BASE_URLBase URL for auth callbacksFalls 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:

openssl rand -hex 32

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:

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:

import { drizzle } from 'drizzle-orm/bun-sql';
import { drizzleAdapter } from 'better-auth/adapters/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:

// 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
}

Full Organization API

See the BetterAuth Organization Plugin docs for the complete API including invitations, member management, and role configuration.

API Keys

Create API keys for programmatic access:

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!

Full API Key API

See the BetterAuth API Key Plugin docs for listing, revoking, and permission schemas.

JWT & Bearer Tokens

// Get token in route handler
const token = await c.var.auth.getToken();
 
// JWKS endpoint: GET /api/auth/.well-known/jwks.json

Full JWT API

See the BetterAuth JWT Plugin docs for token customization and verification.

CLI Commands

Initialize Auth

agentuity project auth init [options]
OptionDescription
--skipMigrationsSkip running database migrations
--skipInstallSkip installing dependencies

ORM Detection

If Drizzle or Prisma is detected in your project, init will skip automatic migrations and prompt you to run them with your ORM tools instead.

Generate Schema

Generate SQL schema for auth tables (useful for manual migrations):

agentuity project auth generate

This outputs the SQL needed to create all auth tables, which you can then run with your preferred migration tool.

Next Steps

Need Help?

Join our DiscordCommunity for assistance or just to hang with other humans building agents.

Send us an email at hi@agentuity.com if you'd like to get in touch.

Please Follow us on

If you haven't already, please Signup for your free account now and start building your first agent!