MCPcopy Index your code
hub / github.com/delmaredigital/payload-better-auth

github.com/delmaredigital/payload-better-auth @v0.9.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.9.1 ↗ · + Follow
215 symbols 557 edges 98 files 30 documented · 14%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

@delmaredigital/payload-better-auth

Better Auth adapter and plugins for Payload CMS. Enables seamless integration between Better Auth and Payload.

Live Demo - Try It Now    Starter Template - Use This

Deploy with Vercel

🔧 Upgrading to 0.9? A second hardening pass (correctness + robustness) with a few breaking changes. Summary (full migration in the CHANGELOG):

  • Passkey on the admin login — the default LoginView no longer imports the optional @better-auth/passkey peer (it broke builds for consumers who hadn't installed it). To keep passkey sign-in on the admin login, point your login view at the passkey wrapper (added in 0.9.1): admin.loginViewComponent: '@delmaredigital/payload-better-auth/components/login-passkey#LoginViewWrapperWithPasskey'. Consumers who don't use passkey need no change.
  • starts_with/ends_with are now correctly anchored (were over-broad via Payload's non-anchored like).
  • Roles: a comma string is one role (normalizeRoles no longer splits) — use an array for multiple roles.
  • Password changes go through Better Auth's changePasswordcanUpdateOwnFields no longer verifies passwords (it was an unthrottled oracle).

Also: multi-instance safety, per-request API-key scope checks (no quota burn), and adapter correctness fixes. See the CHANGELOG.


🔒 Upgrading to 0.8? This is a security-hardening release with breaking changes. Summary (full migration in the CHANGELOG):

  • Roles are assigned server-side on sign-up. The admin login form no longer sends a role, and the first-user-admin hooks ignore any client-supplied role for non-first users. Action: if role is a Better Auth additionalField, set input: false; set the default self-sign-up role via firstUserAdmin: { defaultRole }. Closes a privilege-escalation path (POST { role: 'admin' }).
  • API-key scope enforcement for x-api-key. Keys sent via x-api-key are now scope-checked instead of being treated as a full session under allowSessionOrPermission / allowAuthenticatedUsers.
  • 2FA QR codes render locally (new qrcode.react dependency, auto-installed) — the TOTP secret is no longer sent to a third-party QR service.
  • Node >= 20.9 required; peer ranges capped to tested majors; defaultSignUpRole (LoginView prop) is deprecated and ignored.

See the CHANGELOG for full details and migration steps.


⚠️ Upgrading to 0.7? This release requires Better Auth 1.6 and includes several breaking changes:

  • Schema migration required for projects using the twoFactor plugin — Better Auth 1.6.2 added a verified column to the twoFactor table.
  • oidcProvider@better-auth/oauth-provider — generated OAuth types now reflect the oauth-provider schema. Consumers using oidcProvider() at runtime will keep working, but OauthApplication / PluginId / ModelKey type exports have changed shape. Migrating to @better-auth/oauth-provider is recommended.
  • Client helper type wideningcreatePayloadAuthClient() and payloadAuthPlugins are typed more conservatively to keep .d.ts portable. For typed plugin methods (e.g. client.twoFactor.verifyTotp), list plugins explicitly in createAuthClient({ plugins: [...] }) (see Client-Side Auth below).

See the CHANGELOG for full migration instructions.


Documentation

Full Documentation — API reference, guides, recipes, UI components, and more.

For AI-assisted exploration: DeepWiki


Install

pnpm add @delmaredigital/payload-better-auth better-auth

Requirements: payload >= 3.69.0 · better-auth >= 1.6.0 (1.6.23+ recommended) · next >= 15.5.16 · react >= 19.2.1 · Node >= 20.9

Quick Start

1. Auth Configuration

// src/lib/auth/config.ts
import type { BetterAuthOptions } from 'better-auth'

export const betterAuthOptions: Partial<BetterAuthOptions> = {
  user: {
    additionalFields: {
      // `input: false` keeps `role` server-only — clients cannot set it at
      // sign-up. Role is assigned by the first-user-admin hook; configure the
      // default self-sign-up role via `firstUserAdmin: { defaultRole }`.
      role: { type: 'string', defaultValue: 'user', input: false },
    },
  },
  emailAndPassword: { enabled: true },
}

2. Users Collection

// src/collections/Users/index.ts
import type { CollectionConfig } from 'payload'
import { betterAuthStrategy } from '@delmaredigital/payload-better-auth'

export const Users: CollectionConfig = {
  slug: 'users',
  auth: {
    disableLocalStrategy: true,
    strategies: [betterAuthStrategy()],
  },
  access: {
    read: ({ req }) => {
      if (!req.user) return false
      if (req.user.role === 'admin') return true
      return { id: { equals: req.user.id } }
    },
    admin: ({ req }) => req.user?.role === 'admin',
  },
  fields: [
    { name: 'email', type: 'email', required: true, unique: true },
    { name: 'emailVerified', type: 'checkbox', defaultValue: false },
    { name: 'name', type: 'text' },
    { name: 'image', type: 'text' },
    {
      name: 'role',
      type: 'select',
      defaultValue: 'user',
      options: [
        { label: 'User', value: 'user' },
        { label: 'Admin', value: 'admin' },
      ],
    },
  ],
}

3. Payload Config

// src/payload.config.ts
import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { betterAuth } from 'better-auth'
import {
  betterAuthCollections,
  createBetterAuthPlugin,
  payloadAdapter,
} from '@delmaredigital/payload-better-auth'
import { betterAuthOptions } from './lib/auth/config'
import { Users } from './collections/Users'
import { getBaseUrl } from './lib/auth/getBaseUrl'

const baseUrl = getBaseUrl()

export default buildConfig({
  collections: [Users],
  plugins: [
    betterAuthCollections({
      betterAuthOptions,
      skipCollections: ['user'],
    }),
    createBetterAuthPlugin({
      createAuth: (payload) =>
        betterAuth({
          ...betterAuthOptions,
          database: payloadAdapter({ payloadClient: payload }),
          advanced: { database: { generateId: 'serial' } },
          baseURL: baseUrl,
          secret: process.env.BETTER_AUTH_SECRET,
          trustedOrigins: [baseUrl],
        }),
    }),
  ],
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URL },
  }),
})

