fastify-xauth-better
fastify-xauth-better
Production-ready Fastify plugin for Better Auth with multi-instance support, organizations, 2FA, audit logging, and email templates. Supports multiple simultaneous auth configurations (e.g., admin + user) each with independent cookie namespaces, route prefixes, and middleware.
Installation
npm install @xenterprises/fastify-xauth-better better-auth @prisma/client
Quick Start
import Fastify from "fastify";
import xAuthBetter from "@xenterprises/fastify-xauth-better";
import { PrismaClient } from "@prisma/client";
const fastify = Fastify();
const prisma = new PrismaClient();
await fastify.register(xAuthBetter, {
prisma,
configs: [
{
name: "user",
secret: process.env.AUTH_SECRET, // min 32 chars
baseURL: "http://localhost:3000",
basePath: "/api/auth",
prefix: "/api",
},
],
});
// Protected route — session validated automatically
fastify.get("/api/profile", async (request) => {
return { user: request.user };
});
await fastify.listen({ port: 3000 });
Options
Plugin Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
configs | XAuthBetterConfig[] | — | Yes | Array of auth instance configs (must be non-empty) |
prisma | PrismaClient | fastify.prisma | No | Prisma client; falls back to fastify.prisma decorator |
Instance Config (XAuthBetterConfig)
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name | string | — | Yes | Unique identifier for this auth instance |
secret | string | — | Yes | Auth secret, minimum 32 characters |
baseURL | string | — | Yes | Base URL for auth callbacks (must be a valid URL) |
basePath | string | /api/auth | No | Path prefix for Better Auth routes |
prefix | string | /api | No | Routes starting with this prefix are protected by auth middleware |
excludedPaths | Array | [] | No | Paths to skip auth middleware — string, RegExp, or { url, methods } |
roles | string[] | [] | No | Valid role names for this instance |
appName | string | App | No | Application name used in email templates |
trustedOrigins | string[] | [] | No | Trusted CORS origins |
databaseProvider | string | postgresql | No | Prisma database provider (postgresql, mysql, sqlite) |
requestProperty | string | auth | No | Request property name for the raw session object |
userProperty | string | user | No | Request property name for the user object |
emailAndPassword | object | { enabled: true } | No | Email/password auth settings |
socialProviders | object | {} | No | OAuth providers — google, facebook, github, microsoft |
organizations | object | { enabled: false } | No | Multi-tenant organization support |
twoFactor | object | { enabled: false } | No | 2FA settings — email, sms, totp |
magicLinks | object | { enabled: false } | No | Passwordless auth via magic links |
bearerTokens | object | { enabled: true } | No | API bearer token support |
admin | object | { enabled: true } | No | Admin plugin — impersonation and user management |
advanced | object | See below | No | Cookie, session, and rate limit settings |
templates | object | Built-in defaults | No | Email template overrides |
auditLog | object | { enabled: true } | No | Audit logging configuration |
extraOptions | object | {} | No | Pass-through to Better Auth config |
Advanced Options
| Name | Type | Default | Description |
|---|---|---|---|
advanced.cookiePrefix | string | {name}_auth | Cookie prefix — auto-generated from instance name |
advanced.useSecureCookies | boolean | true in production | Use secure cookies |
advanced.crossSubDomainCookies | boolean | false | Share cookies across subdomains |
advanced.session.expiresIn | number | 604800 | Session TTL in seconds (7 days) |
advanced.session.updateAge | number | 86400 | Session refresh interval in seconds (1 day) |
Audit Log Options
| Name | Type | Default | Description |
|---|---|---|---|
auditLog.enabled | boolean | true | Enable audit logging |
auditLog.events | string[] | All 14 events | Events to capture |
auditLog.retention | number | 365 | Retention in days |
auditLog.captureIp | boolean | true | Capture client IP |
auditLog.captureUserAgent | boolean | true | Capture user agent |
Methods
fastify.xauthbetter decorator
| Property | Type | Description |
|---|---|---|
get(name) | (name: string) => XAuthBetterInstance | Get a specific auth instance by name |
default | XAuthBetterInstance | First registered instance |
configs | Record<string, XAuthBetterInstance> | All registered instances |
pruneAuditLogs(options?) | (options?) => Promise<{ count: number, deleted: boolean }> | Delete old audit log entries |
Instance methods
Each instance returned by fastify.xauthbetter.get(name) or .default exposes:
- requireAuth() — Returns an auth-required preHandler middleware
- requireRole(roles) — Returns a global role-based access control preHandler
- requireOrg() — Returns org-membership-required preHandler; populates
request.organization - requireOrgRole(roles) — Returns an org-scoped role preHandler (requires
requireOrg()first) - getSession(request) — Resolves session from headers/cookies without middleware
- auditLog.log(event, data) — Writes an audit event to
AuthAuditLog - pruneAuditLogs(options) — Deletes old audit log records
Request Properties
When prefix is configured, the auth middleware sets:
| Property | Description |
|---|---|
request.user | Authenticated user object |
request.auth | Raw session object { session, user } |
request.organization | Organization context (only when requireOrg() runs first) |
Multi-Instance Setup
await fastify.register(xAuthBetter, {
prisma,
configs: [
{
name: "admin",
secret: process.env.ADMIN_SECRET,
baseURL: "http://localhost:3000",
basePath: "/api/auth/admin",
prefix: "/api/admin",
roles: ["superadmin", "admin"],
},
{
name: "user",
secret: process.env.USER_SECRET,
baseURL: "http://localhost:3000",
basePath: "/api/auth/user",
prefix: "/api/user",
roles: ["contractor", "homeowner"],
},
],
});
const adminAuth = fastify.xauthbetter.get("admin");
const userAuth = fastify.xauthbetter.get("user");
Email Templates
6 built-in templates with {{variable}} substitution:
| Template | Variables | Description |
|---|---|---|
verification | userName, url, appName | Email verification link |
passwordReset | userName, url, appName | Password reset link |
magicLink | userName, url, appName | Passwordless sign-in link |
twoFactorOTP | userName, code, appName | 2FA verification code |
orgInvite | userName, orgName, inviterName, url, appName | Org invitation |
accountLinked | userName, appName | Account linked notification |
Email delivery requires @xenterprises/fastify-xemail or the email-outbox plugin to be registered.
Error Reference
| Error | Cause |
|---|---|
xAuthBetter: "configs" must be a non-empty array | configs missing or empty |
xAuthBetter: Prisma client is required | No prisma in options and no fastify.prisma decorator |
xAuthBetter: Config "name" is required | Instance config missing name |
xAuthBetter: 'secret' is required and must be a string | Missing secret |
xAuthBetter: 'secret' must be at least 32 characters long | secret shorter than 32 chars |
xAuthBetter: 'baseURL' must be a valid URL | Invalid baseURL |
xAuthBetter: Duplicate instance name "…" | Two configs share the same name |
xAuthBetter: Duplicate basePath "…" | Two configs share the same basePath |
xAuthBetter: Duplicate cookiePrefix "…" | Two configs share the same cookie prefix |
xAuthBetter [name]: 2FA SMS is enabled but xTwilio plugin is not registered | SMS 2FA enabled without Twilio plugin |
Invalid audit event: {event} | Logging an unrecognized event name |
Environment Variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Prisma connection string |
NODE_ENV | No | Set to production to enable secure cookies |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | If using Google OAuth | Google OAuth credentials |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | If using GitHub OAuth | GitHub OAuth credentials |
Auth secret values must be passed explicitly in config — they are not read from environment variables.
How It Works
On registration each config is validated and merged with defaults, then a Better Auth instance is created with a Prisma adapter and the configured plugins (admin, bearer, 2FA, magic links, organizations). A catch-all Fastify route at basePath/* bridges Fastify request/reply to the Web API Request/Response format expected by Better Auth's handler. When prefix is set, an onRequest hook validates sessions for all matching routes (skipping basePath and any excludedPaths), then attaches request.user, request.auth, and optionally request.organization for downstream handlers. All instances are exposed via fastify.xauthbetter for programmatic access to middleware factories, audit logging, and session utilities.
AI Context
package: "@xenterprises/fastify-xauth-better"
type: fastify-plugin
use-when: Production auth with Better Auth — multi-instance, organizations, 2FA, magic links, audit logging, social OAuth
decorator: fastify.xauthbetter (get, default, configs, pruneAuditLogs)
request-decorators: request.user, request.auth, request.organization
requires: Prisma client with Better Auth schema, secret ≥ 32 chars per config
env: DATABASE_URL, NODE_ENV, GOOGLE_CLIENT_ID/SECRET, GITHUB_CLIENT_ID/SECRET (optional)
multi-instance: each configs[] entry gets its own auth instance, route prefix, and cookie namespace
