@delmaredigital/payload-better-auth 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,120 +13,45 @@ Better Auth adapter and plugins for Payload CMS. Enables seamless integration be
13
13
  </p>
14
14
 
15
15
  ---
16
+
16
17
  ## Documentation
17
- For additional documentation and references, visit: [https://deepwiki.com/delmaredigital/payload-better-auth](https://deepwiki.com/delmaredigital/payload-better-auth)
18
18
 
19
- ## Table of Contents
19
+ **[Full Documentation](https://delmaredigital.github.io/payload-better-auth/)** API reference, guides, recipes, UI components, and more.
20
20
 
21
- - [Installation](#installation)
22
- - [Quick Start](#quick-start)
23
- - [MongoDB Setup](#mongodb-setup)
24
- - [API Reference](#api-reference)
25
- - [Customization](#customization)
26
- - [Access Control Helpers](#access-control-helpers)
27
- - [API Key Scope Enforcement](#api-key-scope-enforcement)
28
- - [Plugin Compatibility](#plugin-compatibility)
29
- - [Recipes](#recipes)
30
- - [Types](#types)
31
- - [License](#license)
21
+ For AI-assisted exploration: [DeepWiki](https://deepwiki.com/delmaredigital/payload-better-auth)
32
22
 
33
23
  ---
34
24
 
35
- ## Installation
36
-
37
- ### Requirements
38
-
39
- | Dependency | Version |
40
- |------------|---------|
41
- | `payload` | >= 3.69.0 |
42
- | `@payloadcms/next` | >= 3.69.0 |
43
- | `@payloadcms/ui` | >= 3.69.0 |
44
- | `better-auth` | >= 1.4.0 |
45
- | `@better-auth/passkey` | >= 1.4.18 (if using passkeys) |
46
- | `next` | >= 15.4.8 |
47
- | `react` | >= 19.2.1 |
48
-
49
- ### Install
25
+ ## Install
50
26
 
51
27
  ```bash
52
28
  pnpm add @delmaredigital/payload-better-auth better-auth
53
-
54
- # If using passkeys:
55
- pnpm add @better-auth/passkey
56
- ```
57
-
58
- ### Environment Variables
59
-
60
- Better Auth requires these environment variables:
61
-
62
- ```bash
63
- # Required
64
- BETTER_AUTH_SECRET=your-secret-key-min-32-chars # Must be at least 32 characters
65
-
66
- # Optional - only needed if not using the getBaseUrl() helper below
67
- BETTER_AUTH_URL=http://localhost:3000 # Your app's base URL
68
-
69
- # OAuth Providers (if using social login)
70
- GOOGLE_CLIENT_ID=...
71
- GOOGLE_CLIENT_SECRET=...
72
- # etc.
73
- ```
74
-
75
- **Notes:**
76
- - `BETTER_AUTH_SECRET` is used for signing sessions and tokens - use a secure random string
77
- - `BETTER_AUTH_URL` tells Better Auth where it's hosted - plugins like passkey derive their config from this
78
- - WebAuthn (passkeys) requires HTTPS in production but works on `localhost` for development
79
-
80
- **Vercel Deployment:**
81
-
82
- For seamless support of Vercel preview deployments, use this helper instead of hardcoding URLs:
83
-
84
- ```ts
85
- // src/lib/auth/getBaseUrl.ts
86
- export function getBaseUrl() {
87
- // Vercel preview/production deployments
88
- if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
89
- // Explicit override
90
- if (process.env.BETTER_AUTH_URL) return process.env.BETTER_AUTH_URL
91
- // Local development
92
- return 'http://localhost:3000'
93
- }
94
29
  ```
95
30
 
96
- This automatically handles:
97
- - **Local dev**: Uses `http://localhost:3000`
98
- - **Vercel preview**: Uses the auto-generated `*.vercel.app` URL
99
- - **Production**: Uses your custom domain (set `BETTER_AUTH_URL` in production env)
100
-
101
- ---
31
+ **Requirements:** `payload` >= 3.69.0 · `better-auth` >= 1.4.0 · `next` >= 15.4.8 · `react` >= 19.2.1
102
32
 
103
33
  ## Quick Start
104
34
 
105
- ### Step 1: Create Your Auth Configuration
35
+ ### 1. Auth Configuration
106
36
 
107
37
  ```ts
108
38
  // src/lib/auth/config.ts
109
39
  import type { BetterAuthOptions } from 'better-auth'
110
40
 
111
41
  export const betterAuthOptions: Partial<BetterAuthOptions> = {
112
- // Model names are SINGULAR - they get pluralized automatically
113
- // 'user' becomes 'users', 'session' becomes 'sessions', etc.
114
42
  user: {
115
43
  additionalFields: {
116
44
  role: { type: 'string', defaultValue: 'user' },
117
45
  },
118
46
  },
119
- session: {
120
- expiresIn: 60 * 60 * 24 * 30, // 30 days
121
- },
122
47
  emailAndPassword: { enabled: true },
123
48
  }
124
49
  ```
125
50
 
126
- ### Step 2: Create Your Users Collection
51
+ ### 2. Users Collection
127
52
 
128
53
  ```ts
129
- // src/collections/Users/index.ts (vanilla starter uses folder-based collections)
54
+ // src/collections/Users/index.ts
130
55
  import type { CollectionConfig } from 'payload'
131
56
  import { betterAuthStrategy } from '@delmaredigital/payload-better-auth'
132
57
 
@@ -162,14 +87,7 @@ export const Users: CollectionConfig = {
162
87
  }
163
88
  ```
164
89
 
165
- > **Note:** Plugin-specific fields (e.g., `twoFactorEnabled` for 2FA, `banned` for admin) are **automatically added** to your Users collection by `betterAuthCollections()`. You'll see a log message like:
166
- > ```
167
- > [better-auth] Auto-adding fields to 'users': ['twoFactorEnabled']
168
- > ```
169
-
170
- ### Step 3: Configure Payload (Postgres)
171
-
172
- > For MongoDB, see [MongoDB Setup](#mongodb-setup).
90
+ ### 3. Payload Config
173
91
 
174
92
  ```ts
175
93
  // src/payload.config.ts
@@ -188,46 +106,31 @@ import { getBaseUrl } from './lib/auth/getBaseUrl'
188
106
  const baseUrl = getBaseUrl()
189
107
 
190
108
  export default buildConfig({
191
- collections: [Users /* ...other collections */],
109
+ collections: [Users],
192
110
  plugins: [
193
- // Auto-generate sessions, accounts, verifications collections
194
111
  betterAuthCollections({
195
112
  betterAuthOptions,
196
- skipCollections: ['user'], // We define Users ourselves
113
+ skipCollections: ['user'],
197
114
  }),
198
- // Initialize Better Auth with auto-injected endpoints and admin components
199
115
  createBetterAuthPlugin({
200
116
  createAuth: (payload) =>
201
117
  betterAuth({
202
118
  ...betterAuthOptions,
203
- database: payloadAdapter({
204
- payloadClient: payload,
205
- // adapterConfig: { enableDebugLogs: true }, // Uncomment to enable debug logging
206
- }),
207
- // For Payload's default SERIAL IDs:
208
- advanced: {
209
- database: {
210
- generateId: 'serial',
211
- },
212
- },
119
+ database: payloadAdapter({ payloadClient: payload }),
120
+ advanced: { database: { generateId: 'serial' } },
213
121
  baseURL: baseUrl,
214
122
  secret: process.env.BETTER_AUTH_SECRET,
215
- trustedOrigins: [baseUrl], // Or use withBetterAuthDefaults() below
123
+ trustedOrigins: [baseUrl],
216
124
  }),
217
125
  }),
218
126
  ],
219
- admin: {
220
- user: 'users',
221
- },
222
127
  db: postgresAdapter({
223
128
  pool: { connectionString: process.env.DATABASE_URL },
224
129
  }),
225
130
  })
226
131
  ```
227
132
 
228
- > **⚠️ Note:** The plugin automatically injects its own login page, logout button, and redirect handling. Don't add a custom `beforeLogin` in Payload's `admin.components` directly - use the plugin's options instead (see [Customization](#customization) for `disableLoginView`, `loginViewComponent`, etc.).
229
-
230
- ### Step 4: Client-Side Auth
133
+ ### 4. Client-Side Auth
231
134
 
232
135
  ```ts
233
136
  // src/lib/auth/client.ts
@@ -235,42 +138,14 @@ export default buildConfig({
235
138
 
236
139
  import { createPayloadAuthClient } from '@delmaredigital/payload-better-auth/client'
237
140
 
238
- // Pre-configured with twoFactor, apiKey, and passkey plugins
239
- // Uses window.location.origin automatically - works on any deployment URL
240
141
  export const authClient = createPayloadAuthClient()
241
142
 
242
143
  export const { useSession, signIn, signUp, signOut, twoFactor, passkey } = authClient
243
144
  ```
244
145
 
245
- **Note:** `createPayloadAuthClient()` automatically uses `window.location.origin` as the base URL, so it works seamlessly across local dev, Vercel previews, and production without any configuration.
246
-
247
- **Adding custom plugins (e.g., Stripe):**
248
-
249
- For custom plugins with full TypeScript support, use `createAuthClient` with `payloadAuthPlugins`:
250
-
251
- ```ts
252
- // src/lib/auth/client.ts
253
- 'use client'
254
-
255
- import { createAuthClient, payloadAuthPlugins } from '@delmaredigital/payload-better-auth/client'
256
- import { stripeClient } from '@better-auth/stripe/client'
257
-
258
- // Spread payloadAuthPlugins to include defaults (twoFactor, apiKey, passkey)
259
- // Then add your custom plugins - full type safety!
260
- export const authClient = createAuthClient({
261
- plugins: [...payloadAuthPlugins, stripeClient({ subscription: true })],
262
- })
263
-
264
- // authClient.subscription is fully typed
265
- export const { useSession, signIn, signUp, signOut, twoFactor, passkey, subscription } = authClient
266
- ```
267
-
268
- This approach uses Better Auth's native `createAuthClient` with our default plugins, giving you full type inference for any custom plugins you add.
269
-
270
- ### Step 5: Server-Side Session Access
146
+ ### 5. Server-Side Session
271
147
 
272
148
  ```ts
273
- // In a server component or API route
274
149
  import { headers } from 'next/headers'
275
150
  import { getPayload } from 'payload'
276
151
  import { getServerSession } from '@delmaredigital/payload-better-auth'
@@ -280,1697 +155,17 @@ export default async function Dashboard() {
280
155
  const headersList = await headers()
281
156
  const session = await getServerSession(payload, headersList)
282
157
 
283
- if (!session) {
284
- redirect('/login')
285
- }
158
+ if (!session) { redirect('/login') }
286
159
 
287
160
  return <div>Hello {session.user.name}</div>
288
161
  }
289
162
  ```
290
163
 
291
- **That's it!** The plugin automatically:
292
- - Registers auth API endpoints at `/api/auth/*`
293
- - Injects logout button, login redirect, and login page components
294
- - Handles session management via Better Auth
295
-
296
- ---
297
-
298
- ## MongoDB Setup
299
-
300
- The adapter auto-detects MongoDB and configures itself accordingly. No special adapter configuration is needed.
301
-
302
- ### Key Differences from Postgres
303
-
304
- | | Postgres (default) | MongoDB |
305
- |---|---|---|
306
- | **ID type** | `'number'` (SERIAL) | `'text'` (ObjectId strings) |
307
- | **`generateId`** | Set to `'serial'` | Do not set (MongoDB generates ObjectIds) |
308
- | **`idType`** | `'number'` (default) | Auto-detected as `'text'` |
309
- | **`betterAuthStrategy`** | `idType: 'number'` (default) | `idType: 'text'` |
310
- | **`createSessionHelpers`** | `idType: 'number'` | `idType: 'text'` (or omit) |
311
-
312
- ### Quick Start (MongoDB)
313
-
314
- ```ts
315
- // src/payload.config.ts
316
- import { buildConfig } from 'payload'
317
- import { mongooseAdapter } from '@payloadcms/db-mongodb'
318
- import { betterAuth } from 'better-auth'
319
- import {
320
- betterAuthCollections,
321
- createBetterAuthPlugin,
322
- payloadAdapter,
323
- } from '@delmaredigital/payload-better-auth'
324
- import { betterAuthOptions } from './lib/auth/config'
325
- import { Users } from './collections/Users'
326
-
327
- export default buildConfig({
328
- collections: [Users],
329
- plugins: [
330
- betterAuthCollections({
331
- betterAuthOptions,
332
- skipCollections: ['user'],
333
- }),
334
- createBetterAuthPlugin({
335
- createAuth: (payload) =>
336
- betterAuth({
337
- ...betterAuthOptions,
338
- database: payloadAdapter({ payloadClient: payload }),
339
- // Do NOT set advanced.database.generateId — MongoDB generates ObjectIds
340
- secret: process.env.BETTER_AUTH_SECRET,
341
- trustedOrigins: ['http://localhost:3000'],
342
- }),
343
- }),
344
- ],
345
- db: mongooseAdapter({
346
- url: process.env.DATABASE_URI!,
347
- }),
348
- })
349
- ```
350
-
351
- **Users collection:**
352
-
353
- ```ts
354
- import { betterAuthStrategy } from '@delmaredigital/payload-better-auth'
355
-
356
- export const Users: CollectionConfig = {
357
- slug: 'users',
358
- auth: {
359
- disableLocalStrategy: true,
360
- strategies: [betterAuthStrategy({ idType: 'text' })],
361
- },
362
- // ... fields and access control same as Postgres setup
363
- }
364
- ```
365
-
366
- **Session helpers:**
367
-
368
- ```ts
369
- import { createSessionHelpers } from '@delmaredigital/payload-better-auth'
370
- import type { User } from '@/payload-types'
371
-
372
- export const { getServerSession, getServerUser } = createSessionHelpers<User>({
373
- idType: 'text',
374
- })
375
- ```
376
-
377
- ### Migrating from Postgres to MongoDB
378
-
379
- 1. **Remove `generateId: 'serial'`** from your Better Auth config (the `advanced.database` block)
380
- 2. **Change `betterAuthStrategy()`** to use `idType: 'text'`:
381
- ```ts
382
- strategies: [betterAuthStrategy({ idType: 'text' })]
383
- ```
384
- 3. **Update `createSessionHelpers`** (if using) to `idType: 'text'`:
385
- ```ts
386
- createSessionHelpers<User>({ idType: 'text' })
387
- ```
388
- 4. **Remove `idType: 'number'`** from `adapterConfig` if you had it set explicitly — the adapter auto-detects `'text'` for MongoDB
389
- 5. **Switch the Payload database adapter**:
390
- ```ts
391
- // Before
392
- import { postgresAdapter } from '@payloadcms/db-postgres'
393
- db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL } })
394
-
395
- // After
396
- import { mongooseAdapter } from '@payloadcms/db-mongodb'
397
- db: mongooseAdapter({ url: process.env.DATABASE_URI! })
398
- ```
399
- 6. **Remove `idFieldsAllowlist`/`idFieldsBlocklist`** from `adapterConfig` if set — these are only relevant for serial ID conversion
400
-
401
- ---
402
-
403
- ## API Reference
404
-
405
- ### `payloadAdapter(config)`
406
-
407
- Creates a Better Auth database adapter that uses Payload collections. Uses Better Auth's `createAdapterFactory` for schema-aware transformations, automatically supporting all Better Auth plugins.
408
-
409
- ```ts
410
- payloadAdapter({
411
- payloadClient: payload,
412
- adapterConfig: {
413
- enableDebugLogs: false,
414
- idType: 'number', // Optional - auto-detects from generateId setting
415
- },
416
- })
417
- ```
418
-
419
- | Option | Type | Description |
420
- |--------|------|-------------|
421
- | `payloadClient` | `BasePayload \| () => Promise<BasePayload>` | Payload instance or factory function |
422
- | `adapterConfig.enableDebugLogs` | `boolean` | Enable debug logging (default: `false`) |
423
- | `adapterConfig.dbType` | `'postgres' \| 'mongodb' \| 'sqlite'` | Database type (auto-detected from Payload's adapter) |
424
- | `adapterConfig.idType` | `'number' \| 'text'` | ID type (auto-detected: `'number'` for Postgres/SQLite, `'text'` for MongoDB) |
425
- | `adapterConfig.idFieldsAllowlist` | `string[]` | Additional fields to convert to numeric IDs (default: `[]`) |
426
- | `adapterConfig.idFieldsBlocklist` | `string[]` | Fields to exclude from numeric ID conversion (default: `[]`) |
427
-
428
- **Database & ID Type:**
429
- - **Auto-detected**: The adapter reads `payload.db.name` to determine the database type, then sets `idType` accordingly
430
- - **Postgres/SQLite**: `idType` defaults to `'number'` (SERIAL IDs)
431
- - **MongoDB**: `idType` is always `'text'` (ObjectId strings)
432
- - Set `dbType` explicitly only if auto-detection doesn't work for your adapter
433
-
434
- **Note:** When using number IDs (default), you can optionally set `generateId: 'serial'` in Better Auth to be explicit:
435
- ```typescript
436
- advanced: { database: { generateId: 'serial' } }
437
- ```
438
- This is not required - the adapter handles it automatically. A warning will only appear if you explicitly set `generateId` to something incompatible.
439
-
440
- **ID Field Conversion:**
441
-
442
- When using serial IDs (`idType: 'number'`), the adapter automatically converts string ID fields to numbers. This applies to fields matching `*Id` or `*_id` patterns (like `activeOrganizationId`). This ensures Payload relationship lookups work correctly in access control.
443
-
444
- Use `idFieldsAllowlist` and `idFieldsBlocklist` to customize this behavior:
445
-
446
- ```typescript
447
- payloadAdapter({
448
- payloadClient: payload,
449
- adapterConfig: {
450
- // Add fields that don't follow the *Id pattern but should be converted
451
- idFieldsAllowlist: ['customOrgRef', 'legacyIdentifier'],
452
-
453
- // Exclude fields that end in 'Id' but aren't actually ID references
454
- idFieldsBlocklist: ['visitorId', 'correlationId'],
455
- },
456
- })
457
- ```
458
-
459
- **Custom Collection Names (Optional):**
460
-
461
- By default, the adapter uses standard collection names (`users`, `sessions`, `accounts`, `verifications`). You only need `modelName` if you want **custom** names:
462
-
463
- ```ts
464
- betterAuth({
465
- database: payloadAdapter({ payloadClient: payload }),
466
- // Only set modelName to CUSTOMIZE collection names
467
- // Use SINGULAR form - gets pluralized automatically
468
- user: { modelName: 'member' }, // Changes 'users' → 'members'
469
- session: { modelName: 'auth_session' }, // Changes 'sessions' → 'auth_sessions'
470
- })
471
- ```
472
-
473
- **Note:** If you're using the default collection names, don't set `modelName` at all.
474
-
475
- ### `betterAuthCollections(options)`
476
-
477
- Payload plugin that auto-generates collections from Better Auth schema.
478
-
479
- ```ts
480
- betterAuthCollections({
481
- betterAuthOptions,
482
- skipCollections: ['user'],
483
- adminGroup: 'Auth',
484
- usePlural: true,
485
- customizeCollection: (modelKey, collection) => collection,
486
- })
487
- ```
488
-
489
- | Option | Type | Description |
490
- |--------|------|-------------|
491
- | `betterAuthOptions` | `BetterAuthOptions` | Your Better Auth options |
492
- | `skipCollections` | `string[]` | Collections to skip generating (default: `['user']`) |
493
- | `adminGroup` | `string` | Admin panel group name (default: `'Auth'`) |
494
- | `access` | `CollectionConfig['access']` | Custom access control for generated collections. **Note**: Replaces default access entirely (see caution below). |
495
- | `usePlural` | `boolean` | Pluralize collection slugs (default: `true`) |
496
- | `configureSaveToJWT` | `boolean` | Auto-configure `saveToJWT` for session-critical fields (default: `true`) |
497
- | `firstUserAdmin` | `boolean \| FirstUserAdminOptions` | Make first registered user an admin (default: `true`) |
498
- | `customizeCollection` | `(modelKey, collection) => CollectionConfig` | Customize generated collections |
499
-
500
- > **⚠️ Caution on Custom Access:**
501
- > When providing the `access` option, it **completely replaces** the default access object for all auto-generated collections. It does not merge with or override individual properties.
502
- >
503
- > By default, the plugin sets:
504
- > - `read`: `isAdmin()`
505
- > - `delete`: `isAdmin()`
506
- > - `create`: `() => false` (Manual creation disabled - Better Auth manages these)
507
- > - `update`: `() => false` (Manual update disabled - Better Auth manages these)
508
- >
509
- > You must explicitly handle all access types to ensure your collections remain secure and functional.
510
-
511
- **First User Admin:**
512
-
513
- By default, the first user to register is automatically assigned the admin role. This provides a better out-of-the-box experience - no need to manually update the database to create your first admin.
514
-
515
- ```typescript
516
- // Enabled by default - first user gets role='admin'
517
- betterAuthCollections({ betterAuthOptions })
518
-
519
- // Customize roles
520
- betterAuthCollections({
521
- betterAuthOptions,
522
- firstUserAdmin: {
523
- adminRole: 'super-admin', // Role for first user (default: 'admin')
524
- defaultRole: 'member', // Role for subsequent users (default: 'user')
525
- roleField: 'userRole', // Field name (default: 'role')
526
- },
527
- })
528
-
529
- // Disable
530
- betterAuthCollections({
531
- betterAuthOptions,
532
- firstUserAdmin: false,
533
- })
534
-
535
- **Customization Example:**
536
-
537
- ```ts
538
- betterAuthCollections({
539
- betterAuthOptions,
540
- customizeCollection: (modelKey, collection) => {
541
- if (modelKey === 'session') {
542
- return {
543
- ...collection,
544
- hooks: {
545
- afterDelete: [cleanupExpiredSessions],
546
- },
547
- }
548
- }
549
- return collection
550
- },
551
- })
552
- ```
553
-
554
- ### `createBetterAuthPlugin(options)`
555
-
556
- Payload plugin that initializes Better Auth during Payload's `onInit`.
557
-
558
- ```ts
559
- createBetterAuthPlugin({
560
- createAuth: (payload) => betterAuth({ ... }),
561
- authBasePath: '/auth',
562
- autoRegisterEndpoints: true,
563
- autoInjectAdminComponents: true,
564
- admin: {
565
- login: { title: 'Admin Login' },
566
- },
567
- })
568
- ```
569
-
570
- | Option | Type | Default | Description |
571
- |--------|------|---------|-------------|
572
- | `createAuth` | `(payload: BasePayload) => Auth` | *required* | Factory function that creates the Better Auth instance |
573
- | `authBasePath` | `string` | `'/auth'` | Base path for auth API endpoints |
574
- | `autoRegisterEndpoints` | `boolean` | `true` | Auto-register auth API endpoints |
575
- | `autoInjectAdminComponents` | `boolean` | `true` | Auto-inject admin components when `disableLocalStrategy` detected |
576
- | `admin.disableLogoutButton` | `boolean` | `false` | Disable logout button injection |
577
- | `admin.disableBeforeLogin` | `boolean` | `false` | Disable BeforeLogin redirect injection |
578
- | `admin.disableLoginView` | `boolean` | `false` | Disable login view injection |
579
- | `admin.login.title` | `string` | `'Login'` | Custom login page title |
580
- | `admin.login.afterLoginPath` | `string` | `'/admin'` | Redirect path after successful login |
581
- | `admin.login.requiredRole` | `string \| string[] \| null` | `'admin'` | Required role(s) for admin access. Array = any role matches (unless `requireAllRoles`). Set to `null` to disable. |
582
- | `admin.login.requireAllRoles` | `boolean` | `false` | When `requiredRole` is an array, require ALL roles (true) or ANY role (false). |
583
- | `admin.login.enablePasskey` | `boolean \| 'auto'` | `false` | Enable passkey (WebAuthn) sign-in option. `'auto'` detects if passkey plugin is available. |
584
- | `admin.login.enableSignUp` | `boolean \| 'auto'` | `'auto'` | Enable user registration. `'auto'` detects if sign-up endpoint is available. |
585
- | `admin.login.defaultSignUpRole` | `string` | `'user'` | Default role assigned to new users during registration |
586
- | `admin.login.enableForgotPassword` | `boolean \| 'auto'` | `'auto'` | Enable forgot password link. `'auto'` detects if endpoint is available. |
587
- | `admin.login.resetPasswordUrl` | `string` | - | Custom URL for password reset. If not set, uses inline reset form. |
588
- | `admin.logoutButtonComponent` | `string` | - | Override logout button (import map format) |
589
- | `admin.beforeLoginComponent` | `string` | - | Override BeforeLogin component |
590
- | `admin.loginViewComponent` | `string` | - | Override login view component |
591
- | `admin.betterAuthOptions` | `BetterAuthOptions` | - | Better Auth options (required for management UI auto-detection) |
592
- | `admin.enableManagementUI` | `boolean` | `true` | Enable security management UI (2FA, API keys) |
593
- | `admin.managementPaths.twoFactor` | `string` | `'/security/two-factor'` | Two-factor management view path |
594
- | `admin.managementPaths.apiKeys` | `string` | `'/security/api-keys'` | API keys management view path |
595
- | `admin.managementPaths.passkeys` | `string` | `'/security/passkeys'` | Passkeys management view path |
596
- | `admin.apiKey` | `ApiKeyScopesConfig` | - | API key scopes configuration (see below) |
597
-
598
- #### API Key Scopes Configuration
599
-
600
- API keys can have granular permission scopes. By default, scopes are auto-generated from your Payload collections.
601
-
602
- | Option | Type | Default | Description |
603
- |--------|------|---------|-------------|
604
- | `scopes` | `Record<string, ScopeDefinition>` | - | Custom scope definitions |
605
- | `includeCollectionScopes` | `boolean` | `true` when no custom scopes, `false` when custom scopes provided | Include auto-generated collection scopes |
606
- | `excludeCollections` | `string[]` | `['sessions', 'verifications', 'accounts', 'twoFactors', 'apikeys']` | Collections to exclude from auto-generated scopes |
607
- | `defaultScopes` | `string[]` | `[]` | Default scopes pre-selected when creating a key |
608
- | `requiredRole` | `string \| string[] \| null` | Inherits from `admin.login.requiredRole` or `'admin'` | Role(s) required to create/update/delete API keys. Set to `null` to allow any authenticated user (not recommended) |
609
-
610
- **Restricting API key management to admins:**
611
-
612
- If your `admin.login.requiredRole` includes non-admin roles (e.g., editors who need admin panel access but shouldn't manage API keys), set `requiredRole` explicitly:
613
-
614
- ```typescript
615
- createBetterAuthPlugin({
616
- createAuth,
617
- admin: {
618
- login: {
619
- requiredRole: ['admin', 'content_editor'], // both can access admin panel
620
- },
621
- apiKey: {
622
- requiredRole: 'admin', // only admins can create/update/delete API keys
623
- },
624
- },
625
- })
626
- ```
627
-
628
- > **Note:** API key **verification** is not affected by this setting — existing keys continue to work regardless of who created them. This only restricts key management (create, update, delete).
629
-
630
- **Zero Config (recommended):**
631
- ```typescript
632
- createBetterAuthPlugin({
633
- createAuth,
634
- // Auto-generates: posts:read, posts:write, posts:delete, etc.
635
- })
636
- ```
637
-
638
- **Custom Scopes:**
639
- ```typescript
640
- createBetterAuthPlugin({
641
- createAuth,
642
- admin: {
643
- apiKey: {
644
- scopes: {
645
- 'content:read': {
646
- label: 'Read Content',
647
- description: 'View posts and pages',
648
- permissions: { posts: ['read'], pages: ['read'] }
649
- },
650
- 'content:manage': {
651
- label: 'Manage Content',
652
- description: 'Full content management',
653
- permissions: { posts: ['*'], pages: ['*'] }
654
- }
655
- },
656
- defaultScopes: ['content:read']
657
- }
658
- }
659
- })
660
- ```
661
-
662
- **Hybrid (custom + auto-generated):**
663
- ```typescript
664
- admin: {
665
- apiKey: {
666
- scopes: { 'content:manage': { /* ... */ } },
667
- includeCollectionScopes: true, // Also include posts:read, etc.
668
- excludeCollections: ['users', 'sessions']
669
- }
670
- }
671
- ```
672
-
673
- **How Permissions Work:**
674
-
675
- When creating API keys through the admin UI, users select scopes (e.g., `posts:read`, `posts:write`). These scopes are converted to Better Auth permissions server-side for security - the conversion happens in the Payload endpoint handler before calling Better Auth's API. This ensures that permission assignment is always controlled server-side, following Better Auth's security model.
676
-
677
- ### `betterAuthStrategy(options?)`
678
-
679
- Payload auth strategy for Better Auth session validation.
680
-
681
- ```ts
682
- betterAuthStrategy({
683
- usersCollection: 'users',
684
- idType: 'number', // default — coerces session field IDs for serial IDs
685
- })
686
- ```
687
-
688
- | Option | Type | Description |
689
- |--------|------|-------------|
690
- | `usersCollection` | `string` | The collection slug for users (default: `'users'`) |
691
- | `idType` | `'number' \| 'text'` | Coerces string IDs in session fields (`activeOrganizationId`, etc.) to numbers. Defaults to `'number'` matching the adapter default. Set to `'text'` for UUID IDs. |
692
-
693
- ### `getServerSession<TUser>(payload, headers)`
694
-
695
- Get the current session on the server. Pass your Payload `User` type for full type safety:
696
-
697
- ```ts
698
- import { getServerSession } from '@delmaredigital/payload-better-auth'
699
- import type { User } from '@/payload-types'
700
-
701
- const session = await getServerSession<User>(payload, headersList)
702
- // session.user.role, session.user.firstName, etc. are fully typed
703
- ```
704
-
705
- ### `getServerUser<TUser>(payload, headers)`
706
-
707
- Get the current user on the server (shorthand for `session.user`):
708
-
709
- ```ts
710
- import { getServerUser } from '@delmaredigital/payload-better-auth'
711
- import type { User } from '@/payload-types'
712
-
713
- const user = await getServerUser<User>(payload, headersList)
714
- // user.role, user.firstName, etc. are fully typed
715
- ```
716
-
717
- ### `createSessionHelpers<TUser>(options?)`
718
-
719
- Create typed session helpers bound to your User type. Define once, import everywhere — no generics needed at call sites:
720
-
721
- ```ts
722
- // lib/auth.ts
723
- import { createSessionHelpers } from '@delmaredigital/payload-better-auth'
724
- import type { User } from '@/payload-types'
725
-
726
- export const { getServerSession, getServerUser } = createSessionHelpers<User>()
727
- ```
728
-
729
- ```ts
730
- // app/page.tsx
731
- import { getServerSession } from '@/lib/auth'
732
-
733
- const session = await getServerSession(payload, headersList)
734
- // session.user is typed as User — no generic needed
735
- ```
736
-
737
- **Serial IDs (Payload default):** Better Auth always returns string IDs from `api.getSession()`, which causes Payload relationship fields to reject them. Pass `idType: 'number'` to coerce ID fields to numbers automatically:
738
-
739
- ```ts
740
- export const { getServerSession, getServerUser } = createSessionHelpers<User>({
741
- idType: 'number', // coerces user.id, session.userId, etc. to numbers
742
- })
743
- ```
744
-
745
- | Option | Type | Description |
746
- |--------|------|-------------|
747
- | `idType` | `'number' \| 'text'` | Set to `'number'` when using serial IDs to coerce string IDs to numbers. Matches the adapter's `adapterConfig.idType` option. |
748
-
749
- ### `withBetterAuthDefaults(options)`
750
-
751
- Applies sensible defaults to Better Auth options. Useful for simplifying common configurations.
752
-
753
- ```ts
754
- import { withBetterAuthDefaults } from '@delmaredigital/payload-better-auth'
755
-
756
- betterAuth(withBetterAuthDefaults({
757
- baseURL: 'https://myapp.com',
758
- // trustedOrigins automatically becomes ['https://myapp.com']
759
- }))
760
- ```
761
-
762
- | Default Applied | Condition |
763
- |-----------------|-----------|
764
- | `trustedOrigins: [baseURL]` | When `trustedOrigins` is not set but `baseURL` is |
765
-
766
- Explicit values are never overridden - if you set `trustedOrigins` manually, it won't be changed.
767
-
768
- ### `apiKeyWithDefaults(options?)`
769
-
770
- Wraps Better Auth's `apiKey()` plugin with sensible defaults for use with this package.
771
-
772
- ```ts
773
- import { apiKeyWithDefaults } from '@delmaredigital/payload-better-auth'
774
-
775
- export const betterAuthOptions = {
776
- plugins: [
777
- apiKeyWithDefaults(), // Use instead of apiKey()
778
- ],
779
- }
780
- ```
781
-
782
- | Default Applied | Purpose |
783
- |-----------------|---------|
784
- | `enableMetadata: true` | Allows storing scope names for display in admin UI |
785
-
786
- **Why use this?**
787
-
788
- Better Auth's `apiKey()` plugin disables metadata by default. When you create API keys with scopes through the admin UI, the selected scopes need to be stored in metadata to display them later. Without metadata enabled, keys are created successfully but scopes won't appear in the UI.
789
-
790
- You can still pass any `apiKey()` options:
791
-
792
- ```ts
793
- apiKeyWithDefaults({
794
- rateLimit: { max: 100, window: 60 },
795
- // enableMetadata is already true
796
- })
797
- ```
164
+ **That's it!** The plugin automatically registers auth API endpoints at `/api/auth/*`, injects admin UI components, and handles session management.
798
165
 
799
166
  ---
800
167
 
801
- ## Customization
802
-
803
- ### Role-Based Access Control
804
-
805
- By default, the login page checks that users have the `admin` role before allowing access to the admin panel. Users without the required role see an "Access Denied" message.
806
-
807
- ```ts
808
- createBetterAuthPlugin({
809
- createAuth,
810
- admin: {
811
- login: {
812
- // Default: 'admin' - only users with role='admin' can access
813
- requiredRole: 'admin',
814
-
815
- // Use a different role name
816
- requiredRole: 'editor',
817
-
818
- // Multiple roles - any of these grants access
819
- requiredRole: ['admin', 'editor', 'moderator'],
820
-
821
- // Require ALL roles (instead of any)
822
- requiredRole: ['admin', 'content-manager'],
823
- requireAllRoles: true,
824
-
825
- // Disable role checking entirely
826
- requiredRole: null,
827
- },
828
- },
829
- })
830
- ```
831
-
832
- **For complex RBAC** (multiple roles, permissions, etc.), disable the login view and create your own:
833
-
834
- ```ts
835
- createBetterAuthPlugin({
836
- createAuth,
837
- admin: {
838
- disableLoginView: true,
839
- loginViewComponent: '@/components/admin/CustomLoginWithRBAC',
840
- },
841
- })
842
- ```
843
-
844
- You can use the built-in `LoginView` as a starting point:
845
-
846
- ```tsx
847
- // src/components/admin/CustomLoginWithRBAC.tsx
848
- 'use client'
849
-
850
- import { LoginView } from '@delmaredigital/payload-better-auth/components'
851
-
852
- // Option 1: Wrap and extend the built-in component
853
- export default function CustomLoginWithRBAC() {
854
- // Add your custom RBAC logic here
855
- return <LoginView requiredRole={null} /> // Disable built-in role check
856
- }
857
-
858
- // Option 2: Copy the LoginView source code from the package and customize fully
859
- // See: node_modules/@delmaredigital/payload-better-auth/dist/components/LoginView.js
860
- ```
861
-
862
- ### Disabling Auto-Injection
863
-
864
- If you prefer to handle API routes or admin components manually:
865
-
866
- ```ts
867
- createBetterAuthPlugin({
868
- createAuth,
869
- autoRegisterEndpoints: false, // Handle API route yourself
870
- autoInjectAdminComponents: false, // Handle admin components yourself
871
- })
872
- ```
873
-
874
- **Disabling Only the LoginView:**
875
-
876
- To disable just the login view while keeping other auto-injected components:
877
-
878
- ```ts
879
- createBetterAuthPlugin({
880
- createAuth,
881
- admin: {
882
- disableLoginView: true,
883
- // Optionally provide your own:
884
- loginViewComponent: '@/components/admin/CustomLogin',
885
- },
886
- })
887
- ```
888
-
889
- This is useful when you need:
890
- - Complex RBAC logic beyond simple role checks
891
- - Custom 2FA flows different from the built-in inline handling
892
- - Integration with external identity providers
893
- - Custom branding or UI requirements
894
-
895
- **Frontend Login (outside admin panel):**
896
-
897
- For user-facing login pages (not the Payload admin), you don't need to configure anything in the plugin. Just use the auth client directly in your own React components:
898
-
899
- ```tsx
900
- import { authClient } from '@/lib/auth/client'
901
-
902
- // Use authClient.signIn.email(), authClient.signUp.email(), etc.
903
- // See "Handling 2FA in Custom Login Forms" section below for a complete example
904
- ```
905
-
906
- ### Custom Admin Components
907
-
908
- Override specific admin components while keeping others auto-injected:
909
-
910
- ```ts
911
- createBetterAuthPlugin({
912
- createAuth,
913
- admin: {
914
- // Use custom components (import map format)
915
- loginViewComponent: '@/components/admin/CustomLogin',
916
- logoutButtonComponent: '@/components/admin/CustomLogout',
917
-
918
- // Or disable specific components
919
- disableBeforeLogin: true,
920
- },
921
- })
922
- ```
923
-
924
- ### Manual API Route (Advanced)
925
-
926
- If you disable `autoRegisterEndpoints`, create your own route:
927
-
928
- ```ts
929
- // src/app/api/auth/[...all]/route.ts
930
- import { getPayload } from 'payload'
931
- import config from '@payload-config'
932
- import type { NextRequest } from 'next/server'
933
- import type { PayloadWithAuth } from '@delmaredigital/payload-better-auth'
934
-
935
- export async function GET(request: NextRequest) {
936
- const payload = (await getPayload({ config })) as PayloadWithAuth
937
- return payload.betterAuth.handler(request)
938
- }
939
-
940
- export async function POST(request: NextRequest) {
941
- const payload = (await getPayload({ config })) as PayloadWithAuth
942
- return payload.betterAuth.handler(request)
943
- }
944
- ```
945
-
946
- ---
947
-
948
- ## Access Control Helpers
949
-
950
- Pre-built access control functions for common authorization patterns.
951
-
952
- ### Role-Based Access
953
-
954
- ```typescript
955
- import {
956
- isAdmin,
957
- isAdminField,
958
- isAdminOrSelf,
959
- hasRole,
960
- requireAllRoles,
961
- isAuthenticated,
962
- isAuthenticatedField,
963
- } from '@delmaredigital/payload-better-auth'
964
-
965
- export const Posts: CollectionConfig = {
966
- slug: 'posts',
967
- access: {
968
- read: isAuthenticated(), // Any logged-in user
969
- create: hasRole(['editor', 'admin']), // Any of these roles
970
- update: hasRole(['editor', 'admin']),
971
- delete: requireAllRoles(['admin', 'content-manager']), // Must have ALL roles
972
- },
973
- fields: [
974
- {
975
- name: 'title',
976
- type: 'text',
977
- },
978
- {
979
- name: 'internalNotes',
980
- type: 'textarea',
981
- access: {
982
- read: isAdminField(), // Only admins can read this field
983
- },
984
- },
985
- ],
986
- }
987
- ```
988
-
989
- ### Self-Access Patterns
990
-
991
- ```typescript
992
- import { isAdminOrSelf, canUpdateOwnFields } from '@delmaredigital/payload-better-auth'
993
-
994
- export const Users: CollectionConfig = {
995
- slug: 'users',
996
- access: {
997
- // Admins can read all; users can only read themselves
998
- read: isAdminOrSelf({ adminRoles: ['admin'] }),
999
-
1000
- // Users can update only specific fields on their own profile
1001
- update: canUpdateOwnFields({
1002
- allowedFields: ['name', 'image', 'password'],
1003
- userSlug: 'users',
1004
- requireCurrentPassword: true, // Require currentPassword for password changes
1005
- }),
1006
-
1007
- // Only admins can delete
1008
- delete: isAdmin({ adminRoles: ['admin'] }),
1009
- },
1010
- // ...
1011
- }
1012
- ```
1013
-
1014
- ### Utility Functions
1015
-
1016
- ```typescript
1017
- import { normalizeRoles, hasAnyRole, hasAllRoles, hasAdminRoles } from '@delmaredigital/payload-better-auth'
1018
-
1019
- // Normalize role field (handles string, array, or comma-separated)
1020
- const roles = normalizeRoles(user.role) // ['admin', 'editor']
1021
-
1022
- // Check role membership
1023
- hasAnyRole(user, ['admin', 'editor']) // true if user has any
1024
- hasAllRoles(user, ['admin', 'editor']) // true if user has all
1025
- hasAdminRoles(user, { adminRoles: ['admin', 'super-admin'] }) // true if admin
1026
- ```
1027
-
1028
- ---
1029
-
1030
- ## API Key Scope Enforcement
1031
-
1032
- Enforce API key scopes in your Payload access control. API keys can have granular permission scopes that control what resources they can access.
1033
-
1034
- ### Basic Usage
1035
-
1036
- ```typescript
1037
- import { requireScope, requireAnyScope, requireAllScopes } from '@delmaredigital/payload-better-auth'
1038
-
1039
- export const Posts: CollectionConfig = {
1040
- slug: 'posts',
1041
- access: {
1042
- read: requireScope('posts:read'),
1043
- create: requireScope('posts:write'),
1044
- update: requireScope('posts:write'),
1045
- delete: requireAllScopes(['posts:delete', 'admin:write']), // Must have both
1046
- },
1047
- }
1048
- ```
1049
-
1050
- ### Wildcard Scopes
1051
-
1052
- ```typescript
1053
- // API key with scope 'posts:*' matches 'posts:read', 'posts:write', 'posts:delete'
1054
- // API key with scope '*' matches everything
1055
- ```
1056
-
1057
- ### Allow Both Session and API Key
1058
-
1059
- ```typescript
1060
- import { allowSessionOrScope, allowSessionOrAnyScope } from '@delmaredigital/payload-better-auth'
1061
-
1062
- export const Posts: CollectionConfig = {
1063
- slug: 'posts',
1064
- access: {
1065
- // Allow authenticated users OR API keys with the scope
1066
- read: allowSessionOrScope('posts:read'),
1067
- create: allowSessionOrAnyScope(['posts:write', 'content:manage']),
1068
- },
1069
- }
1070
- ```
1071
-
1072
- ### Manual Validation
1073
-
1074
- ```typescript
1075
- import { validateApiKey, extractApiKeyFromRequest, hasScope } from '@delmaredigital/payload-better-auth'
1076
-
1077
- // In a custom endpoint
1078
- async function myEndpoint({ req }) {
1079
- const keyInfo = await validateApiKey(req)
1080
-
1081
- if (!keyInfo) {
1082
- return Response.json({ error: 'Invalid API key' }, { status: 401 })
1083
- }
1084
-
1085
- // Check specific scope
1086
- if (!hasScope(keyInfo.scopes, 'custom:action')) {
1087
- return Response.json({ error: 'Insufficient permissions' }, { status: 403 })
1088
- }
1089
-
1090
- // keyInfo contains: { id, userId, scopes, keyPrefix, metadata }
1091
- return Response.json({ userId: keyInfo.userId })
1092
- }
1093
- ```
1094
-
1095
- ### Configuration Options
1096
-
1097
- ```typescript
1098
- requireScope('posts:read', {
1099
- apiKeysCollection: 'apikeys', // Collection slug (auto-detected)
1100
- allowAuthenticatedUsers: false, // Also allow session auth
1101
- extractApiKey: (req) => { ... }, // Custom extraction function
1102
- })
1103
- ```
1104
-
1105
- ---
1106
-
1107
- ## Plugin Compatibility
1108
-
1109
- The adapter uses Better Auth's `createAdapterFactory` which is **schema-aware** - it automatically supports all Better Auth plugins without additional configuration. Just install the plugin, add it to your config, and our adapter handles the rest.
1110
-
1111
- ### How It Works
1112
-
1113
- 1. **You install** the plugin package (if separate from core)
1114
- 2. **You configure** the plugin in Better Auth options
1115
- 3. **Our adapter automatically**:
1116
- - Creates the necessary collections via `betterAuthCollections()`
1117
- - Handles all CRUD operations via schema-aware transformations
1118
- - No plugin-specific adapter configuration needed
1119
-
1120
- ### Supported Plugins
1121
-
1122
- | Plugin | Package | Notes |
1123
- |--------|---------|-------|
1124
- | OAuth | `better-auth` (core) | Uses accounts collection |
1125
- | Magic Link | `better-auth` (core) | Uses verifications collection |
1126
- | Email Verification | `better-auth` (core) | Uses verifications collection |
1127
- | Email OTP | `better-auth` (core) | Uses verifications collection |
1128
- | Password Reset | `better-auth` (core) | Uses verifications collection |
1129
- | Two-Factor (TOTP) | `better-auth` (core) | Auto-generates twoFactors collection |
1130
- | API Keys | `better-auth` (core) | Auto-generates apikeys collection |
1131
- | Organizations | `better-auth` (core) | Auto-generates organizations, members, invitations |
1132
- | Admin | `better-auth` (core) | Adds admin fields to users |
1133
- | Passkey | `@better-auth/passkey` (peer dep) | Auto-generates passkeys collection |
1134
-
1135
- ### Example: Core Plugins
1136
-
1137
- Core plugins are included in `better-auth`:
1138
-
1139
- ```typescript
1140
- import { twoFactor, organization, admin } from 'better-auth/plugins'
1141
- import { apiKeyWithDefaults } from '@delmaredigital/payload-better-auth'
1142
-
1143
- betterAuth({
1144
- database: payloadAdapter({ payloadClient: payload }),
1145
- plugins: [
1146
- twoFactor(),
1147
- apiKeyWithDefaults(), // Use this instead of apiKey() for better admin UI support
1148
- organization(),
1149
- admin(),
1150
- ],
1151
- })
1152
- ```
1153
-
1154
- ### Adding Join Fields for Relationships
1155
-
1156
- Some plugins create related data (e.g., user's API keys). To query these relationships from the parent, add join fields:
1157
-
1158
- <details>
1159
- <summary><strong>Why Join Fields?</strong></summary>
1160
-
1161
- Payload uses `join` fields to establish queryable relationships from parent to child. Without them, you can still query the child collection directly (e.g., find API keys by userId), but you can't include them when fetching the parent (e.g., get user with their API keys).
1162
-
1163
- </details>
1164
-
1165
- ### Enabling Plugins That Need Joins
1166
-
1167
- Some Better Auth plugins expect to access related data via joins (e.g., `user.apiKeys`). Payload handles this via `join` fields. Below are the patterns for each plugin.
1168
-
1169
- <details>
1170
- <summary><strong>API Keys</strong></summary>
1171
-
1172
- The API Keys plugin creates an `apiKey` model with a `userId` reference.
1173
-
1174
- **1. Add to your Better Auth config:**
1175
-
1176
- ```ts
1177
- import { apiKeyWithDefaults } from '@delmaredigital/payload-better-auth'
1178
-
1179
- export const betterAuthOptions: Partial<BetterAuthOptions> = {
1180
- // ... existing config
1181
- plugins: [apiKeyWithDefaults()], // Enables metadata for scope display in admin UI
1182
- }
1183
- ```
1184
-
1185
- > **Note:** `apiKeyWithDefaults()` wraps Better Auth's `apiKey()` plugin with `enableMetadata: true` so that selected scopes are stored and displayed in the admin UI. You can still use the raw `apiKey()` from `better-auth/plugins` if you don't need this feature.
1186
-
1187
- **2. Add join field to your Users collection:**
1188
-
1189
- ```ts
1190
- // src/collections/Users.ts
1191
- export const Users: CollectionConfig = {
1192
- slug: 'users',
1193
- // ... existing config
1194
- fields: [
1195
- // ... existing fields
1196
- {
1197
- name: 'apiKeys',
1198
- type: 'join',
1199
- collection: 'apikeys', // Auto-generated collection (lowercase)
1200
- on: 'user', // The field in apikeys that references users
1201
- },
1202
- ],
1203
- }
1204
- ```
1205
-
1206
- **3. (Optional) Configure permission scopes:**
1207
-
1208
- The API Keys management UI (`/admin/security/api-keys`) lets users select permission scopes when creating keys. By default, scopes are auto-generated from your Payload collections. See [API Key Scopes Configuration](#api-key-scopes-configuration) for customization options.
1209
- </details>
1210
-
1211
- <details>
1212
- <summary><strong>Two-Factor Auth (TOTP)</strong></summary>
1213
-
1214
- The Two-Factor plugin creates a `twoFactor` model and adds a `twoFactorEnabled` field to users.
1215
-
1216
- **1. Add to your Better Auth config:**
1217
-
1218
- ```ts
1219
- import { twoFactor } from 'better-auth/plugins'
1220
-
1221
- export const betterAuthOptions: Partial<BetterAuthOptions> = {
1222
- // ... existing config
1223
- plugins: [twoFactor()],
1224
- }
1225
- ```
1226
-
1227
- **2. (Automatic) The `twoFactorEnabled` field is auto-added:**
1228
-
1229
- The `betterAuthCollections()` plugin automatically adds `twoFactorEnabled` to your Users collection. You'll see:
1230
- ```
1231
- [better-auth] Auto-adding fields to 'users': ['twoFactorEnabled']
1232
- ```
1233
-
1234
- **3. (Optional) Add join field for querying user's 2FA records:**
1235
-
1236
- ```ts
1237
- // src/collections/Users.ts
1238
- export const Users: CollectionConfig = {
1239
- slug: 'users',
1240
- fields: [
1241
- // ... existing fields
1242
- {
1243
- name: 'twoFactor',
1244
- type: 'join',
1245
- collection: 'twoFactors',
1246
- on: 'user',
1247
- },
1248
- ],
1249
- }
1250
- ```
1251
-
1252
- **4. (Optional) Add UI components:**
1253
-
1254
- See [Two-Factor Authentication Flow](#two-factor-authentication-flow) for pre-built setup and verification components.
1255
- </details>
1256
-
1257
- <details>
1258
- <summary><strong>Organizations</strong></summary>
1259
-
1260
- The Organizations plugin creates multiple models: `organization`, `member`, and `invitation`.
1261
-
1262
- **1. Add to your Better Auth config:**
1263
-
1264
- ```ts
1265
- import { organization } from 'better-auth/plugins'
1266
-
1267
- export const betterAuthOptions: Partial<BetterAuthOptions> = {
1268
- // ... existing config
1269
- plugins: [organization()],
1270
- }
1271
- ```
1272
-
1273
- **2. Add join field to your Users collection:**
1274
-
1275
- ```ts
1276
- // src/collections/Users.ts
1277
- export const Users: CollectionConfig = {
1278
- slug: 'users',
1279
- fields: [
1280
- // ... existing fields
1281
- {
1282
- name: 'memberships',
1283
- type: 'join',
1284
- collection: 'members', // Auto-generated collection
1285
- on: 'user',
1286
- },
1287
- ],
1288
- }
1289
- ```
1290
-
1291
- **3. (Optional) Customize the Organizations collection:**
1292
-
1293
- ```ts
1294
- betterAuthCollections({
1295
- betterAuthOptions,
1296
- customizeCollection: (modelKey, collection) => {
1297
- if (modelKey === 'organization') {
1298
- return {
1299
- ...collection,
1300
- fields: [
1301
- ...collection.fields,
1302
- {
1303
- name: 'members',
1304
- type: 'join',
1305
- collection: 'members',
1306
- on: 'organization',
1307
- },
1308
- {
1309
- name: 'invitations',
1310
- type: 'join',
1311
- collection: 'invitations',
1312
- on: 'organization',
1313
- },
1314
- ],
1315
- }
1316
- }
1317
- return collection
1318
- },
1319
- })
1320
- ```
1321
- </details>
1322
-
1323
- ### General Pattern for Joins
1324
-
1325
- When a Better Auth plugin creates a model with a foreign key (e.g., `userId`, `organizationId`), you need to:
1326
-
1327
- 1. **Add a join field** to the parent collection pointing to the child collection
1328
- 2. **Specify the `on` field** - this is the relationship field name in the child collection (without `Id` suffix)
1329
-
1330
- The auto-generated collections create relationship fields like `user` (from `userId`), so your join's `on` property should match that field name.
1331
-
1332
- **Collection Slug Casing:** Collection slugs are derived from Better Auth's model names (pluralized). Some plugins use lowercase (`apikey` → `apikeys`, `passkey` → `passkeys`) while others use camelCase (`twoFactor` → `twoFactors`). Always check the actual slug in your Payload admin panel if unsure.
1333
-
1334
- ### Cascade Delete (Cleanup Orphaned Records)
1335
-
1336
- When a user is deleted, their related records (sessions, accounts, API keys, passkeys, etc.) become orphaned. Better Auth provides an `afterDelete` hook for cleanup:
1337
-
1338
- ```typescript
1339
- // src/lib/auth/index.ts
1340
- import { betterAuth } from 'better-auth'
1341
- import { payloadAdapter } from '@delmaredigital/payload-better-auth'
1342
-
1343
- export const auth = betterAuth({
1344
- database: payloadAdapter({ payloadClient: payload }),
1345
- user: {
1346
- deleteUser: {
1347
- enabled: true,
1348
- afterDelete: async (user) => {
1349
- // Clean up all related records
1350
- const collections = ['sessions', 'accounts', 'apikeys', 'passkeys', 'twoFactors']
1351
-
1352
- for (const collection of collections) {
1353
- try {
1354
- await payload.delete({
1355
- collection,
1356
- where: { user: { equals: user.id } },
1357
- })
1358
- } catch {
1359
- // Collection may not exist if plugin not enabled
1360
- }
1361
- }
1362
- },
1363
- },
1364
- },
1365
- // ... other options
1366
- })
1367
- ```
1368
-
1369
- **Note:** This is optional. Orphaned records don't cause errors (Payload doesn't enforce foreign key constraints), but cleanup keeps your database tidy.
1370
-
1371
- ---
1372
-
1373
- ## Additional UI Components
1374
-
1375
- ### User Registration
1376
-
1377
- The `LoginView` **automatically detects** if user registration is available by checking Better Auth's sign-up endpoint. If your Better Auth config has `emailAndPassword.enabled: true` (and not `disableSignUp: true`), the "Create account" link appears automatically.
1378
-
1379
- **No configuration needed** for most cases - it just works based on your Better Auth settings.
1380
-
1381
- **Optional overrides** (only if you need to force show/hide):
1382
- ```typescript
1383
- createBetterAuthPlugin({
1384
- createAuth,
1385
- admin: {
1386
- login: {
1387
- // These options override auto-detection (usually not needed)
1388
- enableSignUp: 'auto', // 'auto' (default) | true | false
1389
- defaultSignUpRole: 'user', // Role assigned to new users (default: 'user')
1390
- },
1391
- },
1392
- })
1393
- ```
1394
-
1395
- **Notes:**
1396
- - `'auto'` (default): Detects availability from Better Auth's sign-up endpoint
1397
- - `true`: Always show registration (even if Better Auth returns 404)
1398
- - `false`: Never show registration
1399
- - New users are assigned `defaultSignUpRole` (default: `'user'`)
1400
- - If email verification is required, users see a success message to check their email
1401
- - Role-based access control still applies - users without `requiredRole` see "Access Denied"
1402
-
1403
- ### Password Reset Flow
1404
-
1405
- The `LoginView` **automatically detects** if password reset is available by checking Better Auth's reset endpoint. The "Forgot password?" link appears automatically when available.
1406
-
1407
- **No configuration needed** for most cases - it just works based on your Better Auth settings.
1408
-
1409
- **Optional overrides** (only if you need to force show/hide or use a custom URL):
1410
- ```typescript
1411
- createBetterAuthPlugin({
1412
- createAuth,
1413
- admin: {
1414
- login: {
1415
- // These options override auto-detection (usually not needed)
1416
- enableForgotPassword: 'auto', // 'auto' (default) | true | false
1417
- resetPasswordUrl: '/custom-reset', // Optional: redirect to custom page instead of inline form
1418
- },
1419
- },
1420
- })
1421
- ```
1422
-
1423
- #### Standalone Components
1424
-
1425
- For custom password reset pages outside the admin panel:
1426
-
1427
- ```typescript
1428
- import { ForgotPasswordView, ResetPasswordView } from '@delmaredigital/payload-better-auth/components/auth'
1429
- ```
1430
-
1431
- **ForgotPasswordView** - Email input form to request a password reset link.
1432
-
1433
- ```tsx
1434
- <ForgotPasswordView
1435
- logo={<MyLogo />}
1436
- title="Forgot Password"
1437
- loginPath="/admin/login"
1438
- successMessage="Check your email for a reset link."
1439
- />
1440
- ```
1441
-
1442
- **ResetPasswordView** - Form to set a new password (expects `?token=` in URL).
1443
-
1444
- ```tsx
1445
- <ResetPasswordView
1446
- logo={<MyLogo />}
1447
- title="Reset Password"
1448
- afterResetPath="/admin/login"
1449
- minPasswordLength={8}
1450
- />
1451
- ```
1452
-
1453
- ### Two-Factor Authentication Flow
1454
-
1455
- The plugin's `LoginView` **automatically handles 2FA verification inline**. When a user with 2FA enabled signs in:
1456
-
1457
- 1. User enters email/password and submits
1458
- 2. If 2FA is enabled, the form transitions to a TOTP code input step
1459
- 3. User enters their 6-digit code from their authenticator app
1460
- 4. Upon successful verification, the user is redirected to the admin panel
1461
-
1462
- **No additional configuration required** - the LoginView handles the full flow automatically.
1463
-
1464
- #### Standalone Components for Custom Flows
1465
-
1466
- For custom frontend implementations (outside the admin panel), use these components:
1467
-
1468
- ```typescript
1469
- import { TwoFactorSetupView, TwoFactorVerifyView } from '@delmaredigital/payload-better-auth/components/twoFactor'
1470
- ```
1471
-
1472
- **TwoFactorSetupView** - QR code display, manual secret entry, backup codes, and verification.
1473
-
1474
- ```tsx
1475
- <TwoFactorSetupView
1476
- logo={<MyLogo />}
1477
- title="Set Up Two-Factor Authentication"
1478
- afterSetupPath="/admin"
1479
- onSetupComplete={() => console.log('2FA enabled!')}
1480
- />
1481
- ```
1482
-
1483
- **TwoFactorVerifyView** - TOTP code or backup code entry during login.
1484
-
1485
- ```tsx
1486
- <TwoFactorVerifyView
1487
- logo={<MyLogo />}
1488
- title="Two-Factor Authentication"
1489
- afterVerifyPath="/admin"
1490
- onVerifyComplete={() => console.log('Verified!')}
1491
- />
1492
- ```
1493
-
1494
- All components use Payload CSS variables for native theme integration (light/dark mode).
1495
-
1496
- #### Handling 2FA in Custom Login Forms
1497
-
1498
- **Important:** If you have a custom login form (outside the admin panel), you **must** check for `twoFactorRedirect` in the sign-in response. Without this check, users with 2FA enabled will appear to log in but won't actually be authenticated.
1499
-
1500
- ```typescript
1501
- // src/lib/auth/client.ts
1502
- 'use client'
1503
-
1504
- import { createPayloadAuthClient } from '@delmaredigital/payload-better-auth/client'
1505
-
1506
- // Pre-configured with twoFactor, apiKey, and passkey plugins
1507
- export const authClient = createPayloadAuthClient()
1508
-
1509
- export const { signIn, signUp, signOut, twoFactor } = authClient
1510
- ```
1511
-
1512
- ```tsx
1513
- // src/components/auth/login-form.tsx
1514
- 'use client'
1515
-
1516
- import { useState } from 'react'
1517
- import { useRouter } from 'next/navigation'
1518
- import { signIn, twoFactor } from '@/lib/auth/client'
1519
-
1520
- export function LoginForm() {
1521
- const router = useRouter()
1522
- const [email, setEmail] = useState('')
1523
- const [password, setPassword] = useState('')
1524
- const [totpCode, setTotpCode] = useState('')
1525
- const [twoFactorRequired, setTwoFactorRequired] = useState(false)
1526
- const [error, setError] = useState<string | null>(null)
1527
- const [loading, setLoading] = useState(false)
1528
-
1529
- async function handleLogin(e: React.FormEvent) {
1530
- e.preventDefault()
1531
- setError(null)
1532
- setLoading(true)
1533
-
1534
- try {
1535
- const result = await signIn.email({ email, password })
1536
-
1537
- if (result.error) {
1538
- setError(result.error.message || 'Failed to sign in')
1539
- return
1540
- }
1541
-
1542
- // IMPORTANT: Check if 2FA is required
1543
- if (result.data?.twoFactorRedirect) {
1544
- setTwoFactorRequired(true)
1545
- return
1546
- }
1547
-
1548
- // Success - redirect to dashboard
1549
- router.push('/dashboard')
1550
- router.refresh()
1551
- } catch {
1552
- setError('An unexpected error occurred')
1553
- } finally {
1554
- setLoading(false)
1555
- }
1556
- }
1557
-
1558
- async function handleTotpVerify(e: React.FormEvent) {
1559
- e.preventDefault()
1560
- setError(null)
1561
- setLoading(true)
1562
-
1563
- try {
1564
- const result = await twoFactor.verifyTotp({ code: totpCode })
1565
-
1566
- if (result.error) {
1567
- setError(result.error.message || 'Invalid verification code')
1568
- return
1569
- }
1570
-
1571
- // Success - redirect to dashboard
1572
- router.push('/dashboard')
1573
- router.refresh()
1574
- } catch {
1575
- setError('An unexpected error occurred')
1576
- } finally {
1577
- setLoading(false)
1578
- }
1579
- }
1580
-
1581
- // Show TOTP verification form
1582
- if (twoFactorRequired) {
1583
- return (
1584
- <form onSubmit={handleTotpVerify}>
1585
- <h2>Two-Factor Authentication</h2>
1586
- <p>Enter the 6-digit code from your authenticator app</p>
1587
- <input
1588
- type="text"
1589
- inputMode="numeric"
1590
- pattern="[0-9]*"
1591
- value={totpCode}
1592
- onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
1593
- placeholder="000000"
1594
- autoComplete="one-time-code"
1595
- />
1596
- {error && <div className="error">{error}</div>}
1597
- <button type="submit" disabled={loading || totpCode.length !== 6}>
1598
- {loading ? 'Verifying...' : 'Verify'}
1599
- </button>
1600
- <button type="button" onClick={() => setTwoFactorRequired(false)}>
1601
- Back to login
1602
- </button>
1603
- </form>
1604
- )
1605
- }
1606
-
1607
- // Show login form
1608
- return (
1609
- <form onSubmit={handleLogin}>
1610
- <input
1611
- type="email"
1612
- value={email}
1613
- onChange={(e) => setEmail(e.target.value)}
1614
- placeholder="Email"
1615
- required
1616
- />
1617
- <input
1618
- type="password"
1619
- value={password}
1620
- onChange={(e) => setPassword(e.target.value)}
1621
- placeholder="Password"
1622
- required
1623
- />
1624
- {error && <div className="error">{error}</div>}
1625
- <button type="submit" disabled={loading}>
1626
- {loading ? 'Signing in...' : 'Sign In'}
1627
- </button>
1628
- </form>
1629
- )
1630
- }
1631
- ```
1632
-
1633
- **Key Points:**
1634
- - Always check `result.data?.twoFactorRedirect` after `signIn.email()`
1635
- - When 2FA is required, the sign-in sets a temporary cookie - show the TOTP form immediately
1636
- - Use `twoFactor.verifyTotp({ code })` to complete authentication
1637
- - The TOTP cookie is session-scoped, so the user must complete verification in the same browser session
1638
-
1639
- ### Passkey Sign-In
1640
-
1641
- Enable passwordless authentication using WebAuthn passkeys.
1642
-
1643
- **Option 1: Enable in LoginView**
1644
-
1645
- ```typescript
1646
- createBetterAuthPlugin({
1647
- createAuth,
1648
- admin: {
1649
- login: {
1650
- enablePasskey: true, // Shows "Sign in with Passkey" button
1651
- },
1652
- },
1653
- })
1654
- ```
1655
-
1656
- **Option 2: Standalone Button for Custom Forms**
1657
-
1658
- ```typescript
1659
- import { PasskeySignInButton } from '@delmaredigital/payload-better-auth/components'
1660
-
1661
- function CustomLoginForm() {
1662
- return (
1663
- <div>
1664
- {/* Your email/password form */}
1665
-
1666
- <PasskeySignInButton
1667
- onSuccess={(user) => {
1668
- router.push('/dashboard')
1669
- }}
1670
- onError={(error) => {
1671
- setError(error)
1672
- }}
1673
- label="Sign in with Passkey"
1674
- loadingLabel="Authenticating..."
1675
- className="my-button-class" // Accepts all button props
1676
- />
1677
- </div>
1678
- )
1679
- }
1680
- ```
1681
-
1682
- The `PasskeySignInButton` handles the full WebAuthn authentication flow with Better Auth.
1683
-
1684
- ### Passkey Registration
1685
-
1686
- For registering new passkeys (e.g., in account security settings):
1687
-
1688
- **Bundled Component:**
1689
-
1690
- ```typescript
1691
- import { PasskeyRegisterButton } from '@delmaredigital/payload-better-auth/components'
1692
-
1693
- function SecuritySettings() {
1694
- return (
1695
- <PasskeyRegisterButton
1696
- passkeyName="My MacBook"
1697
- onSuccess={(passkey) => {
1698
- console.log('Passkey registered:', passkey.id)
1699
- refetchPasskeys()
1700
- }}
1701
- onError={(error) => {
1702
- setError(error)
1703
- }}
1704
- label="Add Passkey"
1705
- loadingLabel="Registering..."
1706
- />
1707
- )
1708
- }
1709
- ```
1710
-
1711
- **Using the Auth Client Directly:**
1712
-
1713
- ```typescript
1714
- import { createPayloadAuthClient } from '@delmaredigital/payload-better-auth/client'
1715
-
1716
- // Already includes passkeyClient plugin
1717
- const authClient = createPayloadAuthClient()
1718
-
1719
- // Register a new passkey
1720
- await authClient.passkey.addPasskey({ name: 'My Device' })
1721
-
1722
- // List user's passkeys
1723
- const { data: passkeys } = await authClient.passkey.listUserPasskeys()
1724
-
1725
- // Delete a passkey
1726
- await authClient.passkey.deletePasskey({ id: passkeyId })
1727
- ```
1728
-
1729
- **Full Passkey Management Component:**
1730
-
1731
- For building custom passkey management UIs, you can use the bundled management client:
1732
-
1733
- ```typescript
1734
- import { PasskeysManagementClient } from '@delmaredigital/payload-better-auth/management'
1735
-
1736
- function PasskeysSettingsPage() {
1737
- return <PasskeysManagementClient title="Manage Passkeys" />
1738
- }
1739
- ```
1740
-
1741
- This component provides a complete UI for listing, registering, and deleting passkeys, using Payload CSS variables for theme integration.
1742
-
1743
- ### Security Management UI
1744
-
1745
- The plugin auto-injects management views for security features based on which Better Auth plugins are enabled:
1746
-
1747
- | View | Path | Plugin Required |
1748
- |------|------|-----------------|
1749
- | Two-Factor Auth | `/admin/security/two-factor` | `twoFactor()` |
1750
- | API Keys | `/admin/security/api-keys` | `apiKey()` |
1751
- | Passkeys | `/admin/security/passkeys` | `passkey()` |
1752
-
1753
- A "Security" navigation section is added to the admin sidebar.
1754
-
1755
- **Configuration:**
1756
-
1757
- ```typescript
1758
- createBetterAuthPlugin({
1759
- createAuth,
1760
- admin: {
1761
- betterAuthOptions, // Required for plugin detection
1762
- enableManagementUI: true, // Default: true
1763
- },
1764
- })
1765
- ```
1766
-
1767
- **Note:** Sessions are managed via Payload's default collection view at `/admin/collections/sessions`.
1768
-
1769
- ---
1770
-
1771
- ## Recipes
1772
-
1773
- Common patterns and solutions for Better Auth integration.
1774
-
1775
- ### Auto-Create Organization on User Signup
1776
-
1777
- A common pattern is to automatically create a personal workspace/organization when a user signs up (and verifies their email). The key is to use Better Auth's organization API (`auth.api.createOrganization()`) rather than raw adapter calls, so that `organizationHooks` fire properly.
1778
-
1779
- **The Challenge:** Database hooks are defined in `betterAuthOptions` before the `auth` instance exists, so you can't directly reference `auth.api` in your hooks.
1780
-
1781
- **The Solution:** Use a lazy auth instance singleton:
1782
-
1783
- **Step 1: Create an auth instance singleton**
1784
-
1785
- ```typescript
1786
- // src/lib/auth/instance.ts
1787
- import type { betterAuth } from 'better-auth'
1788
-
1789
- type AuthInstance = ReturnType<typeof betterAuth>
1790
-
1791
- let authInstance: AuthInstance | null = null
1792
-
1793
- export function setAuthInstance(auth: AuthInstance): void {
1794
- authInstance = auth
1795
- }
1796
-
1797
- export function getAuthInstance(): AuthInstance {
1798
- if (!authInstance) {
1799
- throw new Error('Auth not initialized')
1800
- }
1801
- return authInstance
1802
- }
1803
- ```
1804
-
1805
- **Step 2: Store the instance after creation**
1806
-
1807
- ```typescript
1808
- // src/payload.config.ts (or wherever you configure plugins)
1809
- import { setAuthInstance } from '@/lib/auth/instance'
1810
-
1811
- createBetterAuthPlugin({
1812
- createAuth: (payload) => {
1813
- const auth = betterAuth({
1814
- ...betterAuthOptions,
1815
- database: payloadAdapter({ payloadClient: payload }),
1816
- // ... other options
1817
- })
1818
-
1819
- // Store for use in database hooks
1820
- setAuthInstance(auth)
1821
-
1822
- return auth
1823
- },
1824
- })
1825
- ```
1826
-
1827
- **Step 3: Use the organization API in database hooks**
1828
-
1829
- ```typescript
1830
- // src/lib/auth/config.ts
1831
- import { getAuthInstance } from './instance'
1832
-
1833
- export const betterAuthOptions: Partial<BetterAuthOptions> = {
1834
- // ... other options
1835
-
1836
- plugins: [
1837
- organization({
1838
- // organizationHooks fire when using auth.api.createOrganization()
1839
- organizationHooks: {
1840
- afterCreateOrganization: async ({ organization }) => {
1841
- // This runs for ALL org creations - auto-created, manual, API
1842
- console.log(`Organization ${organization.id} created`)
1843
- // Create related records, send welcome email, etc.
1844
- },
1845
- },
1846
- }),
1847
- ],
1848
-
1849
- databaseHooks: {
1850
- user: {
1851
- update: {
1852
- after: async (user, ctx) => {
1853
- // Only proceed if user just verified their email
1854
- if (!user.emailVerified) return
1855
-
1856
- // Check if user already has an organization (e.g., joined via invitation)
1857
- const existingMembership = await ctx?.context?.adapter?.findOne({
1858
- model: 'member',
1859
- where: [{ field: 'userId', value: user.id }],
1860
- })
1861
- if (existingMembership) return
1862
-
1863
- // Create organization using the proper API
1864
- // This ensures organizationHooks.afterCreateOrganization fires
1865
- const auth = getAuthInstance()
1866
- await auth.api.createOrganization({
1867
- body: {
1868
- name: `${user.name}'s Workspace`,
1869
- slug: generateUniqueSlug(user.name),
1870
- userId: user.id,
1871
- },
1872
- })
1873
- },
1874
- },
1875
- },
1876
- },
1877
- }
1878
- ```
1879
-
1880
- **Why This Matters:**
1881
-
1882
- | Approach | organizationHooks fire? | Recommended? |
1883
- |----------|------------------------|--------------|
1884
- | `auth.api.createOrganization()` | ✅ Yes | ✅ Yes |
1885
- | `ctx.context.adapter.create({ model: 'organization' })` | ❌ No | ❌ No |
1886
-
1887
- Using the raw adapter bypasses Better Auth's organization plugin entirely. Any logic in `organizationHooks` (like creating related records, sending notifications, or syncing with external systems) won't run.
1888
-
1889
- **Handling Invitations:**
1890
-
1891
- Users who join via invitation already have a membership record (created when they accept the invitation), so the `existingMembership` check prevents creating a duplicate personal organization for them.
1892
-
1893
- ---
1894
-
1895
- ## Types
1896
-
1897
- The package exports comprehensive TypeScript types for Better Auth integration.
1898
-
1899
- ### Core Types
1900
-
1901
- ```typescript
1902
- import type {
1903
- PayloadWithAuth,
1904
- PayloadRequestWithBetterAuth,
1905
- BetterAuthReturn,
1906
- } from '@delmaredigital/payload-better-auth'
1907
-
1908
- // PayloadWithAuth - Payload instance with betterAuth attached
1909
- const payload = await getPayload({ config }) as PayloadWithAuth
1910
- const session = await payload.betterAuth.api.getSession({ headers })
1911
-
1912
- // PayloadRequestWithBetterAuth - Request type with typed payload
1913
- function myHook({ req }: { req: PayloadRequestWithBetterAuth }) {
1914
- const auth = req.payload.betterAuth
1915
- }
1916
- ```
1917
-
1918
- ### Hook and Endpoint Types
1919
-
1920
- ```typescript
1921
- import type {
1922
- CollectionHookWithBetterAuth,
1923
- EndpointWithBetterAuth,
1924
- } from '@delmaredigital/payload-better-auth'
1925
-
1926
- // Typed collection hooks with Better Auth access
1927
- const afterChangeHook: CollectionHookWithBetterAuth<typeof authOptions, MyDoc> = async ({
1928
- req,
1929
- doc,
1930
- }) => {
1931
- const session = await req.payload.betterAuth.api.getSession({ headers: req.headers })
1932
- // ...
1933
- return doc
1934
- }
1935
-
1936
- // Typed endpoints
1937
- const myEndpoint: EndpointWithBetterAuth<typeof authOptions> = {
1938
- path: '/custom',
1939
- method: 'get',
1940
- handler: async ({ req }) => {
1941
- const auth = req.payload.betterAuth
1942
- // ...
1943
- },
1944
- }
1945
- ```
1946
-
1947
- ### Generated Schema Types
1948
-
1949
- Auto-generated types for all Better Auth models:
1950
-
1951
- ```typescript
1952
- import type {
1953
- User,
1954
- BetterAuthSession,
1955
- Account,
1956
- Apikey,
1957
- Passkey,
1958
- Organization,
1959
- Member,
1960
- TwoFactor,
1961
- // ... and more
1962
- } from '@delmaredigital/payload-better-auth'
1963
- ```
1964
-
1965
- ### Type Generation
1966
-
1967
- Regenerate types from Better Auth schema (useful after adding plugins):
1968
-
1969
- ```bash
1970
- pnpm generate:types
1971
- ```
1972
-
1973
- ---
168
+ For MongoDB setup, API reference, customization, access control helpers, API key scopes, plugin compatibility, UI components (2FA, passkeys, password reset), recipes, and types — see the **[full documentation](https://delmaredigital.github.io/payload-better-auth/)**.
1974
169
 
1975
170
  ## License
1976
171