4. Client-Side Auth

// src/lib/auth/client.ts
'use client'

import { createAuthClient, twoFactorClient } from '@delmaredigital/payload-better-auth/client'
import { passkeyClient } from '@better-auth/passkey/client'

export const authClient = createAuthClient({
  plugins: [twoFactorClient(), passkeyClient()],
})

export const { useSession, signIn, signUp, signOut, twoFactor, passkey } = authClient

Listing plugins inline (rather than using createPayloadAuthClient() or spreading payloadAuthPlugins) ensures twoFactor and other plugin methods are typed on the returned client.

5. Server-Side Session

import { headers } from 'next/headers'
import { getPayload } from 'payload'
import { getServerSession } from '@delmaredigital/payload-better-auth'

export default async function Dashboard() {
  const payload = await getPayload({ config })
  const headersList = await headers()
  const session = await getServerSession(payload, headersList)

  if (!session) { redirect('/login') }

  return 

Hello {session.user.name}


}

That's it! The plugin automatically registers auth API endpoints at /api/auth/*, injects admin UI components, and handles session management.


For MongoDB setup, API reference, customization, access control helpers, API key scopes, plugin compatibility, UI components (2FA, passkeys, password reset, passwordless login via magic-link & email-OTP), recipes, and types — see the full documentation.

License

MIT

Extension points exported contracts — how you extend this code

VerifyApiKeyResult (Interface)
Minimal typed view of the api-key plugin endpoint we call.
src/utils/apiKeyAccess.ts
PayloadRequestWithBetterAuth (Interface)
(no doc)
src/types/betterAuth.ts
PayloadAuthClientOptions (Interface)
(no doc)
src/exports/client.ts
MockDocument (Interface)
(no doc)
tests/adapter/mocks.ts
DetectedMethods (Interface)
(no doc)
src/utils/loginMethods.ts
MockPayloadOptions (Interface)
(no doc)
tests/adapter/mocks.ts
AuthOptionsLike (Interface)
(no doc)
src/utils/loginMethods.ts
SocialProvider (Interface)
(no doc)
src/utils/loginMethods.ts

Core symbols most depended-on inside this repo

payloadAdapter
called by 31
src/adapter/index.ts
renderLogin
called by 22
tests/components/login/_harness.tsx
betterAuthStrategy
called by 17
src/plugin/index.ts
resolveAvailability
called by 15
src/utils/loginMethods.ts
getClient
called by 11
src/components/LoginView.tsx
apiKeyPermissionsToScopes
called by 10
src/plugin/index.ts
detectDbType
called by 9
src/adapter/index.ts
detectEnabledMethods
called by 8
src/utils/loginMethods.ts

Shape

Function 205
Interface 9
Method 1

Languages

TypeScript100%

Modules by API surface

src/components/LoginView.tsx18 symbols
src/utils/apiKeyAccess.ts15 symbols
src/components/management/ApiKeysManagementClient.tsx14 symbols
src/adapter/index.ts14 symbols
src/adapter/collections.ts14 symbols
src/utils/access.ts13 symbols
src/plugin/index.ts10 symbols
src/components/management/TwoFactorManagementClient.tsx10 symbols
src/utils/loginMethods.ts9 symbols
tests/adapter/mocks.ts7 symbols
src/components/management/PasskeysManagementClient.tsx6 symbols
src/utils/session.ts5 symbols

For agents

$ claude mcp add payload-better-auth \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page