Skip to main content
API

Authentication

VoltAgent supports optional authentication. For new integrations, use authNext, which treats all routes as private by default and separates console access from user access. The legacy auth option is still supported but deprecated.

Quick Start​

Option 1: No Authentication (Default)​

Use for development and internal tools:

import { VoltAgent } from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";

new VoltAgent({
agents: { myAgent },
server: honoServer(), // No auth configuration needed
});

All endpoints are publicly accessible.

Protect everything by default. Explicitly allow public routes, and use a Console Access Key for management endpoints.

Using Hono​

import { jwtAuth } from "@voltagent/server-core";
import { honoServer } from "@voltagent/server-hono";

new VoltAgent({
agents: { myAgent },
server: honoServer({
authNext: {
provider: jwtAuth({
secret: process.env.JWT_SECRET!,
}),
publicRoutes: ["GET /health"],
},
}),
});

Using Elysia​

import { jwtAuth } from "@voltagent/server-elysia";
import { elysiaServer } from "@voltagent/server-elysia";

new VoltAgent({
agents: { myAgent },
server: elysiaServer({
authNext: {
provider: jwtAuth({
secret: process.env.JWT_SECRET!,
}),
publicRoutes: ["GET /health"],
},
}),
});

Legacy auth is still supported for existing integrations. See the Legacy auth section at the end for details.

Concepts​

  • User token: A JWT from your identity provider, sent in Authorization: Bearer <token>.
  • Console access key: A static key for management, docs, observability, and updates endpoints. Set VOLTAGENT_CONSOLE_ACCESS_KEY and send x-console-access-key (or ?key= for WebSocket).
  • Public routes: Endpoints that bypass auth. You control them.
  • Dev bypass: x-voltagent-dev: true is accepted only when NODE_ENV is not "production".

How Authentication Works​

When auth is enabled, VoltAgent evaluates each request based on the configured mode.

authNext​

  1. Match publicRoutes (authNext + provider). If matched, request is public.
  2. Match consoleRoutes (authNext or defaults). If matched, require console key or dev bypass.
  3. Everything else requires a user token (JWT) or dev bypass.

Legacy auth​

  1. Routes in DEFAULT_LEGACY_PUBLIC_ROUTES (alias DEFAULT_PUBLIC_ROUTES) are always public.
  2. Routes in PROTECTED_ROUTES require a user token (JWT).
  3. defaultPrivate: true applies to custom routes only.

Route Access Summary​

Default access by endpoint group:

