@xenterprises/fastify-xauth-better
v2.0.1
Published
Production-ready Fastify plugin for Better Auth with multi-instance support, organizations, 2FA, audit logging, and email templates
Readme
@xenterprises/fastify-xauth-better
Production-ready Fastify plugin for Better Auth with multi-instance support, organizations, 2FA, audit logging, and email templates.
Features
- Multi-Instance Support: Run multiple isolated auth instances (e.g., admin + user)
- Organizations: Multi-tenant support with role-based access control
- 2FA: Email and SMS two-factor authentication
- Magic Links: Passwordless authentication
- Audit Logging: Comprehensive security audit trail with 14 event types
- Email Templates: 6 default templates with customization support
- Cookie Isolation: Auto-generated unique cookie prefixes per instance
- Type-Safe: Full TypeScript support via Better Auth
- Prisma Integration: Seamless database integration
Installation
npm install @xenterprises/fastify-xauth-better better-auth @prisma/clientQuick 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,
baseURL: 'http://localhost:3000',
basePath: '/api/auth',
prefix: '/api',
},
],
});
await fastify.listen({ port: 3000 });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');Configuration
Required Fields
name(string): Unique instance identifiersecret(string): Auth secret, minimum 32 charactersbaseURL(string): Base URL for auth callbacks
Optional Fields
basePath(string): Auth routes path (default:/api/auth)prefix(string): Routes to protect (default: none)excludedPaths(array): Paths to skip auth middlewareroles(array): Valid role names for this instance
Organizations
{
organizations: {
enabled: true,
orgIdHeader: 'X-Organization-Id',
orgIdFromUrl: /^\/orgs\/([^\/]+)/,
}
}2FA Configuration
{
twoFactor: {
email: true,
sms: true,
totp: false,
}
}Note: SMS requires @xenterprises/fastify-xtwilio plugin.
Email Features
{
emailAndPassword: { enabled: true },
magicLinks: { enabled: true },
}Note: Email features require @xenterprises/fastify-xemail plugin.
Middleware Usage
Global Role Check
fastify.get('/admin/dashboard', {
preHandler: [fastify.xauthbetter.default.requireRole(['admin', 'superadmin'])],
}, async (request, reply) => {
return { message: 'Admin dashboard' };
});Organization Role Check
fastify.get('/orgs/:orgId/settings', {
preHandler: [fastify.xauthbetter.default.requireOrgRole(['owner', 'admin'])],
}, async (request, reply) => {
return { organization: request.organization };
});Organization Membership
fastify.get('/orgs/:orgId/projects', {
preHandler: [fastify.xauthbetter.default.requireOrg()],
}, async (request, reply) => {
return { projects: [] };
});Audit Logging
Automatic Events
The plugin automatically logs 14 security events:
auth.login.success,auth.login.failed,auth.logoutauth.password.changed,auth.password.reset.requested,auth.password.reset.completedauth.2fa.enabled,auth.2fa.disabledauth.session.revokedauth.account.linked,auth.account.bannedauth.org.joined,auth.org.left,auth.org.role.changed
Manual Logging
const instance = fastify.xauthbetter.get('user');
await instance.auditLog.log('auth.login.success', {
userId: 'user_123',
metadata: { method: 'email' },
request,
});Cleanup
await fastify.xauthbetter.pruneAuditLogs({ olderThanDays: 365 });Email Templates
Default Templates
6 built-in templates:
verification- Email verificationpasswordReset- Password reset linkmagicLink- Passwordless logintwoFactorOTP- 2FA codeorgInvite- Organization invitationaccountLinked- Account linked notification
Customization
{
templates: {
verification: {
subject: 'Custom subject for {{userName}}',
html: '<html>Custom template with {{url}}</html>',
from: '[email protected]',
},
}
}SendGrid Integration
{
templates: {
verification: {
templateId: 'd-abc123xyz',
},
}
}Prisma Schema Requirements
Add these models to your Prisma schema:
model User {
id String @id @default(cuid())
email String @unique
emailVerified Boolean @default(false)
name String?
image String?
role String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("user")
}
model Session {
id String @id @default(cuid())
userId String
expiresAt DateTime
token String @unique
ipAddress String?
userAgent String?
activeOrganizationId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("session")
}
model Account {
id String @id @default(cuid())
userId String
accountId String
providerId String
accessToken String?
refreshToken String?
idToken String?
expiresAt DateTime?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([providerId, accountId])
@@map("account")
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
logo String?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("organization")
}
model Member {
id String @id @default(cuid())
organizationId String
userId String
role String @default("member")
createdAt DateTime @default(now())
@@unique([userId, organizationId])
@@index([organizationId])
@@index([userId])
@@map("member")
}
model Invitation {
id String @id @default(cuid())
organizationId String
email String
role String
status String @default("pending")
expiresAt DateTime?
inviterId String
createdAt DateTime @default(now())
@@index([organizationId])
@@index([email])
@@index([status])
@@map("invitation")
}
model AuthAuditLog {
id String @id @default(cuid())
event String
userId String?
targetId String?
metadata Json?
ip String?
userAgent String?
createdAt DateTime @default(now())
@@index([userId])
@@index([event])
@@index([createdAt])
@@map("auth_audit_log")
}API Reference
fastify.xauthbetter
get(name)- Get specific instancedefault- First registered instanceconfigs- All instancespruneAuditLogs(options)- Cleanup audit logs
Instance API
Each instance provides:
auth- Raw better-auth instanceconfig- Merged configurationauditLog- Audit loggertemplateRenderer- Email template renderergetSession(request)- Get session from requestrequireAuth()- Auth middleware factoryrequireRole(roles)- Global role middlewarerequireOrgRole(roles)- Org role middlewarerequireOrg()- Org membership middleware
Migration from v1.x
Breaking Changes
- Configuration Structure: Config is now validated and merged with defaults
- Multi-Instance:
configsarray replaces single config - Cookie Prefix: Auto-generated from instance name
- Middleware: Separate factories for global vs org roles
- Audit Logging: Now required, not optional
- Email Templates: Built-in defaults with customization
Migration Steps
- Update
better-authto^1.4.0 - Wrap config in
configsarray - Add
namefield to config - Update Prisma schema with organization models
- Run migrations:
npx prisma migrate dev - Update middleware usage (see examples above)
License
ISC
