X Enterprises

fastify-xauth-better

Production-ready Better Auth plugin for Fastify with multi-instance support, organizations, 2FA, audit logging, and email templates.

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

NameTypeDefaultRequiredDescription
configsXAuthBetterConfig[]YesArray of auth instance configs (must be non-empty)
prismaPrismaClientfastify.prismaNoPrisma client; falls back to fastify.prisma decorator

Instance Config (XAuthBetterConfig)

NameTypeDefaultRequiredDescription
namestringYesUnique identifier for this auth instance
secretstringYesAuth secret, minimum 32 characters
baseURLstringYesBase URL for auth callbacks (must be a valid URL)
basePathstring/api/authNoPath prefix for Better Auth routes
prefixstring/apiNoRoutes starting with this prefix are protected by auth middleware
excludedPathsArray[]NoPaths to skip auth middleware — string, RegExp, or { url, methods }
rolesstring[][]NoValid role names for this instance
appNamestringAppNoApplication name used in email templates
trustedOriginsstring[][]NoTrusted CORS origins
databaseProviderstringpostgresqlNoPrisma database provider (postgresql, mysql, sqlite)
requestPropertystringauthNoRequest property name for the raw session object
userPropertystringuserNoRequest property name for the user object
emailAndPasswordobject{ enabled: true }NoEmail/password auth settings
socialProvidersobject{}NoOAuth providers — google, facebook, github, microsoft
organizationsobject{ enabled: false }NoMulti-tenant organization support
twoFactorobject{ enabled: false }No2FA settings — email, sms, totp
magicLinksobject{ enabled: false }NoPasswordless auth via magic links
bearerTokensobject{ enabled: true }NoAPI bearer token support
adminobject{ enabled: true }NoAdmin plugin — impersonation and user management
advancedobjectSee belowNoCookie, session, and rate limit settings
templatesobjectBuilt-in defaultsNoEmail template overrides
auditLogobject{ enabled: true }NoAudit logging configuration
extraOptionsobject{}NoPass-through to Better Auth config

Advanced Options

NameTypeDefaultDescription
advanced.cookiePrefixstring{name}_authCookie prefix — auto-generated from instance name
advanced.useSecureCookiesbooleantrue in productionUse secure cookies
advanced.crossSubDomainCookiesbooleanfalseShare cookies across subdomains
advanced.session.expiresInnumber604800Session TTL in seconds (7 days)
advanced.session.updateAgenumber86400Session refresh interval in seconds (1 day)

Audit Log Options

NameTypeDefaultDescription
auditLog.enabledbooleantrueEnable audit logging
auditLog.eventsstring[]All 14 eventsEvents to capture
auditLog.retentionnumber365Retention in days
auditLog.captureIpbooleantrueCapture client IP
auditLog.captureUserAgentbooleantrueCapture user agent

Methods

fastify.xauthbetter decorator

PropertyTypeDescription
get(name)(name: string) => XAuthBetterInstanceGet a specific auth instance by name
defaultXAuthBetterInstanceFirst registered instance
configsRecord<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:

Request Properties

When prefix is configured, the auth middleware sets:

PropertyDescription
request.userAuthenticated user object
request.authRaw session object { session, user }
request.organizationOrganization 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:

TemplateVariablesDescription
verificationuserName, url, appNameEmail verification link
passwordResetuserName, url, appNamePassword reset link
magicLinkuserName, url, appNamePasswordless sign-in link
twoFactorOTPuserName, code, appName2FA verification code
orgInviteuserName, orgName, inviterName, url, appNameOrg invitation
accountLinkeduserName, appNameAccount linked notification

Email delivery requires @xenterprises/fastify-xemail or the email-outbox plugin to be registered.

Error Reference

ErrorCause
xAuthBetter: "configs" must be a non-empty arrayconfigs missing or empty
xAuthBetter: Prisma client is requiredNo prisma in options and no fastify.prisma decorator
xAuthBetter: Config "name" is requiredInstance config missing name
xAuthBetter: 'secret' is required and must be a stringMissing secret
xAuthBetter: 'secret' must be at least 32 characters longsecret shorter than 32 chars
xAuthBetter: 'baseURL' must be a valid URLInvalid 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 registeredSMS 2FA enabled without Twilio plugin
Invalid audit event: {event}Logging an unrecognized event name

Environment Variables

VariableRequiredDescription
DATABASE_URLYesPrisma connection string
NODE_ENVNoSet to production to enable secure cookies
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETIf using Google OAuthGoogle OAuth credentials
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETIf using GitHub OAuthGitHub 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
Copyright © 2026