Endpoint GroupExamplesauthNext AccessLegacy auth Access
ExecutionPOST /agents/:id/text, POST /workflows/:id/run, POST /tools/:name/executeUser token (JWT)User token (JWT)
ManagementGET /agents, GET /workflows, GET /toolsConsole keyPublic
Docs + UIGET /, GET /doc, GET /uiConsole keyPublic
DiscoveryGET /mcp/servers, GET /agents/:id/cardConsole keyPublic
Observability/observability/*, GET /api/logs, WS /ws/observability/**Console keyConsole key or JWT
UpdatesGET /updates, POST /updates, POST /updates/:packageNameConsole keyConsole key or JWT
Custom routesGET /health, POST /webhooks/*User token (JWT) by defaultPublic by default

Notes:

  • WebSocket console endpoints require ?key=<key> or ?dev=true in non-production.
  • In legacy auth, defaultPrivate: true changes custom routes only; it does not change default public routes.

authNext is a policy layer that decides how each route is accessed:

  • public: No authentication required
  • console: Requires Console Access Key (or dev bypass)
  • user: Requires a valid user token (JWT)

Console routes cover management, docs, and observability endpoints used by the VoltAgent Console UI.

Access Resolution Order​

authNext resolves access in this order:

  1. public routes (from authNext.publicRoutes + provider.publicRoutes)
  2. console routes (from authNext.consoleRoutes or defaults)
  3. user routes (everything else)

If a route matches both public and console, public wins.

When using authNext, define public routes on authNext.publicRoutes. provider.publicRoutes is merged in for provider defaults.

Default Console Routes​

By default, authNext treats these as console routes (Console Key required):

Management

  • GET /agents
  • GET /agents/:id
  • GET /workflows
  • GET /workflows/:id
  • GET /tools
  • GET /agents/:id/history
  • GET /workflows/executions
  • GET /workflows/:id/executions/:executionId/state

Docs + Landing

  • GET /
  • GET /doc
  • GET /ui

Discovery

  • GET /agents/:id/card
  • GET /mcp/servers
  • GET /mcp/servers/:serverId
  • GET /mcp/servers/:serverId/tools

Observability + Updates

  • /observability/*
  • GET /api/logs
  • GET /updates
  • POST /updates
  • POST /updates/:packageName

WebSocket (Console Channels)

  • WS /ws
  • WS /ws/logs
  • WS /ws/observability/**

This list is defined in packages/server-core/src/auth/defaults.ts.

Route Pattern Syntax​

Route patterns support:

  • Method prefix: GET /agents/:id
  • Path params: /agents/:id
  • Single wildcard: /observability/* (matches /observability/x and deeper)
  • Double-star: /ws/observability/** (matches /ws/observability and children)

Making Routes Public​

Add routes to authNext.publicRoutes:

authNext: {
provider: jwtAuth({ secret: process.env.JWT_SECRET! }),
publicRoutes: [
"GET /health",
"POST /webhooks/*",
],
},

Custom Console Routes​

authNext.consoleRoutes replaces the default console list:

authNext: {
provider: jwtAuth({ secret: process.env.JWT_SECRET! }),
consoleRoutes: [
"/observability/*",
"GET /updates",
"POST /updates",
],
},

If you want the defaults plus custom routes, include the defaults explicitly.

import { DEFAULT_CONSOLE_ROUTES } from "@voltagent/server-core";

authNext: {
provider: jwtAuth({ secret: process.env.JWT_SECRET! }),
consoleRoutes: [...DEFAULT_CONSOLE_ROUTES, "GET /admin/metrics"],
},

Console Access Key​

In production, set a Console Access Key:

NODE_ENV=production
VOLTAGENT_CONSOLE_ACCESS_KEY=your-console-key

Provide the key via:

  • Header: x-console-access-key: <key>
  • Query: ?key=<key> (required for WebSocket)

Development Bypass​

In non-production, the dev bypass is allowed:

# HTTP requests
x-voltagent-dev: true

# WebSocket connections
?dev=true

This bypass works for both console and user routes.

Auth Providers​

VoltAgent includes a JWT provider and supports custom providers via the AuthProvider interface. Providers validate tokens and return a user object; role and tenant checks live in your handlers or middleware.

JWT (jwtAuth)​

jwtAuth can be used with authNext or legacy auth:

import { jwtAuth } from "@voltagent/server-core";

const provider = jwtAuth({
secret: process.env.JWT_SECRET!,
mapUser: (payload) => ({
id: payload.sub,
email: payload.email,
tenantId: payload.tenant_id,
role: payload.role,
}),
verifyOptions: {
algorithms: ["HS256"],
audience: "voltagent-api",
issuer: "my-auth-service",
},
});

Note: defaultPrivate only affects legacy auth. For authNext, use authNext.publicRoutes.

Custom Providers (AuthProvider)​

If you use a different identity system, implement AuthProvider and plug it into authNext:

import { VoltAgent } from "@voltagent/core";
import type { AuthProvider } from "@voltagent/server-core";
import { honoServer } from "@voltagent/server-hono";

const provider: AuthProvider = {
type: "my-provider",
async verifyToken(token, _request) {
// Validate token and return your user object
return { id: "user-id", role: "user" };
},
};

new VoltAgent({
agents: { myAgent },
server: honoServer({
authNext: { provider },
}),
});

Provider Recipes​

VoltAgent does not ship official packages for these providers yet, but you can wire them up by implementing AuthProvider. Use the examples below as starting points.

All examples assume:

import type { AuthProvider } from "@voltagent/server-core";

Auth0 (JWKS)​

Env:

  • AUTH0_DOMAIN
  • AUTH0_AUDIENCE
import { createRemoteJWKSet, jwtVerify } from "jose";

const domain = process.env.AUTH0_DOMAIN!;
const audience = process.env.AUTH0_AUDIENCE!;
const jwks = createRemoteJWKSet(new URL(`https://${domain}/.well-known/jwks.json`));

const provider: AuthProvider = {
type: "auth0",
async verifyToken(token) {
const { payload } = await jwtVerify(token, jwks, {
issuer: `https://${domain}/`,
audience,
});
return payload;
},
};

Clerk (JWKS)​

Env:

  • CLERK_JWKS_URI (from Clerk dashboard)
  • CLERK_SECRET_KEY
  • CLERK_PUBLISHABLE_KEY
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(new URL(process.env.CLERK_JWKS_URI!));

const provider: AuthProvider = {
type: "clerk",
async verifyToken(token) {
const { payload } = await jwtVerify(token, jwks);
return payload;
},
};

If you need organization membership or user lookups, use @clerk/backend with the secret and publishable keys.

WorkOS (JWKS)​

Env:

  • WORKOS_API_KEY
  • WORKOS_CLIENT_ID
import { WorkOS } from "@workos-inc/node";
import { createRemoteJWKSet, jwtVerify } from "jose";

const workos = new WorkOS(process.env.WORKOS_API_KEY!, {
clientId: process.env.WORKOS_CLIENT_ID!,
});
const jwksUrl = workos.userManagement.getJwksUrl(process.env.WORKOS_CLIENT_ID!);
const jwks = createRemoteJWKSet(new URL(jwksUrl));

const provider: AuthProvider = {
type: "workos",
async verifyToken(token) {
const { payload } = await jwtVerify(token, jwks);
return payload;
},
};

Firebase​

Env:

  • FIREBASE_SERVICE_ACCOUNT (path to service account JSON)
  • FIRESTORE_DATABASE_ID or FIREBASE_DATABASE_ID (optional)
import { initializeApp, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

const app = initializeApp({
credential: cert(process.env.FIREBASE_SERVICE_ACCOUNT!),
});

const provider: AuthProvider = {
type: "firebase",
async verifyToken(token) {
return getAuth(app).verifyIdToken(token);
},
};

Supabase​

Env:

  • SUPABASE_URL
  • SUPABASE_ANON_KEY
import { createClient } from "@supabase/supabase-js";

const client = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);

const provider: AuthProvider = {
type: "supabase",
async verifyToken(token) {
const { data, error } = await client.auth.getUser(token);
if (error) {
throw error;
}
return data.user;
},
};

Better Auth​

Env:

  • DATABASE_URL (or whatever your Better Auth setup requires)
import { betterAuth } from "better-auth";

const auth = betterAuth({
// Your Better Auth configuration
});

const provider: AuthProvider = {
type: "better-auth",
async verifyToken(token, request) {
const headers = new Headers();
const authHeader = request?.headers?.get("authorization");
headers.set("Authorization", authHeader ?? `Bearer ${token}`);

const cookie = request?.headers?.get("cookie");
if (cookie) {
headers.set("Cookie", cookie);
}

const result = await auth.api.getSession({ headers });
if (!result?.user) {
return null;
}

return result.user;
},
};

These providers authenticate user routes only. Console routes still use the Console Access Key.

WebSocket Authentication​

VoltAgent WebSocket endpoints are used by the Console (logs, observability). Browsers cannot send headers in the handshake, so use query params:

// Console auth (observability, logs)
const wsConsole = new WebSocket(`ws://localhost:3141/ws/observability?key=${consoleKey}`);

// Dev bypass (non-production only)
const wsDev = new WebSocket("ws://localhost:3141/ws/observability?dev=true");

If you expose custom WebSocket endpoints, include them in your authNext route patterns and use ?token= for user access or ?key= for console access.

Testing Your Authentication​

Generate a Test Token​

Create generate-token.js:

import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

const token = jwt.sign(
{
id: "test-user",
email: "test@example.com",
role: "admin",
},
process.env.JWT_SECRET,
{ expiresIn: "24h" }
);

console.log("Token:", token);

Test Protected Endpoints (authNext)​

# Execution requires JWT
curl -X POST http://localhost:3141/agents/my-agent/text \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"input": "Hello"}'

# Management requires Console Key
curl http://localhost:3141/agents \
-H "x-console-access-key: YOUR_CONSOLE_KEY"

Troubleshooting​

Console Shows 401 Errors​

Production:

export NODE_ENV=production
export VOLTAGENT_CONSOLE_ACCESS_KEY=your-key

Then provide the same key via header or ?key=.

Development:

# Ensure NODE_ENV is not "production"
unset NODE_ENV

WebSocket Connection Fails​

Common causes:

  1. Missing ?key= (console) or ?token= (custom user WS)
  2. NODE_ENV=production with dev bypass headers
  3. Using console key on user routes or JWT on console routes

Mixed Authentication Issues​

Remember the authNext split:

  • User routes (execution) -> JWT
  • Console routes (management, docs, observability, updates) -> Console Key
  • Public routes -> no auth

Security Best Practices​

1. Use Environment Variables​

// Good: Environment variable
const provider = jwtAuth({
secret: process.env.JWT_SECRET!,
});

2. Generate Strong Secrets​

openssl rand -hex 32

3. Use HTTPS in Production​

if (process.env.NODE_ENV === "production") {
app.use(async (c, next) => {
if (c.req.header("x-forwarded-proto") !== "https") {
return c.redirect(`https://${c.req.header("host")}${c.req.url}`);
}
await next();
});
}

Next Steps​

Legacy auth (Deprecated)​

Legacy auth uses two default lists:

  • DEFAULT_LEGACY_PUBLIC_ROUTES (alias DEFAULT_PUBLIC_ROUTES): management, docs, discovery
  • PROTECTED_ROUTES: execution, tool execution, observability, updates

When auth is enabled:

  • Execution endpoints require JWT
  • Management and docs remain public
  • defaultPrivate: true only protects custom/unknown routes, but does not override DEFAULT_LEGACY_PUBLIC_ROUTES (alias DEFAULT_PUBLIC_ROUTES)

If you need /agents, /workflows, /doc, or /ui protected, use authNext.

Legacy Behavior Summary​

Endpoint TypeLegacy Auth (Default)Legacy Auth (defaultPrivate: true)
Execution (POST /agents/*/text)ProtectedProtected
Management (GET /agents)PublicPublic
Docs (/doc, /ui)PublicPublic
Custom routesPublicProtected

Observability and updates still require Console Access Key in production.

Legacy Usage​

Minimal setup with JWT:

import { VoltAgent } from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";
import { jwtAuth } from "@voltagent/server-core";

new VoltAgent({
agents: { myAgent },
server: honoServer({
auth: jwtAuth({
secret: process.env.JWT_SECRET!,
}),
}),
});

Legacy Route Controls​

auth supports publicRoutes and defaultPrivate on the provider:

auth: jwtAuth({
secret: process.env.JWT_SECRET!,

// Protect all unknown/custom routes by default
defaultPrivate: true,

// Add additional public routes
publicRoutes: ["GET /health", "POST /webhooks/*"],
}),

Important behavior:

  • publicRoutes are added to the default public list
  • defaultPrivate: true does not make default public routes private
  • To protect management or docs endpoints, switch to authNext

Legacy Route Pattern Syntax​

Legacy route patterns use the same matcher as authNext:

  • GET /agents/:id
  • /observability/*
  • /ws/observability/**

Table of Contents