A Payload CMS plugin that integrates Better Auth for seamless user authentication and management.
- Better Auth as Single Source of Truth — All user operations managed through Better Auth
- SecondaryStorage Pattern — Pluggable storage with SQLite (dev) or Redis (production)
- Instant Session Validation — Payload reads sessions directly from shared storage (no HTTP calls)
- Automatic Session Invalidation — Logout in Better Auth immediately invalidates Payload sessions
- Horizontal Scaling — Redis adapter supports multiple instances
- Timestamp-based Coordination — Automatic reconciliation without race conditions
- Custom Login UI — Replaces Payload's default login with Better Auth authentication
- Auto-extending Users Collection — Plugin extends your existing users collection with auth integration
- Better Auth Collections — Dedicated collections for each auth method (email-password, magic-link)
pnpm add payload-better-auth better-authRequirements: Node.js 22+ (for native SQLite), Better Auth 1.4.10+, Payload CMS 3.37.0+
// lib/syncAdapter.ts
import { DatabaseSync } from 'node:sqlite'
import { createSqliteStorage } from 'payload-better-auth/storage'
const db = new DatabaseSync('.sync-state.db')
export const storage = createSqliteStorage({ db })
// lib/eventBus.ts
import { DatabaseSync } from 'node:sqlite'
import { createSqlitePollingEventBus } from 'payload-better-auth/eventBus'
const db = new DatabaseSync('.event-bus.db')
export const eventBus = createSqlitePollingEventBus({ db })// lib/auth.ts
import { betterAuth } from 'better-auth'
import { admin, apiKey } from 'better-auth/plugins'
import Database from 'better-sqlite3'
import { payloadBetterAuthPlugin } from 'payload-better-auth'
import type { User } from './payload-types' // Generated Payload types
import buildConfig from './payload.config.js'
import { eventBus } from './eventBus'
import { storage } from './syncAdapter'
export const auth = betterAuth({
database: new Database(process.env.BETTER_AUTH_DB_PATH || './better-auth.db'),
secret: process.env.BETTER_AUTH_SECRET,
emailAndPassword: { enabled: true },
plugins: [
admin(),
apiKey(),
payloadBetterAuthPlugin<User>({
payloadConfig: buildConfig,
token: process.env.RECONCILE_TOKEN,
storage, // Shared with Payload plugin
eventBus, // Shared with Payload plugin
// Map Better Auth user data to your Payload user fields
mapUserToPayload: (baUser) => ({
email: baUser.email ?? '',
name: baUser.name ?? '',
// Add defaults for any required fields in your users collection
}),
}),
],
})// payload.config.ts
import { buildConfig } from 'payload'
import { betterAuthPayloadPlugin } from 'payload-better-auth'
import { eventBus } from './lib/eventBus'
import { storage } from './lib/syncAdapter'
export default buildConfig({
collections: [
// Optional: Define your own users collection - it will be auto-extended
{
slug: 'users',
fields: [
{ name: 'email', type: 'email', required: true },
{ name: 'name', type: 'text' },
// Add your custom fields...
],
// Your access rules are preserved and OR'd with BA sync access
access: {
read: ({ req }) => Boolean(req.user),
},
},
// ... other collections
],
plugins: [
betterAuthPayloadPlugin({
betterAuthClientOptions: {
externalBaseURL: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
internalBaseURL: process.env.INTERNAL_SERVER_URL || 'http://localhost:3000',
},
storage, // Shared with Better Auth plugin
eventBus, // Shared with Better Auth plugin
collectionPrefix: '__better_auth', // optional, this is the default
debug: false, // Enable to see BA collections in admin panel
// Optional: Custom access for BA collections
baCollectionsAccess: {
read: ({ req }) => req.user?.role === 'admin',
delete: ({ req }) => req.user?.role === 'admin',
},
}),
],
// ... rest of your config
})If you don't define a users collection, a minimal one will be created automatically.
BETTER_AUTH_SECRET=your-secret-min-32-chars
BETTER_AUTH_DB_PATH=./better-auth.db
BA_TO_PAYLOAD_SECRET=your-sync-secret
RECONCILE_TOKEN=your-api-token
PAYLOAD_SECRET=your-payload-secret
DATABASE_URI=file:./payload.dbYour access rules are preserved and combined with Better Auth's internal access. BA sync operations (signed with BA_TO_PAYLOAD_SECRET) always pass.
// Example: Allow admins to manage all users, regular users to read only
{
slug: 'users',
access: {
read: () => true, // everyone can read
create: ({ req }) => req.user?.role === 'admin', // only admins create manually
update: ({ req, id }) => req.user?.role === 'admin' || req.user?.id === id,
delete: ({ req }) => req.user?.role === 'admin',
},
}
// Result: BA sync operations pass via signature, manual operations use your rulesThe plugin creates two additional collections for auth method data:
__better_auth_email_password- Email/password account data__better_auth_magic_link- Magic link account data
These collections are locked down by default (only BA sync agent can access). You can optionally open up read and delete access.
Note: These collections are hidden from the admin panel by default. Set
debug: truein the Payload plugin options to make them visible under the "Better Auth (DEBUG)" group for troubleshooting.
For multi-server or geo-distributed deployments, use the Redis storage and EventBus adapters:
// lib/syncAdapter.ts
import { createRedisStorage } from 'payload-better-auth/storage'
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)
export const storage = createRedisStorage({ redis })
// lib/eventBus.ts
import { createRedisEventBus } from 'payload-better-auth/eventBus'
import Redis from 'ioredis'
// Redis Pub/Sub requires separate connections for publishing and subscribing
const publisher = new Redis(process.env.REDIS_URL)
const subscriber = new Redis(process.env.REDIS_URL)
export const eventBus = createRedisEventBus({ publisher, subscriber })Then pass the same instances to both plugins:
// In Better Auth config:
payloadBetterAuthPlugin({
storage,
eventBus,
payloadConfig: buildConfig,
token: process.env.RECONCILE_TOKEN,
mapUserToPayload: (baUser) => ({ ... }),
})
// In Payload config:
betterAuthPayloadPlugin({
storage,
eventBus,
betterAuthClientOptions: { ... },
})For detailed configuration options, API endpoints, architecture details, and production considerations, see the MANUAL.md.
# Install dependencies
pnpm install
# Reset databases and run migrations
pnpm reset
# Start development server
pnpm devThe dev server starts at http://localhost:3000 with a mail server at port 1080.
To test the Redis integration locally:
# Start Redis container
pnpm docker:redis
# Run dev server with Redis (instead of SQLite)
pnpm dev:redis
# Stop Redis when done
pnpm docker:redis:stopThis project uses Husky for Git hooks:
- pre-commit: Builds the plugin and stages
dist/, blocks manual version changes - pre-push: Runs lint, typecheck, and tests before pushing
- commit-msg: Validates commit messages follow Conventional Commits
When you're happy with your changes, just commit — the build is handled for you!
This project uses semantic-release for automated versioning. Do not manually edit the version field in package.json — it will be rejected by the pre-commit hook.
Versions are determined automatically from your commit messages:
| Commit Type | Version Bump | Example |
|---|---|---|
fix: |
Patch (1.0.0 → 1.0.1) | fix: resolve login redirect bug |
feat: |
Minor (1.0.0 → 1.1.0) | feat: add OAuth provider support |
feat!: or BREAKING CHANGE: |
Major (1.0.0 → 2.0.0) | feat!: redesign auth API |
When you push to main, the CI will automatically:
- Analyze commits since the last release
- Determine the next version
- Update
package.jsonandCHANGELOG.md - Create a Git tag and GitHub Release
# Latest
pnpm add github:benjaminpreiss/payload-better-auth
# Specific version
pnpm add github:benjaminpreiss/payload-better-auth#v1.2.0| Script | Description |
|---|---|
pnpm dev |
Start dev server with mail server |
pnpm dev:redis |
Start dev server with Redis (instead of SQLite) |
pnpm docker:redis |
Start Redis container via Docker Compose |
pnpm docker:redis:stop |
Stop Redis container |
pnpm build |
Build the plugin |
pnpm reset |
Reset databases and run all migrations |
pnpm test |
Run all tests |
pnpm lint |
Run ESLint |
pnpm typecheck |
Run TypeScript type checking |
pnpm generate:types |
Generate Payload types |
├── src/ # Plugin source code
│ ├── storage/ # SecondaryStorage implementations (SQLite, Redis)
│ ├── eventBus/ # EventBus implementations (SQLite polling, Redis Pub/Sub)
│ ├── better-auth/ # Better Auth integration & reconcile queue
│ ├── collections/ # Payload collections (Users, BetterAuth)
│ ├── components/ # React components (Login UI)
│ ├── payload/ # Payload plugin
│ ├── shared/ # Shared utilities (deduplicated logger)
│ └── exports/ # Client/RSC exports
├── dev/ # Development environment
│ ├── app/ # Next.js app
│ ├── lib/ # Dev configuration
│ └── tests/ # Test files
└── dist/ # Built output
MIT