Authentication
VoltAgent supports optional authentication to secure your AI agents and workflows. You can run without authentication, use simple JWT tokens, or implement complex auth flows - the choice is yours.
Getting Started
Option 1: No Authentication (Default)
Perfect 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. This is the simplest way to get started.
Option 2: Basic JWT Authentication
Protect execution endpoints while keeping management endpoints public:
import { jwtAuth } from "@voltagent/server-core";
new VoltAgent({
agents: { myAgent },
server: honoServer({
auth: jwtAuth({
secret: process.env.JWT_SECRET!, // Your JWT secret key
}),
}),
});
With this setup:
- ✅ Agent/workflow execution endpoints require JWT token
- ✅ Management endpoints (list agents, etc.) remain public
- ✅ Documentation endpoints remain public
Option 3: Protect Everything (Recommended for Production)
Make all routes private by default, then explicitly make some public:
new VoltAgent({
agents: { myAgent },
server: honoServer({
auth: jwtAuth({
secret: process.env.JWT_SECRET!,
defaultPrivate: true, // Protect all routes
publicRoutes: [
// Except these
"GET /health",
"GET /",
"POST /webhooks/*",
],
}),
}),
});
Environment Variables
# Required for JWT authentication
JWT_SECRET=your-secret-key-here # Generate with: openssl rand -hex 32
# Required for Console in production
VOLTAGENT_CONSOLE_ACCESS_KEY=your-console-key-here # Generate with: openssl rand -hex 32
NODE_ENV=production # Set to enable Console authentication
Common Use Cases
Public API with Protected Admin Routes
Most endpoints are public, only admin operations require auth:
auth: jwtAuth({
secret: process.env.JWT_SECRET,
// defaultPrivate: false (default - only execution endpoints protected)
publicRoutes: [
"GET /api/public/*", // Additional public routes
],
});
Private API with Public Health Check
Everything requires auth except health monitoring:
auth: jwtAuth({
secret: process.env.JWT_SECRET,
defaultPrivate: true, // Everything protected
publicRoutes: [
"GET /health", // Health check for load balancers
"GET /metrics", // Metrics for monitoring
],
});
Multi-Tenant SaaS Application
Extract tenant information from JWT tokens:
auth: jwtAuth({
secret: process.env.JWT_SECRET,
defaultPrivate: true,
// Transform JWT payload to your user model
mapUser: (payload) => ({
id: payload.sub,
tenantId: payload.tenant_id,
email: payload.email,
role: payload.role,
}),
});
Then access tenant info in your agents:
const agent = new Agent({
name: "TenantAgent",
hooks: {
onStart: async ({ context }) => {
const user = context.context.get("user");
const tenantId = user?.tenantId;
// Filter data by tenant
},
},
});
How Authentication Works
What Gets Protected?
When you enable authentication with default settings:
| Endpoint Type | No Auth | With Auth (Default) | With Auth (defaultPrivate: true) |
|---|---|---|---|
Execution (POST /agents/*/text) | Public | Protected | Protected |
Management (GET /agents) | Public | Public | Protected |
Documentation (/doc, /ui) | Public | Public | Protected |
| Your Custom Routes | Public | Public | Protected |
User Context in Agents
When a request is authenticated, user information is automatically available:
const agent = new Agent({
name: "MyAgent",
// Dynamic instructions based on user
instructions: ({ context }) => {
const user = context?.get("user");
if (user?.role === "admin") {
return "You have admin privileges...";
}
return "You are a standard user...";
},
// Access user in hooks
hooks: {
onStart: async ({ context }) => {
const user = context.context.get("user");
console.log("Request from:", user?.email);
},
},
});
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);
console.log("\nTest with curl:");
console.log(`curl -H "Authorization: Bearer ${token}" http://localhost:3141/agents/my-agent/text`);
Test Protected Endpoints
# Without token (will fail with 401)
curl -X POST http://localhost:3141/agents/my-agent/text \
-H "Content-Type: application/json" \
-d '{"input": "Hello"}'
# With token (will succeed)
curl -X POST http://localhost:3141/agents/my-agent/text \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"input": "Hello"}'
Advanced Configuration
Custom User Mapping
Transform JWT payload into your application's user model:
auth: jwtAuth({
secret: process.env.JWT_SECRET,
mapUser: (payload) => ({
// Map JWT claims to your user object
id: payload.sub || payload.user_id,
email: payload.email,
name: payload.name || payload.given_name,
// Add custom fields
tenantId: payload.tenant_id || payload.org_id,
permissions: payload.permissions || [],
tier: payload.subscription_tier || "free",
// Add computed properties
isAdmin: payload.role === "admin" || payload.admin === true,
canAccessPremiumFeatures: ["premium", "enterprise"].includes(payload.tier),
}),
});
JWT Verification Options
Configure how JWT tokens are verified:
auth: jwtAuth({
secret: process.env.JWT_SECRET,
verifyOptions: {
// Accepted signing algorithms
algorithms: ["HS256", "RS256"],
// Token audience (aud claim)
audience: "https://api.example.com",
// Token issuer (iss claim)
issuer: "https://auth.example.com",
},
});
Using RS256 (Public Key)
For tokens signed with RS256:
import fs from "fs";
auth: jwtAuth({
secret: fs.readFileSync("public-key.pem"),
verifyOptions: {
algorithms: ["RS256"],
},
});
Complete Configuration Example
All options together:
auth: jwtAuth({
// JWT secret or public key
secret: process.env.JWT_SECRET!,
// Protect all routes by default
defaultPrivate: true,
// Routes that don't require auth
publicRoutes: ["GET /health", "GET /", "POST /webhooks/*", "GET /api/public/*"],
// Transform JWT to user object
mapUser: (payload) => ({
id: payload.sub,
email: payload.email,
tenantId: payload.tenant_id,
permissions: payload.permissions || [],
}),
// JWT verification settings
verifyOptions: {
algorithms: ["HS256"],
audience: "voltagent-api",
issuer: "my-auth-service",
},
});
Console & Observability Authentication
VoltAgent Console uses a separate authentication system for observability endpoints.
Understanding Dual Authentication
VoltAgent uses two independent auth systems:
- End-User Auth (JWT): For your application's users accessing agents/workflows
- Console Auth: For developers accessing the observability dashboard
Developer Mode
In development (NODE_ENV !== "production"), the Console works automatically:
# Console connects without authentication
npm run dev # NODE_ENV is not "production"
The Console sends x-voltagent-dev: true header which is accepted in development mode.
Production Mode
In production, set a Console Access Key:
# Server environment variables
NODE_ENV=production
VOLTAGENT_CONSOLE_ACCESS_KEY=your-secure-key-here
When accessing the Console:
- Console detects 401 error
- Prompts for access key
- Stores key locally
- Automatically retries requests
WebSocket Authentication
Browsers cannot send headers during WebSocket handshake, so use query parameters:
// User authentication (JWT)
const token = "your-jwt-token";
const ws = new WebSocket(`ws://localhost:3141/ws?token=${token}`);
// Console authentication (development)
const ws = new WebSocket("ws://localhost:3141/ws/observability?dev=true");
// Console authentication (production)
const key = localStorage.getItem("voltagent_console_access_key");
const ws = new WebSocket(`ws://localhost:3141/ws/observability?key=${key}`);
API Reference
Protected Routes
When authentication is configured, these endpoints require valid tokens:
Agent Execution
POST /agents/:id/text- Generate textPOST /agents/:id/stream- Stream textPOST /agents/:id/chat- Chat streamPOST /agents/:id/object- Generate objectPOST /agents/:id/stream-object- Stream object
Workflow Execution
POST /workflows/:id/run- Run workflowPOST /workflows/:id/stream- Stream workflowPOST /workflows/:id/executions/:executionId/suspend- SuspendPOST /workflows/:id/executions/:executionId/resume- ResumePOST /workflows/:id/executions/:executionId/cancel- Cancel
Observability (Console Auth)
GET /observability/*- All observability endpointsWS /ws/observability- Real-time observability
Public Routes (Default)
These remain public unless defaultPrivate: true:
Management
GET /agents- List agentsGET /agents/:id- Get agent detailsGET /workflows- List workflowsGET /workflows/:id- Get workflow details
Documentation
GET /- Landing pageGET /doc- OpenAPI specGET /ui- Swagger UI
Discovery
GET /agents/:id/card- Agent card (A2A)GET /mcp/servers- MCP serversGET /mcp/servers/:id- MCP server details
Error Responses
Authentication failures return consistent JSON errors:
// No token provided
{
"success": false,
"error": "Authentication required"
}
// Invalid token
{
"success": false,
"error": "Invalid token: jwt malformed"
}
// Expired token
{
"success": false,
"error": "Token expired"
}
Troubleshooting
Console Shows 401 Errors
Problem: VoltAgent Console displays authentication errors.
Solution for Development:
# Ensure NODE_ENV is not "production"
unset NODE_ENV
# or
NODE_ENV=development npm run dev
Solution for Production:
# Set Console Access Key on server
export VOLTAGENT_CONSOLE_ACCESS_KEY=your-key-here
export NODE_ENV=production
# Console will prompt for the key - enter the same value
WebSocket Connection Fails
Problem: WebSocket connections are rejected with 401.
Common Causes:
- Missing token in query params - Browsers can't send headers in WebSocket handshake
- Expired JWT token - Generate a new token
- Wrong authentication method - Use JWT for user endpoints, Console Key for observability
Solution:
// Correct: Token in query params
const ws = new WebSocket(`ws://localhost:3141/ws?token=${token}`);
// Wrong: Trying to send headers (doesn't work in browsers)
const ws = new WebSocket("ws://localhost:3141/ws", {
headers: { Authorization: `Bearer ${token}` }, // ❌ Won't work
});
Mixed Authentication Issues
Problem: Some endpoints work, others return 401.
Remember the dual authentication:
- User endpoints (
/agents/*/text,/workflows/*/run) → JWT token - Observability (
/observability/*) → Console Access Key or dev bypass - WebSockets → Query parameters for both types
Console Access Key Not Working
Problem: Entered key but still getting 401.
Checklist:
-
Verify server has the key:
echo $VOLTAGENT_CONSOLE_ACCESS_KEY -
Check NODE_ENV:
echo $NODE_ENV # Should be "production" if using key -
Clear browser storage:
- Open DevTools → Application → Local Storage
- Delete
voltagent_console_access_key - Refresh and re-enter key
-
Verify key format (no extra spaces):
// Correct
VOLTAGENT_CONSOLE_ACCESS_KEY = abc123;
// Wrong (has quotes)
VOLTAGENT_CONSOLE_ACCESS_KEY = "abc123";
Security Best Practices
1. Use Environment Variables
// ❌ Bad: Hardcoded secret
const auth = jwtAuth({
secret: "my-secret-key",
});
// ✅ Good: Environment variable
const auth = jwtAuth({
secret: process.env.JWT_SECRET!,
});
// ✅ Better: With validation
if (!process.env.JWT_SECRET) {
throw new Error("JWT_SECRET environment variable is required");
}
const auth = jwtAuth({
secret: process.env.JWT_SECRET,
});
2. Generate Strong Secrets
# Generate secure random keys
openssl rand -hex 32
# Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
3. Use HTTPS in Production
// Enforce 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();
});
}
4. Implement Token Refresh
// Short-lived access tokens with refresh tokens
const accessToken = createJWT(payload, secret, { expiresIn: "15m" });
const refreshToken = createJWT(payload, refreshSecret, { expiresIn: "7d" });
// Add refresh endpoint to publicRoutes
publicRoutes: ["POST /auth/refresh"];
5. Rate Limiting
import { rateLimiter } from "hono-rate-limiter";
server: honoServer({
configureApp: (app) => {
app.use(
"/agents/*/text",
rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Max 100 requests
standardHeaders: "draft-6",
keyGenerator: (c) => c.req.header("x-forwarded-for") || "anonymous",
})
);
},
auth,
});
Next Steps
- Learn about Custom Endpoints with authentication
- Explore Streaming with authenticated connections
- Read about Agent Endpoints in detail
- Set up Observability with Console authentication