npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xenterprises/fastify-xauth-better

v2.1.2

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.

Install

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',
    },
  ],
});

await fastify.listen({ port: 3000 });

Options

Plugin Options

| Name | Type | Default | Required | Description | |------|------|---------|----------|-------------| | configs | XAuthBetterConfig[] | — | Yes | Array of auth instance configurations (must be non-empty) | | prisma | PrismaClient | fastify.prisma | No | Prisma client instance. 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 valid URL) | | basePath | string | /api/auth | No | Path prefix for Better Auth routes | | prefix | string | /api | No | Routes starting with this prefix are protected | | excludedPaths | Array | [] | No | Paths to skip auth middleware (strings, 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 origins for CORS | | databaseProvider | string | postgresql | No | Prisma database provider (postgresql, mysql, sqlite) | | requestProperty | string | auth | No | Property name for session on request | | userProperty | string | user | No | Property name for user on request | | 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 org 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, user management) | | advanced | object | See defaults | No | Cookie, session, and rate limit settings | | templates | object | See 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[] | 19 events | Events to log | | auditLog.retention | number | 365 | Retention period in days | | auditLog.captureIp | boolean | true | Capture client IP | | auditLog.captureUserAgent | boolean | true | Capture user agent |

Decorated Properties

fastify.xauthbetter

| Property | Type | Description | |----------|------|-------------| | get(name) | function | Get a specific auth instance by name | | default | XAuthBetterInstance | First registered instance | | configs | Record<string, XAuthBetterInstance> | All registered instances | | pruneAuditLogs(options?) | function | Delete old audit log entries |

Instance API

Each instance returned by get(name) or default provides:

| Property | Type | Description | |----------|------|-------------| | auth | Auth | Raw Better Auth instance | | config | object | Merged configuration | | auditLog | AuditLogger | Audit logger (log(event, data)) | | templateRenderer | TemplateRenderer | Email template renderer | | getSession(request) | function | Get session from request headers/cookies | | requireAuth() | function | Returns auth middleware | | requireRole(roles) | function | Returns global role middleware | | requireOrgRole(roles) | function | Returns org-scoped role middleware | | requireOrg() | function | Returns org membership middleware |

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');

Middleware Usage

Auth Protection

Routes under a configured prefix are automatically protected. You can also use middleware directly:

fastify.get('/api/admin/dashboard', {
  preHandler: [fastify.xauthbetter.default.requireRole(['admin', 'superadmin'])],
}, async (request) => {
  return { user: request.user };
});

Organization Membership

fastify.get('/orgs/:orgId/projects', {
  preHandler: [userAuth.requireOrg()],
}, async (request) => {
  return { organization: request.organization };
});

Organization Roles

fastify.put('/orgs/:orgId/settings', {
  preHandler: [userAuth.requireOrgRole(['owner', 'admin'])],
}, async (request) => {
  return { updated: true };
});

Excluded Paths

{
  excludedPaths: [
    '/api/public',                         // string prefix
    /^\/api\/webhooks/,                     // regex
    { url: '/api/health', methods: ['GET'] } // url + methods
  ]
}

Organizations

{
  organizations: {
    enabled: true,
    orgIdHeader: 'X-Organization-Id',
    orgIdFromUrl: /^\/orgs\/([^\/]+)/,  // extract from URL path
  }
}

Organization context resolution priority: URL path > HTTP header > session.

2FA Configuration

{
  twoFactor: {
    enabled: true,
    email: true,   // requires email service plugin
    sms: true,     // requires @xenterprises/fastify-xtwilio
    totp: true,    // authenticator app
  }
}

Audit Logging

Allowed Events

14 audit events are supported:

  • auth.login.success, auth.login.failed, auth.logout
  • auth.password.changed, auth.password.reset.requested, auth.password.reset.completed
  • auth.2fa.enabled, auth.2fa.disabled
  • auth.session.revoked
  • auth.account.linked, auth.account.banned
  • auth.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,
});

Pruning

// Delete logs older than 365 days
await fastify.xauthbetter.pruneAuditLogs({ olderThanDays: 365 });

// Dry run (count only, no deletion)
const { count } = await fastify.xauthbetter.pruneAuditLogs({
  olderThanDays: 90,
  dryRun: true,
});

Email Templates

6 built-in templates with variable substitution:

| Template | Variables | Description | |----------|-----------|-------------| | verification | userName, url, appName | Email verification | | passwordReset | userName, url, appName | Password reset link | | magicLink | userName, url, appName | Passwordless sign-in | | twoFactorOTP | userName, code, appName | 2FA verification code | | orgInvite | userName, orgName, inviterName, url, appName | Organization invitation | | accountLinked | userName, appName | Account linked notification |

Custom Templates

{
  templates: {
    verification: {
      subject: 'Welcome {{userName}}!',
      html: '<html>Custom template with {{url}}</html>',
    },
  }
}

SendGrid Template Override

{
  templates: {
    verification: {
      templateId: 'd-abc123xyz',  // SendGrid dynamic template ID
    },
  }
}

Email sending requires @xenterprises/fastify-xemail or email-outbox plugin.

Environment Variables

| Name | Required | Description | |------|----------|-------------| | AUTH_SECRET | Yes | Auth secret (min 32 chars) — pass via config, not read directly | | DATABASE_URL | Yes | PostgreSQL connection string for Prisma | | NODE_ENV | No | Set to production for secure cookies | | GOOGLE_CLIENT_ID | No | Google OAuth client ID | | GOOGLE_CLIENT_SECRET | No | Google OAuth client secret | | GITHUB_CLIENT_ID | No | GitHub OAuth client ID | | GITHUB_CLIENT_SECRET | No | GitHub OAuth client secret |

Errors

| Error | When | |-------|------| | xAuthBetter: "configs" must be a non-empty array | Missing or empty configs | | xAuthBetter: Prisma client is required | No prisma in options or fastify.prisma | | 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 | Short secret | | xAuthBetter: 'baseURL' must be a valid URL | Invalid baseURL | | xAuthBetter: Duplicate instance name | Two configs with same name | | xAuthBetter: Duplicate basePath | Two configs with same basePath | | xAuthBetter: Duplicate cookiePrefix | Two configs with same cookie prefix | | xAuthBetter: 2FA SMS is enabled but xTwilio plugin is not registered | SMS 2FA without Twilio plugin | | Invalid audit event: {event} | Logging an unrecognized event |

Prisma Schema

Add these models to your schema (see prisma/schema.prisma for complete reference):

  • User — user accounts with email, name, role
  • Session — active sessions with tokens and expiry
  • Account — linked OAuth/credential accounts
  • Organization — multi-tenant organizations
  • Member — org membership with roles
  • Invitation — org invitations
  • AuthAuditLog — security audit trail

How It Works

The plugin creates one or more Better Auth instances, each with its own cookie namespace, basePath, and middleware scope. On registration:

  1. Each config is validated against a strict schema and merged with sensible defaults.
  2. A Better Auth instance is created with Prisma adapter, configured plugins (admin, bearer, 2FA, magic links, organizations), and email template integration.
  3. A catch-all Fastify route (basePath/*) forwards requests to Better Auth's handler, converting between Fastify request/reply and Web API Request/Response.
  4. If prefix is set, an onRequest hook validates sessions for all matching routes (excluding basePath and configured excludedPaths).
  5. Everything is exposed via fastify.xauthbetter decorator for programmatic access.

License

UNLICENSED