Plugin compatibility
Which Better Auth plugins work with the Payload adapter, which collections they generate, and how to add joins and cascade deletes.
The adapter is built on Better Auth's createAdapterFactory, which is schema-aware, and betterAuthCollections() generates a Payload collection for every model in the installed Better Auth schema. Better Auth plugins that add models or fields work without adapter changes.
| Plugin | Package | Notes |
|---|---|---|
| OAuth / social providers | better-auth | Uses the accounts collection. |
| Generic OAuth / OIDC | better-auth | Providers appear as social providers on the admin login. |
| Magic Link | better-auth | Uses verifications. Admin login UI supported. |
| Email OTP | better-auth | Uses verifications. Admin login UI supported. |
| Two-Factor | better-auth | Generates twoFactors. Admin login and management UI supported. |
| Organizations | better-auth | Generates organizations, members, invitations. |
| JWT | better-auth | Generates jwks. |
| API Keys | @better-auth/api-key | Generates apikeys. Admin management UI supported. |
| Passkey | @better-auth/passkey | Generates passkeys. Admin login and management UI supported. |
| OAuth Provider | @better-auth/oauth-provider | Generates the OAuth client and token collections. |
Slugs above assume the default usePlural: true.
Do not use nextCookies()
Better Auth's nextCookies() plugin is incompatible with Payload. It causes repeated form-state submissions and input resets in the admin panel. It exists for Server Actions; this plugin already passes cookies through its endpoint proxy. Remove it from your Better Auth plugins. The plugin warns at startup when it detects it.
Join fields
Payload uses join fields for queryable parent-to-child relationships. When a plugin creates a model with a foreign key to the user, add a join field to your users collection:
// API keys
{ name: 'apiKeys', type: 'join', collection: 'apikeys', on: 'user' }
// Two-factor
{ name: 'twoFactor', type: 'join', collection: 'twoFactors', on: 'user' }
// Organization memberships
{ name: 'memberships', type: 'join', collection: 'members', on: 'user' }Cascade delete
To clean up a deleted user's records, use Better Auth's deleteUser.afterDelete:
user: {
deleteUser: {
enabled: true,
afterDelete: async (user) => {
const collections = ['sessions', 'accounts', 'apikeys', 'passkeys', 'twoFactors']
for (const col of collections) {
try {
await payload.delete({ collection: col, where: { user: { equals: user.id } } })
} catch {} // Collection may not exist
}
},
},
}payload here is your Payload instance, in scope where you call betterAuth() inside createAuth: (payload) => ….

