Authentication Adapters
Velox TS provides a pluggable authentication system that integrates with popular authentication providers through adapters. This allows you to use services like Clerk, Auth0, or Better Auth without writing custom authentication logic.
Available Adapters
Section titled “Available Adapters”| Adapter | Provider | Use Case |
|---|---|---|
| JWT Adapter | Built-in | Custom JWT authentication |
| Clerk Adapter | Clerk | Complete user management platform |
| Auth0 Adapter | Auth0 | Enterprise identity platform |
| Better Auth Adapter | Better Auth | Open-source authentication library |
Core Concepts
Section titled “Core Concepts”Adapter Pattern
Section titled “Adapter Pattern”All adapters implement the same interface, providing:
- Session verification - Validate authentication tokens/cookies
- User loading - Transform provider user data to Velox TS format
- Context integration - Automatically inject
ctx.userandctx.session
How It Works
Section titled “How It Works”- Request arrives with authentication credentials (JWT, cookie, etc.)
- Adapter verifies credentials with the provider
- User and session data are loaded and transformed
- Data is injected into procedure context as
ctx.userandctx.session - Guards and procedures can access authenticated user data
// After adapter processes requestprocedure() .guard(authenticated) .query(({ ctx }) => { // ctx.user - User object from adapter // ctx.session - Session info from adapter return { email: ctx.user.email }; })Clerk Adapter
Section titled “Clerk Adapter”Integrate Clerk - a complete user management platform with authentication, user profiles, and organization management.
Installation
Section titled “Installation”npm install @clerk/backendClerk adapter is a peer dependency - you must install @clerk/backend separately.
Configuration
Section titled “Configuration”import { createAuthAdapterPlugin } from '@veloxts/auth';import { createClerkAdapter } from '@veloxts/auth/adapters';import { createClerkClient } from '@clerk/backend';
// Create Clerk clientconst clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY!, publishableKey: process.env.CLERK_PUBLISHABLE_KEY,});
// Create adapterconst adapter = createClerkAdapter({ clerk, authorizedParties: ['http://localhost:3000', 'https://myapp.com'], fetchUserData: true, // Fetch full user profile from Clerk API debug: process.env.NODE_ENV === 'development',});
// Create and register pluginapp.register(createAuthAdapterPlugin(adapter));Configuration Options
Section titled “Configuration Options”interface ClerkAdapterConfig { /** Clerk client instance from @clerk/backend */ clerk: ClerkClient;
/** Authorized origins for token verification */ authorizedParties?: string[];
/** Expected audience claim (optional) */ audiences?: string | string[];
/** Clock tolerance in seconds (default: 5) */ clockTolerance?: number;
/** Custom authorization header name (default: 'authorization') */ authHeader?: string;
/** Fetch full user data from Clerk API (default: true) */ fetchUserData?: boolean;
/** Enable debug logging */ debug?: boolean;}How It Works
Section titled “How It Works”- Extracts JWT from
Authorization: Bearer <token>header - Verifies token with Clerk’s backend SDK
- Optionally fetches full user profile from Clerk API
- Transforms Clerk user/session to Velox TS format
- Injects into
ctx.userandctx.session
Available User Data
Section titled “Available User Data”procedure() .guard(authenticated) .query(({ ctx }) => { // Standard fields ctx.user.id; // Clerk user ID ctx.user.email; // Primary email ctx.user.name; // Full name ctx.user.image; // Profile picture URL ctx.user.emailVerified; // Email verification status
// Provider-specific data ctx.user.providerData.organizationId; // Clerk org ID ctx.user.providerData.organizationRole; // Org role ctx.user.providerData.organizationSlug; // Org slug ctx.user.providerData.organizationPermissions; // Org permissions ctx.user.providerData.publicMetadata; // Public metadata ctx.user.providerData.privateMetadata; // Private metadata
// Session info ctx.session.sessionId; // Clerk session ID ctx.session.expiresAt; // Expiration timestamp })Organization Support
Section titled “Organization Support”Clerk adapter automatically includes organization data if the token contains organization claims:
procedure() .guard(authenticated) .query(({ ctx }) => { const orgId = ctx.user.providerData.organizationId; const orgRole = ctx.user.providerData.organizationRole; const orgPermissions = ctx.user.providerData.organizationPermissions;
if (orgPermissions?.includes('org:invoices:create')) { // User has permission to create invoices } })Disabling User Data Fetching
Section titled “Disabling User Data Fetching”For performance, you can skip fetching full user data and use only JWT claims:
const adapter = createClerkAdapter({ clerk, fetchUserData: false, // Only use data from JWT claims});This reduces API calls but limits available user data to what’s in the token.
Auth0 Adapter
Section titled “Auth0 Adapter”Integrate Auth0 - an enterprise identity platform with extensive authentication features and compliance certifications.
Installation
Section titled “Installation”No additional dependencies required. The adapter uses Web Crypto API for JWT verification.
Configuration
Section titled “Configuration”import { createAuthAdapterPlugin } from '@veloxts/auth';import { createAuth0Adapter } from '@veloxts/auth/adapters';
const adapter = createAuth0Adapter({ domain: process.env.AUTH0_DOMAIN!, // e.g., 'your-tenant.auth0.com' audience: process.env.AUTH0_AUDIENCE!, // Your API identifier clientId: process.env.AUTH0_CLIENT_ID, // Optional: validate azp claim debug: process.env.NODE_ENV === 'development',});
app.register(createAuthAdapterPlugin(adapter));Configuration Options
Section titled “Configuration Options”interface Auth0AdapterConfig { /** Auth0 tenant domain (e.g., 'your-tenant.auth0.com') */ domain: string;
/** API audience identifier (must match Auth0 API settings) */ audience: string;
/** Client ID (optional, validates azp claim if provided) */ clientId?: string;
/** Custom JWT verifier implementation (optional) */ jwtVerifier?: JwtVerifier;
/** JWKS cache TTL in ms (default: 3600000 = 1 hour) */ jwksCacheTtl?: number;
/** Clock tolerance in seconds (default: 5) */ clockTolerance?: number;
/** Custom authorization header name (default: 'authorization') */ authHeader?: string;
/** Token issuer override (default: https://{domain}/) */ issuer?: string;
/** Enable debug logging */ debug?: boolean;}How It Works
Section titled “How It Works”- Extracts JWT from
Authorization: Bearer <token>header - Fetches JWKS (JSON Web Key Set) from Auth0
- Verifies JWT signature using JWKS public keys
- Validates claims (issuer, audience, expiration)
- Transforms Auth0 claims to Velox TS format
- Injects into
ctx.userandctx.session
JWKS Caching
Section titled “JWKS Caching”The adapter caches JWKS keys to avoid fetching them on every request:
- Cache TTL: 1 hour (default) - Keys are refreshed after expiration
- Rate Limiting: Minimum 5 seconds between refresh attempts
- Promise Locking: Concurrent requests share the same refresh promise
const adapter = createAuth0Adapter({ domain: 'your-tenant.auth0.com', audience: 'https://your-api.example.com', jwksCacheTtl: 7200000, // Cache for 2 hours});Available User Data
Section titled “Available User Data”procedure() .guard(authenticated) .query(({ ctx }) => { // Standard fields ctx.user.id; // Auth0 user ID (sub claim) ctx.user.email; // Email from token ctx.user.name; // Display name ctx.user.image; // Profile picture URL ctx.user.emailVerified; // Email verification status
// Provider-specific data ctx.user.providerData.scope; // Token scopes ctx.user.providerData.permissions; // RBAC permissions ctx.user.providerData.organizationId; // Auth0 org ID ctx.user.providerData.organizationName; // Auth0 org name
// Session info ctx.session.sessionId; // Auth0 session ID ctx.session.expiresAt; // Token expiration ctx.session.providerData.issuedAt; // Token issued at ctx.session.providerData.notBefore; // Token not before })RBAC and Permissions
Section titled “RBAC and Permissions”Auth0 adapter supports Role-Based Access Control (RBAC):
import { hasPermission } from '@veloxts/auth';
deleteInvoice: procedure() .guard(hasPermission('delete:invoices')) .mutation(async ({ input, ctx }) => { // User has 'delete:invoices' permission from Auth0 return await ctx.db.invoice.delete({ where: { id: input.id } }); }),Permissions are extracted from the permissions claim in the JWT.
Organization Support
Section titled “Organization Support”Auth0 Organizations (part of Auth0 B2B features) are automatically included:
procedure() .guard(authenticated) .query(({ ctx }) => { const orgId = ctx.user.providerData.organizationId; const orgName = ctx.user.providerData.organizationName;
// Filter data by organization return await ctx.db.resource.findMany({ where: { organizationId: orgId }, }); })Custom Domain Support
Section titled “Custom Domain Support”If you use a custom domain with Auth0, specify it explicitly:
const adapter = createAuth0Adapter({ domain: 'auth.myapp.com', // Custom domain audience: 'https://api.myapp.com', issuer: 'https://auth.myapp.com/', // Optional: override issuer});Better Auth Adapter
Section titled “Better Auth Adapter”Integrate Better Auth - an open-source, lightweight authentication library for Node.js.
Installation
Section titled “Installation”npm install better-authConfiguration
Section titled “Configuration”import { betterAuth } from 'better-auth';import { createAuthAdapterPlugin } from '@veloxts/auth';import { createBetterAuthAdapter } from '@veloxts/auth/adapters';
// Create Better Auth instanceconst auth = betterAuth({ database: prisma, emailAndPassword: { enabled: true }, socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }, },});
// Create adapterconst adapter = createBetterAuthAdapter({ auth, debug: process.env.NODE_ENV === 'development',});
// Create and register pluginapp.register(createAuthAdapterPlugin(adapter));How It Works
Section titled “How It Works”- Reads session cookie set by Better Auth
- Calls Better Auth’s session validation API
- Transforms Better Auth user/session to Velox TS format
- Injects into
ctx.userandctx.session
Available User Data
Section titled “Available User Data”procedure() .guard(authenticated) .query(({ ctx }) => { // Standard fields ctx.user.id; // Better Auth user ID ctx.user.email; // User email ctx.user.name; // Display name ctx.user.emailVerified; // Email verification status
// Session info ctx.session.sessionId; // Better Auth session ID ctx.session.expiresAt; // Session expiration })Using Adapters with Guards
Section titled “Using Adapters with Guards”All adapters work seamlessly with Velox TS guards:
import { authenticated, hasRole, hasPermission } from '@veloxts/auth';
// Require authenticationgetProfile: procedure() .guard(authenticated) .query(({ ctx }) => ctx.user),
// Require specific roleadminDashboard: procedure() .guard(authenticated) .guard(hasRole('admin')) .query(({ ctx }) => { /* ... */ }),
// Require permission (Auth0/Clerk)deleteUser: procedure() .guard(hasPermission('users:delete')) .mutation(({ input, ctx }) => { /* ... */ }),Guards automatically work with adapter-provided user/session data.
Creating Custom Adapters
Section titled “Creating Custom Adapters”You can create custom adapters by extending BaseAuthAdapter:
import { BaseAuthAdapter, type AuthAdapterConfig } from '@veloxts/auth';import type { FastifyRequest } from 'fastify';
interface MyAdapterConfig extends AuthAdapterConfig { apiKey: string;}
class MyCustomAdapter extends BaseAuthAdapter<MyAdapterConfig> { private apiKey: string | null = null;
constructor() { super('my-adapter', '1.0.0'); }
async initialize(fastify: FastifyInstance, config: MyAdapterConfig): Promise<void> { await super.initialize(fastify, config); this.apiKey = config.apiKey; }
async getSession(request: FastifyRequest): Promise<AdapterSessionResult | null> { // Extract credentials from request const token = request.headers.authorization?.replace('Bearer ', ''); if (!token) return null;
// Verify with your auth provider const userData = await verifyWithMyProvider(token, this.apiKey); if (!userData) return null;
// Transform to Velox TS format return { user: { id: userData.id, email: userData.email, name: userData.name, emailVerified: userData.emailVerified, }, session: { sessionId: userData.sessionId, userId: userData.id, expiresAt: userData.expiresAt, isActive: true, }, }; }
getRoutes(): AdapterRoute[] { return []; // Return auth routes if needed }}
export function createMyCustomAdapter(config: MyAdapterConfig) { const adapter = new MyCustomAdapter(); return Object.assign(adapter, { config });}Then use it like any other adapter:
const adapter = createMyCustomAdapter({ apiKey: process.env.MY_API_KEY!,});
app.register(createAuthAdapterPlugin(adapter));Migration from JWT Manager
Section titled “Migration from JWT Manager”If you’re using the legacy jwtManager API, migrate to the JWT adapter:
import { authPlugin, jwtManager } from '@veloxts/auth';
const jwt = jwtManager({ secret: process.env.JWT_SECRET!, refreshSecret: process.env.JWT_REFRESH_SECRET!, accessTokenExpiry: '15m', refreshTokenExpiry: '7d',});
await app.register(authPlugin, { jwt });import { createAuthAdapterPlugin } from '@veloxts/auth';import { createJwtAdapter } from '@veloxts/auth/adapters';
const adapter = createJwtAdapter({ jwt: { secret: process.env.JWT_SECRET!, refreshSecret: process.env.JWT_REFRESH_SECRET!, accessTokenExpiry: '15m', refreshTokenExpiry: '7d', }, userLoader: async (userId) => { return await db.user.findUnique({ where: { id: userId } }); },});
app.register(createAuthAdapterPlugin(adapter));The adapter pattern provides better extensibility and consistency across providers.
Best Practices
Section titled “Best Practices”Environment Variables
Section titled “Environment Variables”Store provider secrets in environment variables:
CLERK_SECRET_KEY=sk_live_...CLERK_PUBLISHABLE_KEY=pk_live_...
AUTH0_DOMAIN=your-tenant.auth0.comAUTH0_AUDIENCE=https://api.yourapp.comAUTH0_CLIENT_ID=your_client_idNever commit secrets to version control.
Debug Mode
Section titled “Debug Mode”Enable debug logging in development to troubleshoot authentication issues:
const adapter = createClerkAdapter({ clerk, debug: process.env.NODE_ENV === 'development',});Debug logs show token verification, user loading, and error details.
Token Expiration
Section titled “Token Expiration”Configure appropriate token expiration times:
- Short-lived access tokens (15-60 minutes) - Reduce risk of token theft
- Longer refresh tokens (7-30 days) - Balance security with UX
- Session-based adapters (Clerk, Auth0) - Handle expiration automatically
Performance Optimization
Section titled “Performance Optimization”For high-traffic APIs:
-
Disable unnecessary data fetching:
createClerkAdapter({ fetchUserData: false }) -
Adjust JWKS cache TTL:
createAuth0Adapter({ jwksCacheTtl: 7200000 }) // 2 hours -
Use connection pooling for database user lookups
Error Handling
Section titled “Error Handling”Adapters return null for invalid/missing authentication. Use guards to enforce authentication:
// Returns null if not authenticated - guard handles itprocedure() .guard(authenticated) .query(({ ctx }) => { // ctx.user is guaranteed to exist here return ctx.user; })Don’t check ctx.user manually - let guards handle authentication requirements.
Troubleshooting
Section titled “Troubleshooting”Token verification fails (Clerk)
Section titled “Token verification fails (Clerk)”Cause: Incorrect authorizedParties or token issued for wrong audience
Fix: Ensure authorizedParties matches your frontend origin:
createClerkAdapter({ clerk, authorizedParties: ['http://localhost:3000'], // Must match frontend})Token verification fails (Auth0)
Section titled “Token verification fails (Auth0)”Cause: Incorrect audience or JWKS fetch failure
Fix:
- Verify
audiencematches your Auth0 API identifier - Check that Auth0 domain is correct
- Enable debug mode to see JWKS fetch errors
createAuth0Adapter({ domain: 'your-tenant.auth0.com', // Verify this is correct audience: 'https://your-api.example.com', // Must match Auth0 API debug: true,})User data missing
Section titled “User data missing”Cause: Token doesn’t include expected claims
Fix: Configure your provider to include required claims in tokens:
- Clerk: Enable user profile in token settings
- Auth0: Add custom claims via Auth0 Actions
- Better Auth: Ensure session includes user data
Clock skew errors
Section titled “Clock skew errors”Cause: Server time differs from provider time
Fix: Increase clock tolerance:
// Clerk (seconds)createClerkAdapter({ clerk, clockTolerance: 10 }) // 10 seconds
// Auth0 (seconds)createAuth0Adapter({ domain: '...', audience: '...', clockTolerance: 10 }) // 10 secondsOr synchronize your server’s clock using NTP.
Related Content
Section titled “Related Content”- Guards - Protect procedures with authorization checks
- JWT Authentication - Built-in JWT authentication
- Sessions - Cookie-based authentication
- Rate Limiting - Protect against abuse