Control who can access your agents and routes. Agentuity Auth supports sessions for browser users, API keys for programmatic access, and bearer tokens for service-to-service calls. Built on BetterAuth, it works with any PostgreSQL database.
Quick Start
Install the auth and drizzle packages:
bun add @agentuity/auth @agentuity/drizzleCreate an auth instance, mount the auth routes, and protect your API with session middleware:
import { createRouter } from '@agentuity/runtime';
import { createAuth, mountAuthRoutes, createSessionMiddleware } from '@agentuity/auth';
const auth = createAuth({
connectionString: process.env.DATABASE_URL,
});
const router = createRouter();
// Expose sign-in, sign-up, session, and token endpoints
router.on(['GET', 'POST'], '/api/auth/*', mountAuthRoutes(auth));
// Require a valid session for all other API routes
router.use('/api/*', createSessionMiddleware(auth));
export default router;Auth Methods
| Method | When to Use | How It Works |
|---|---|---|
| Session | Browser users with a login flow | Cookie-based, managed by BetterAuth |
| API Key | Server-to-server or CLI tools | Header: x-agentuity-auth-api-key or Authorization: ApiKey <key> |
| Bearer Token | JWT-based service auth | Header: Authorization: Bearer <token> |
You can check which method authenticated a request with ctx.auth.authMethod, which returns 'session', 'api-key', or 'bearer'.
Auth in Different Contexts
Default Plugins
Agentuity Auth includes four plugins automatically:
| Plugin | Purpose |
|---|---|
organization | Multi-tenancy with teams, roles, and invitations |
jwt | JWT token generation with a JWKS endpoint |
bearer | Bearer token auth via the Authorization header |
apiKey | API key authentication for programmatic access |
Pass skipDefaultPlugins: true to createAuth() and add only the plugins you need. This is useful when you want a minimal setup or need to customize plugin options.
Database Setup
Agentuity Auth needs a PostgreSQL database to store users, sessions, and keys.
Connection string (simplest). Provide a URL and Agentuity creates the Bun SQL connection and Drizzle adapter internally:
import { createAuth } from '@agentuity/auth';
const auth = createAuth({
connectionString: process.env.DATABASE_URL,
});Bring your own adapter. If you already have a Drizzle setup, pass it directly:
import { createAuth } from '@agentuity/auth';
import { drizzleAdapter } from '@agentuity/drizzle';
import { drizzle } from 'drizzle-orm/bun-sql';
import * as authSchema from '@agentuity/auth/schema';
import * as appSchema from './schema';
const schema = { ...authSchema, ...appSchema };
const db = drizzle(process.env.DATABASE_URL!, { schema });
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, and apiKey.
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 |
Trusted origins are resolved automatically from AGENTUITY_CLOUD_DOMAINS (set by the platform on deploy) and AUTH_TRUSTED_DOMAINS (comma-separated, developer-set).
Generate a secure secret with openssl rand -hex 32 and store it in your environment variables before deploying.
OAuth/OIDC Integration
When your app needs to authenticate users through an external OIDC provider (Google, GitHub, Okta, Auth0, or any OIDC-compliant service), use the OAuth flow utilities from @agentuity/core/oauth. These implement the standard authorization code flow with token management and storage.
The BetterAuth-based auth above manages your own user sessions. OAuth/OIDC integration is for delegating authentication to a third-party provider, then storing and refreshing the resulting tokens.
Environment Variables
Set OAUTH_ISSUER and all endpoint URLs are derived automatically. Override individual endpoints when the provider uses non-standard paths.
| Variable | Description | Default |
|---|---|---|
OAUTH_CLIENT_ID | Client ID from your OIDC provider | Required |
OAUTH_CLIENT_SECRET | Client secret (required for token exchange) | Required |
OAUTH_ISSUER | Base URL; derives all endpoint URLs | -- |
OAUTH_AUTHORIZE_URL | Override authorize endpoint | {issuer}/authorize |
OAUTH_TOKEN_URL | Override token endpoint | {issuer}/oauth/token |
OAUTH_USERINFO_URL | Override userinfo endpoint | {issuer}/userinfo |
OAUTH_REVOKE_URL | Override revoke endpoint | {issuer}/revoke |
OAUTH_END_SESSION_URL | Override end-session endpoint | {issuer}/end_session |
OAUTH_SCOPES | Space-separated scopes | openid profile email |
Authorization Code Flow
A typical login flow: redirect the user, exchange the callback code for tokens, then fetch the user profile.
import { createRouter } from '@agentuity/runtime';
import {
buildAuthorizeUrl,
exchangeToken,
fetchUserInfo,
logout,
} from '@agentuity/core/oauth';
const api = createRouter();
// Step 1: Redirect user to the OIDC provider
api.get('/oauth/login', (c) => {
const redirectUri = `${new URL(c.req.url).origin}/api/oauth/callback`;
const loginUrl = buildAuthorizeUrl(redirectUri, { prompt: 'consent' });
return c.redirect(loginUrl);
});
// Step 2: Handle the callback, exchange code for tokens
api.get('/oauth/callback', async (c) => {
const code = c.req.query('code');
if (!code) return c.json({ error: 'Missing authorization code' }, 400);
const redirectUri = `${new URL(c.req.url).origin}/api/oauth/callback`;
const token = await exchangeToken(code, redirectUri);
const user = await fetchUserInfo(token.access_token);
c.var.logger.info('user authenticated', { sub: user.sub, email: user.email });
// Store token and set session cookie (see Token Storage below)
return c.redirect('/');
});
// Step 3: Logout, revoke token
api.get('/oauth/logout', async (c) => {
// Retrieve the stored token (from cookie, KV, or session)
const refreshToken = c.req.header('x-refresh-token');
if (refreshToken) {
await logout(refreshToken);
}
return c.redirect('/');
});
export default api;Every function reads from OAUTH_* environment variables by default. Pass an OAuthFlowConfig object as the last argument to override any setting per-call.
Token Storage
KeyValueTokenStorage persists tokens in ctx.kv and auto-refreshes expired access tokens on retrieval.
import { KeyValueTokenStorage, isTokenExpired } from '@agentuity/core/oauth';
// Create once (pass ctx.kv from the request context)
const tokenStore = new KeyValueTokenStorage(ctx.kv, {
namespace: 'oauth-tokens', // optional, this is the default
prefix: 'myapp:', // optional key prefix for multi-tenant scoping
config: { issuer: process.env.OAUTH_ISSUER }, // enables auto-refresh
});
// Store tokens after initial exchange
await tokenStore.set('user:123', tokenResponse);
// Retrieve: auto-refreshes if expired and a refresh_token exists
const token = await tokenStore.get('user:123');
if (token && isTokenExpired(token)) {
// Auto-refresh failed (no refresh_token, or provider rejected it)
// Redirect user to login again
}Tokens with a refresh_token persist until explicitly invalidated, because get() auto-refreshes them. Tokens without a refresh_token receive a KV TTL matching their expires_in value, so they clean up automatically.
Token Refresh
Auto-refresh happens transparently when you call tokenStore.get(). If you need to refresh manually (outside of storage), use refreshToken directly:
import { refreshToken } from '@agentuity/core/oauth';
const newToken = await refreshToken(previousToken.refresh_token);
// newToken contains a fresh access_token (and possibly a new refresh_token)Logout
Full logout involves two steps: revoke the token server-side, then remove it from storage.
// Revoke server-side and remove from storage in one call
const removed = await tokenStore.invalidate('user:123');
// `removed` is the token that was stored, or null if none existedinvalidate() revokes the refresh token (or access token as fallback) via the provider's revocation endpoint (RFC 7009), then deletes the entry from KV. Revocation is best-effort: the token is removed from storage even if the revocation request fails.
For standalone revocation without storage, use logout() directly:
import { logout } from '@agentuity/core/oauth';
await logout(refreshToken); // Calls the revocation endpointFor CLI-based OAuth app management (creating clients, rotating secrets, viewing activity), see OAuth Commands.
Next Steps
- React Frontend Auth: AuthProvider setup, useAuth hooks, and sign-in/sign-up forms
- ctx.auth Reference: Complete method signatures and return types
- CLI OAuth Commands: Create and manage OAuth/OIDC applications