BETTER-AUTH.

Changelog

All changes, fixes, and updates

Every release shipped to Better Auth, straight from GitHub.

CHANGELOG

better-auth

❗ Breaking Changes

  • Prevented unverified TOTP enrollment from blocking sign-in (#8711)

Migration: Adds a verified column to the twoFactor table (defaults to true). Existing rows are unaffected. No data migration required.

Features

  • Included enabled 2FA methods in sign-in redirect response (#8772)

Bug Fixes

  • Fixed OAuth state verification against cookie-stored nonce to prevent CSRF (#8949)
  • Fixed infinite router refresh loops in nextCookies() by replacing cookie probe with header-based RSC detection (#9059)
  • Fixed cross-provider account collision in link-social callback (#8983)
  • Included RelayState in signed SAML AuthnRequests (#9058)

For detailed changes, see CHANGELOG

@better-auth/oauth-provider

Bug Fixes

  • Fixed multi-valued query params collapsing through prompt redirects (#9060)
  • Rejected skip_consent at schema level in dynamic client registration (#8998)

For detailed changes, see CHANGELOG

@better-auth/sso

Bug Fixes

  • Fixed SAMLResponse decoding failures caused by line-wrapped base64 (#8968)

For detailed changes, see CHANGELOG

Contributors

Thanks to everyone who contributed to this release:

@aarmful, @cyphercodes, @dvanmali, @gustavovalverde, @jaydeep-pipaliya, @ping-maxwell

Full changelog: v1.6.1...v1.6.2

better-auth

Bug Fixes

  • Fixed endpoint instrumentation to always use the route template (#9023)
  • Returned INVALID_PASSWORD for all checkPassword failures (#8902)
  • Restored getSession accessibility in generic Auth<O> context (#9017)

For detailed changes, see CHANGELOG

Contributors

Thanks to everyone who contributed to this release:

@bytaesu, @jonathansamines, @ping-maxwell

Full changelog: v1.6.0...v1.6.1

Blog post: Better Auth 1.6

better-auth

❗ Breaking Changes

  • Aligned freshAge calculation with session creation time instead of update time (#8762)

Migration: session.freshAge now calculates from createdAt. Set session: { freshAge: 0 } to disable the check entirely.

Features

  • Added experimental OpenTelemetry instrumentation for endpoints, hooks, middleware, and database operations (#8027)
  • Added resendStrategy option to reuse existing OTP in email-otp plugin (#8560)
  • Added enable option for HaveIBeenPwned plugin (#8728)
  • Added request metadata to sendMagicLink callback (#8571)
  • Added dedicated secret option to OAuth proxy to reduce shared key exposure (#8699)
  • Added explicit organizationId parameter in team endpoints (#5062)
  • Added WeChat social provider (#5189)
  • Added twoFactorPage config option for custom 2FA page routing (#5329)

Bug Fixes

  • Deprecated oidc-provider plugin in favor of @better-auth/oauth-provider (#8985) – @better-auth
  • Fixed access control indexing type (#8155)
  • Added origin check middleware to password reset request (#8392)
  • Fixed account cookie comparison to use provider accountId instead of internal id (#8786)
  • Fixed session id generation when using secondary storage without database (#8927)
  • Fixed skipOriginCheck array handling (#8582)
  • Fixed misleading rate limit IP warning (#8617)
  • Passed user field through idToken sign-in body for Apple name support (#8417)
  • Preserved custom session fields on focus refresh (#8354)
  • Fixed double encoded cookie (#8133)
  • Prevented revoked sessions from being restored via database fallback (#8708)
  • Resolved duplicate operationId in admin plugin endpoints (#8570)
  • Rethrew phone sendOTP failures instead of silently swallowing them (#8842)
  • Set stateless cookieCache maxAge to match session.expiresIn (#8648)
  • Threw on duplicate email when autoSignIn: false without requireEmailVerification (#8521)
  • Fixed accountInfo endpoint to use accountId instead of internal id (#8346)
  • Restored deprecated createAdapter and type exports for backwards compatibility (#8461)
  • Fixed Response return for HTTP request contexts (#7521)
  • Fixed throw: true handling in client session refresh (#8610)
  • Preserved stale session data on network or server errors (#8437)
  • Fixed bundler re-export type resolution with direct imports (#8261)
  • Fixed Set-Cookie header splitting with lookahead heuristic (#8301)
  • Prioritized generateId: "uuid" over adapter customIdGenerator (#8679)
  • Fixed date string revival in safeJSONParse for pre-parsed objects (#8248)
  • Fixed postgres migration to use CREATE INDEX (#8538)
  • Triggered sessionSignal after requesting email change in email-otp (#8816)
  • Fixed generic-oauth to use discovery userinfo endpoint instead of hardcoded URLs (#8223)
  • Normalized missing resolver path in last-login-method plugin (#8589)
  • Returned additional fields in /magic-link/verify (#7223)
  • Fixed OAuth proxy to read callback params from body for form_post (#8895)
  • Fixed double-hashing of OAuth state when storeIdentifier is hashed (#8980)
  • Fixed redirect_uri validation for prompt=none in oidc-provider (#8398)
  • Opted into FedCM to suppress Google GSI deprecation warnings (#8720)
  • Filtered null organizations in listUserInvitations (#8694)
  • Fixed multi-role user handling in invite and member removal checks (#8442)
  • Enforced authorization on SCIM management endpoints and normalized passkey ownership checks (#8843)
  • Allowed passwordless users to manage 2FA (#7243)
  • Wired twoFactorTable option to schema modelName (#8443)
  • Prevented any from collapsing auth.$Infer and client inference types (#8981)
  • Fixed updateUser to not overwrite unrelated username fields (#7570)
  • Enforced username uniqueness in updateUser (#8731)
  • Used non-blocking scrypt for password hashing to avoid blocking the event loop (#8685)

For detailed changes, see CHANGELOG

@better-auth/sso

❗ Breaking Changes

  • Enabled InResponseTo validation by default for SP-initiated SAML flows (#8736)

Migration: Set sso({ saml: { enableInResponseToValidation: false } }) to restore the previous behavior.

Features

  • Added logging for OIDC callback code validation failures (#8693)

Bug Fixes

  • Patched transitive node-forge vulnerability via samlify pin (#8838)
  • Fixed bare domain handling in domain verification (#8369)
  • Preferred UserInfo endpoint over ID token and mapped sub claim correctly (#8276)
  • Fixed provisionUser inconsistency and added provisionUserOnEveryLogin option (#8818)
  • Skipped state cookie check for SAML ACS cross-site POST (#8735)
  • Fixed verification operations to use internalAdapter (#8353)
  • Fixed ESM compatibility with namespace import for samlify (#8697)

For detailed changes, see CHANGELOG

@better-auth/mongo-adapter

❗ Breaking Changes

  • Stored UUIDs as native BSON UUID type (#8681)

Migration: New documents use native BSON UUIDs. Existing string UUIDs continue to work. No data migration required.

For detailed changes, see CHANGELOG

@better-auth/oauth-provider

Features

  • Added pairwise subject identifiers (OIDC Core Section 8) (#8292)
  • Added public client prelogin endpoint (#8214)

Bug Fixes

  • Allowed localhost subdomains in isLocalhost function (#8286)
  • Fixed fetch redirect CORS after login (#8519)
  • Allowed customIdTokenClaims to override standard claims (#7865)
  • Enforced DB-backed sessions when secondary storage is enabled (#8894)
  • Fixed dist declaration type errors (#8701)
  • Fixed dynamic baseURL config handling in init (#8649)
  • Improved allowed paths for oauth_query in client plugin (#8320)
  • Allowed customIdTokenClaims to override acr and auth_time (#8633)
  • Normalized auth_time timestamps across adapter shapes (#8761)
  • Returned JSON redirects from post-login OAuth continuation to fix CORS-blocked 302s (#8815)
  • Fixed PAR scope loss, loopback redirect matching, and DCR skip_consent (#8632)
  • Added prompt=none support (#8554)

For detailed changes, see CHANGELOG

@better-auth/stripe

Features

  • Added customizable prorationBehavior per plan (#8525)

Bug Fixes

  • Improved organization customer search by adding customerType check (#8609)
  • Replaced {CHECKOUT_SESSION_ID} placeholder in success callbackURL (#8568)
  • Returned correct priceId for annual subscriptions in list (#8810)

For detailed changes, see CHANGELOG

@better-auth/drizzle-adapter

Features

  • Added case-insensitive query support (mode: "insensitive") (#8556)

Bug Fixes

  • Fixed Drizzle adapter failing date transformation (#8289)
  • Used IS NULL / IS NOT NULL for null value comparisons (#8660)

For detailed changes, see CHANGELOG

@better-auth/expo

Features

  • Exposed plugin version field on all built-in plugins (#8750)

Bug Fixes

  • Fixed shim require issue (#8253)
  • Fixed origin override handling across mutable and immutable requests (#8405)

For detailed changes, see CHANGELOG

@better-auth/prisma-adapter

Bug Fixes

  • Moved adapter packages to dependencies to fix missing module errors (#8401)
  • Used updateMany fallback for non-unique updates (#8524)
  • Used deleteMany when deleting by non-unique field (#8314)

For detailed changes, see CHANGELOG

auth

Features

  • Migrated MCP server URL to mcp.better-auth.com (#8747)

Bug Fixes

  • Fixed path alias resolution from extended tsconfig files (#8520)
  • Treated omitted required as true in Drizzle and Prisma generators (#8614)

For detailed changes, see CHANGELOG

@better-auth/electron

Bug Fixes

  • Fixed verification operations with secondary storage (#8247)
  • Handled safeStorage encryption failures gracefully (#8530)

For detailed changes, see CHANGELOG

@better-auth/passkey

Features

  • Added pre-auth registration and WebAuthn extensions support (#7154)

Bug Fixes

  • Fixed error message strings in passkey client (#8751)

For detailed changes, see CHANGELOG

@better-auth/test-utils

Features

  • Exported adapter test suites from @better-auth/test-utils/adapter (#8564) – @better-auth

Bug Fixes

  • Removed using keyword for runtime compatibility (#8756)

For detailed changes, see CHANGELOG

@better-auth/api-key

Bug Fixes

  • Fixed turbo caching, enforced lockfile integrity, and expanded pre-commit hooks (#8892)

For detailed changes, see CHANGELOG

@better-auth/core

Bug Fixes

  • Stopped marking redirect APIErrors as span errors in OpenTelemetry traces (#8850)

For detailed changes, see CHANGELOG

@better-auth/kysely-adapter

Bug Fixes

  • Removed deprecated numUpdatedOrDeletedRows from D1 dialect (#8798)

For detailed changes, see CHANGELOG

@better-auth/telemetry

Bug Fixes

  • Used conditional exports to replace dynamic import hacks (#8458)

For detailed changes, see CHANGELOG

Contributors

Thanks to everyone who contributed to this release:

@aarmful, @bytaesu, @dvanmali, @Eric-Song-Nop, @formatlos, @GautamBytes, @GoPro16, @gustavovalverde, @himself65, @jonathansamines, @jslno, @mrgrauel, @NathanColosimo, @okisdev, @olliethedev, @Oluwatobi-Mustapha, @OscarCornish, @ping-maxwell, @raihanbrillmark, @sicarius97, @Sigmabrogz, @wuzgood98, @xiaoyu2er, @YevheniiKotyrlo

Full changelog: v1.5.6...v1.6.0

Β Β Β πŸš€ Features

  • Agent auth plugin – @Bekacru
  • core: Add experimental opentelemetry instrumentation – @jonathansamines @bytaesu
  • email-otp: Add resendStrategy option to reuse existing OTP – @bytaesu
  • magic-link: Add request metadata to sendMagicLink – @mrgrauel
  • mongo-adapter: Store UUIDs as native BSON UUID – @bytaesu
  • oauth-provider: Public client prelogin endpoint – @dvanmali
  • organization: Explicit organizationId in team endpoints – @xiaoyu2er @himself65
  • social-provider: Add wechat social provider – @Eric-Song-Nop @himself65
  • stripe: Allow customizable prorationBehavior per plan – @bytaesu
  • test-utils: Export adapter test suites from @better-auth/test-utils/adapter – @bytaesu
  • two-factor: Add twoFactorPage in config – @wuzgood98

   🐞 Bug Fixes

  • Handle skipOriginCheck array – @jslno
  • Prevent revoked sessions from being restored via database fallback – @bytaesu
  • api:
    • Return Response for HTTP request contexts – @gustavovalverde
  • client:
    • Handle throw:true in session refresh – @bytaesu
  • core:
    • Prioritize generateId "uuid" over adapter customIdGenerator – @bytaesu
  • docs:
    • Improve AI chat security and cleanup – @himself65
    • Add missing Encore icon to sidebar icons – @himself65
  • electron:
    • Handle safeStorage encryption failures gracefully – @jslno
  • oauth-provider:
    • Support prompt=none – @dvanmali
    • Improve allowed paths for oauth_query for client plugin – @dvanmali
    • Fix dist declaration type errors – @gustavovalverde
  • organization:
    • Filter null organizations in listUserInvitations – @raihanbrillmark
  • sso:
    • Use namespace import for samlify to fix ESM compatibility – @himself65
  • stripe:
    • Replace {CHECKOUT_SESSION_ID} placeholder in success callbackURL – @bytaesu
    • Improve organization customer search by adding customerType check – @bytaesu
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • cli: Warn when old @better-auth/cli is used with better-auth v1.5.x+ – @himself65
Β Β Β Β View changes on GitHub

Β Β Β πŸš€ Features

  • oauth-provider: Pairwise subject identifiers (OIDC Core Β§8) – @gustavovalverde @himself65

   🐞 Bug Fixes

  • Pass user field through idToken sign-in body for Apple name support – @bytaesu
  • Add missing SubpageItem properties for docs-sidebar compatibility – @bytaesu
  • Add icon prop to SubpageLink component – @bytaesu
  • Correct sign-in link to dash.better-auth.com – @bytaesu
  • Restore features.tsx and align import with canary – @bytaesu
  • Add suppressHydrationWarning to video elements – @bytaesu
  • Preserve custom session fields on focus refresh – @jslno
  • Throw on duplicate email when autoSignIn: false without requireEmailVerification – @himself65
  • Add origin check middleware to password reset request – @jslno
  • adapters: Restore deprecated createAdapter and type exports for backcompat – @himself65
  • blog: Fix RSS feed link path, image path and blog date – @0-Sandy
  • cli: Resolve path aliases from extended tsconfig files – @himself65
  • client: Preserve stale session data on network or server errors – @bytaesu
  • db: Use CREATE INDEX for postgres migration – @himself65
  • oauth-provider: Avoid fetch redirect CORS after login – @GautamBytes
  • oidc-provider: Validate redirect_uri for prompt=none – @jslno
  • organization: Handle multi-role users in invite and member removal checks – @himself65
  • prisma-adapter: Fall back to updateMany for non-unique updates – @himself65
  • sso: Handle bare domains in domain verification – @himself65
  • telemetry: Use conditional exports to replace dynamic import hacks – @himself65
  • two-factor: Wire twoFactorTable option to schema modelName – @himself65
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • Move adapter packages to dependencies to fix missing module errors – @himself65
  • expo: Handle origin override across mutable and immutable requests – @NathanColosimo @bytaesu
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • account: Use accountId instead of id in accountInfo endpoint – @NathanColosimo @himself65
  • sso: Use internalAdapter for verification operations – @himself65
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • Access control indexing type – @YevheniiKotyrlo @himself65
  • Prevent double encoded cookie – @Oluwatobi-Mustapha @himself65
  • cookies:
    • Use lookahead heuristic for splitting Set-Cookie headers – @bytaesu
  • oauth-provider:
    • Allow localhost subdomains in isLocalhost function – @sicarius97 @himself65
    • CustomIdTokenClaims should override standard claims – @gustavovalverde
  • prisma-adapter:
    • Use deleteMany when deleting by non-unique field – @himself65
  • sso:
    • Prefer UserInfo endpoint over ID token and map sub claim correctly – @himself65
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • client: Use direct imports to fix bundler re-export type resolution – @himself65
  • core: Revive date strings in safeJSONParse for pre-parsed objects – @himself65
  • db: Support verification operations with secondary storage – @himself65
  • expo: Avoid shim require – @himself65
  • generic-oauth: Use discovery userinfo endpoint instead of hardcoded URLs – @himself65
Β Β Β Β View changes on GitHub

Better Auth 1.5 Release

We’re excited to announce the release of Better Auth 1.5! πŸŽ‰

This is our biggest release yet, with over 600 commits, 70 new features, 200 bug fixes, and 7 entirely new packages. From MCP authentication to Electron desktop support, this release brings Better Auth to new platforms and use cases.

We’re also announcing our new Infrastructure product. It lets you use a full user management and analytics dashboard, security and protection tooling, audit logs, a self-service SSO UI, and more, all with your own Better Auth instance.

Starting with this release, the self-service SSO dashboard β€” which lets your enterprise customers onboard their own SAML providers without support tickets β€” is powered by Better Auth Infrastructure. If you’re using the SSO plugin in production, we recommend upgrading to the Pro or Business tier to get access to the dashboard and streamline your enterprise onboarding.

And soon, you’ll be able to host your Better Auth instance on our infrastructure as well, so you can own your auth at scale without worrying about infrastructure needs.

Sign up now: https://better-auth.com/sign-in πŸš€

To upgrade, run:

npx auth upgrade

πŸš€ Highlights

New Better Auth CLI

We’re introducing a new standalone CLI: npx auth. This replaces the previous @better-auth/cli package, which will be deprecated in a future release.

npx auth init

With a single interactive command, npx auth init scaffolds a complete Better Auth setup β€” configuration file, database adapter, and framework integration.

All existing commands like migrate and generate are available through the new CLI as well:

npx auth migrate   # Run database migrations
npx auth generate  # Generate auth schema
npx auth upgrade   # Upgrade Better Auth to the latest version

The generate command now also supports a --adapter flag, letting you generate schema output tailored to your specific database adapter without needing a full Better Auth config file:

npx auth generate --adapter prisma
npx auth generate --adapter drizzle

Remote MCP Auth Client

The MCP plugin now ships a framework-agnostic remote auth client. If your MCP server is separate from your Better Auth instance, you can verify tokens and protect resources without duplicating auth logic.

πŸ‘‰ Read more about MCP authentication

import { createMcpAuthClient } from "better-auth/plugins/mcp/client";

const mcpAuth = createMcpAuthClient({
    authURL: "<https://my-app.com/api/auth>",
});

// Use as a handler wrapper
const handler = mcpAuth.handler(async (req, session) => {
    // session contains userId, scopes, accessToken, clientId, etc.
    return new Response("OK");
});

// Or verify tokens directly
const session = await mcpAuth.verifyToken(token);

It also comes with built-in framework adapters for Hono and Express-like servers:

import { mcpAuthHono } from "better-auth/plugins/mcp/client/adapters";

const middleware = mcpAuthHono(mcpAuth);

OAuth 2.1 Provider

The new @better-auth/oauth-provider plugin turns your Better Auth instance into a full OAuth 2.1 authorization server with OIDC compatibility. Issue access tokens, manage client registrations, and let third-party apps authenticate against your API β€” including MCP agents.

πŸ‘‰ Read more about the OAuth Provider

import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";
import { oauthProvider } from "@better-auth/oauth-provider";

export const auth = betterAuth({
    plugins: [
        jwt(),
        oauthProvider({
            loginPage: "/sign-in",
            consentPage: "/consent",
        }),
    ],
});

Key features:

  • OAuth 2.1 with OIDC: Supports authorization_code, refresh_token, and client_credentials grants with openid scope support.
  • MCP-ready: Works out of the box as an authorization server for MCP tools and agents.
  • Dynamic Client Registration: Allow clients to register dynamically, with support for both public and confidential clients.
  • JWT & JWKS verification: Sign access tokens as JWTs and verify them remotely via the /jwks endpoint.
  • Consent & authorization flows: Built-in consent, account selection, and post-login redirect screens.
  • Token introspection & revocation: RFC 7662 and RFC 7009 compliant endpoints.
  • Per-endpoint rate limiting: Configurable rate limits for each OAuth endpoint.

Note:

The OAuth 2.1 Provider replaces the previous OIDC Provider plugin, which will be deprecated in a future release. The MCP plugin will also transition to use the OAuth 2.1 Provider as its foundation. See the migration guide for upgrading from the OIDC Provider plugin.

Electron Integration

Full desktop authentication support for Electron apps. The plugin handles the complete OAuth flow β€” opening the system browser, exchanging authorization codes via custom protocol, and managing cookies securely.

πŸ‘‰ Read more about Electron integration

import { betterAuth } from "better-auth";
import { electron } from "@better-auth/electron";

export const auth = betterAuth({
    plugins: [electron()],
});
import { createAuthClient } from "better-auth/client";
import { electronClient } from "@better-auth/electron/client";

const client = createAuthClient({
    plugins: [
        electronClient({
            protocol: "com.example.myapp",
        }),
    ],
});

// Opens system browser, handles callback, returns session
await client.requestAuth();

Internationalization (i18n)

The new i18n plugin provides type-safe error message translations with automatic locale detection from headers, cookies, or sessions.

πŸ‘‰ Read more about i18n

import { betterAuth } from "better-auth";
import { i18n } from "@better-auth/i18n";

export const auth = betterAuth({
    plugins: [
        i18n({
            defaultLocale: "en",
            detection: ["header", "cookie"],
            translations: {
                en: { USER_NOT_FOUND: "User not found" },
                fr: { USER_NOT_FOUND: "Utilisateur non trouvΓ©" },
                es: { USER_NOT_FOUND: "Usuario no encontrado" },
            },
        }),
    ],
});

Error codes are fully typed β€” your IDE will autocomplete all available error codes from every registered plugin.

Typed Error Codes

Every error response now includes a machine-readable code field. All first-party plugins define their own typed error codes using defineErrorCodes, and the APIError class supports them natively.

import { defineErrorCodes } from "@better-auth/core";

export const MY_ERROR_CODES = defineErrorCodes({
    USER_NOT_FOUND: "User not found",
    INVALID_TOKEN: "The provided token is invalid",
});

// In route handlers:
throw APIError.from("BAD_REQUEST", MY_ERROR_CODES.USER_NOT_FOUND);

Error responses now look like:

{
    "code": "USER_NOT_FOUND",
    "message": "User not found"
}

This is the foundation that the i18n plugin builds on β€” every error code from every plugin is discoverable at compile time, so translation dictionaries are fully type-checked.

SSO β€” Production Ready

The SSO plugin has received extensive hardening to be production-ready, with 23+ commits improving security and compliance.

Self-Service SSO Dashboard

As part of our new Infrastructure product, the SSO plugin is now accompanied by a self-service dashboard for onboarding enterprise customers. Organization admins can generate a shareable link that walks enterprise customers through configuring their SAML identity provider β€” no back-and-forth support tickets required.

The dashboard is available at:

https://better-auth.com/dashboard/[project]/organization/[orgId]/enterprise

From there, you can generate onboarding links, monitor SSO connection status, and manage provider configurations for each organization.

SAML Single Logout (SLO)

Full support for both SP-initiated and IdP-initiated SAML Single Logout:

import { betterAuth } from "better-auth";
import { sso } from "@better-auth/sso";

export const auth = betterAuth({
    plugins: [
        sso({
            saml: {
                enableSingleLogout: true, // [!code highlight]
                wantLogoutRequestSigned: true,
                wantLogoutResponseSigned: true,
            },
        }),
    ],
});

Additional SSO Improvements

  • Signed SAML AuthnRequests: Configurable signature and digest algorithms.
  • Multi-domain providers: Bind SSO providers to multiple domains.
  • InResponseTo validation: Prevent replay attacks on SAML assertions.
  • Algorithm restrictions: Block deprecated signature/digest algorithms.
  • Clock skew tolerance: Configurable tolerance for SAML timestamp validation.
  • OIDC ID token aud claim validation: Verify audience in OpenID Connect flows.
  • Provider CRUD endpoints: List, get, update, and delete SSO providers via API.
  • Shared OIDC redirect URI: Single redirect URI for all OIDC providers.

Unified Before & After Hooks

Plugin hooks and global hooks now share the same AuthMiddleware type, making the hooks system consistent and composable across the entire auth pipeline.

import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";

export const auth = betterAuth({
    hooks: {
        before: createAuthMiddleware(async (ctx) => {
            // Runs before every endpoint
            console.log("Request to:", ctx.path);
        }),
        after: createAuthMiddleware(async (ctx) => {
            // Runs after every endpoint, with access to the response
            console.log("Response:", ctx.context.returned);
        }),
    },
});

Plugins use the same middleware type with matchers for targeted interception:

hooks: {
    before: [{
        matcher: (ctx) => ctx.path === "/sign-in/email",
        handler: createAuthMiddleware(async (ctx) => { /* ... */ }),
    }],
},

Dynamic Base URL

Better Auth can now resolve the base URL dynamically from incoming requests, making it work seamlessly with Vercel preview deployments, multi-domain setups, and reverse proxies.

πŸ‘‰ Read more about dynamic base URL

import { betterAuth } from "better-auth";

export const auth = betterAuth({
    baseURL: {
        allowedHosts: [
            "myapp.com",
            "*.vercel.app",       // Any Vercel preview
            "preview-*.myapp.com", // Pattern match
        ],
        fallback: "<https://myapp.com>",
        protocol: "auto",
    },
});

Verification on Secondary Storage

Verification tokens can now be stored in secondary storage (e.g., Redis) instead of β€” or in addition to β€” the database. Identifiers can be hashed for extra security.

import { betterAuth } from "better-auth";

export const auth = betterAuth({
    secondaryStorage: {
        // ... your Redis config
    },
    verification: {
        storeIdentifier: "hashed",     // Hash verification identifiers // [!code highlight]
        storeInDatabase: false,        // Only use secondary storage // [!code highlight]
    },
});

You can also configure per-identifier overrides:

verification: {
    storeIdentifier: {
        default: "plain",
        overrides: {
            "email-verification": "hashed",
            "password-reset": "hashed",
        },
    },
},

Rate Limiter Improvements

The rate limiter has been improved with separate request/response handling, hardened defaults, and IPv6 support.

  • Separate request and response phases: Rejected requests are no longer counted against the rate limit.
  • Hardened default rules: Sign-in/sign-up limited to 3 requests per 10 seconds
  • IPv6 subnet support: Rate limiting by IPv6 prefix with configurable subnet size.
  • Plugin-level rate limit rules: Plugins can define their own rate limit rules.
  • Expired entry cleanup: Automatic cleanup for the memory storage backend.
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    advanced: {
        ipAddress: {
            ipv6Subnet: 64, // Rate limit by /64 subnet
        },
    },
});

Non-Destructive Secret Key Rotation

Better Auth now supports rotating BETTER_AUTH_SECRET without invalidating existing sessions, tokens, or encrypted data. When you need to rotate your secret β€” whether for scheduled rotation or incident response β€” you can introduce a new key while keeping old keys available for decryption.

import { betterAuth } from "better-auth";

export const auth = betterAuth({
    secrets: [
        { version: 2, value: "new-secret-key-at-least-32-chars" },   // current (first = active)
        { version: 1, value: "old-secret-key-still-used-to-decrypt" }, // previous
    ],
});

Or via environment variable:

BETTER_AUTH_SECRETS="2:new-secret-key,1:old-secret-key"

New data is always encrypted with the latest key (first in the array), while decryption automatically tries all configured keys. This lets you roll secrets gradually without downtime or data loss.

Seat-Based Billing (Stripe)

The Stripe plugin now supports per-seat billing for organizations. Member changes automatically sync seat quantity with Stripe.

import { betterAuth } from "better-auth";
import { stripe } from "@better-auth/stripe";
import { organization } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [
        organization(),
        stripe({
            stripeClient,
            stripeWebhookSecret: "whsec_...",
            subscription: {
                enabled: true,
                plans: [
                    {
                        name: "team",
                        priceId: "price_base_monthly",
                        seatPriceId: "price_per_seat", // [!code highlight]
                    },
                ],
            },
            organization: { enabled: true }, // [!code highlight]
        }),
    ],
});

The plugin also adds support for usage-based billing via lineItems, subscription schedules with scheduleAtPeriodEnd, and billingInterval tracking.

Test Utilities Plugin

A new testUtils plugin provides factories, database helpers, and auth utilities for integration and E2E testing.

πŸ‘‰ Read more about test utilities

import { betterAuth } from "better-auth";
import { testUtils } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [testUtils({ captureOTP: true })],
});
const ctx = await auth.$context;
const test = ctx.test;

// Create and save a test user
const user = test.createUser({ email: "test@example.com" });
const savedUser = await test.saveUser(user);

// Login and get auth headers
const { headers, session, token } = await test.login({ userId: user.id });

// Capture OTPs for verification tests
const otp = test.getOTP("test@example.com");

Update Session Endpoint

A new /update-session endpoint allows updating custom additional session fields on the fly.

// Client-side
await authClient.updateSession({
    theme: "dark",
    language: "en",
});

This is useful when you have additional session fields that need to change without re-authentication.

Adapter Extraction

Database adapters have been extracted into their own packages. This is a major architectural change that reduces bundle size and allows adapters to be versioned independently.

PackageDescription
@better-auth/drizzle-adapterDrizzle ORM adapter
@better-auth/prisma-adapterPrisma adapter
@better-auth/kysely-adapterKysely adapter
@better-auth/mongo-adapterMongoDB adapter
@better-auth/memory-adapterIn-memory adapter

The main better-auth package re-exports all adapters, so existing imports continue to work. But you can now install only the adapter you need for smaller bundles:

import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { betterAuth } from "better-auth/minimal";

export const auth = betterAuth({
    database: drizzleAdapter(db, { provider: "pg" }),
});

Cloudflare D1 Support

Better Auth now natively supports Cloudflare D1 as a first-class database option. Pass your D1 binding directly β€” no custom adapter setup required.

import { betterAuth } from "better-auth";

export default {
    async fetch(request, env) {
        const auth = betterAuth({
            database: env.DB, // D1 binding, auto-detected // [!code highlight]
        });
        return auth.handler(request);
    },
} satisfies ExportedHandler<{ DB: D1Database }>;

The built-in D1 dialect handles query execution, batch operations, and introspection through D1's native API. Note that D1 does not support interactive transactions β€” Better Auth uses D1's batch() API for atomicity instead.

✨ More Features

Authentication & Sessions

  • verifyPassword API: New server-side endpoint to verify the current user's password.
  • setShouldSkipSessionRefresh: Programmatically skip session refresh for specific requests.
  • deferSessionRefresh: Support for read-replica database setups.
  • Awaitable social provider config: Provider configuration can now be async.
  • Limit enumeration on sign-up: When email verification is required, sign-up no longer reveals existing accounts.
  • customSyntheticUser option: Support plugin fields in enumeration-protected responses (#8097).
  • Form data support for email sign-in/sign-up: In addition to JSON bodies.
  • Automatic base URL detection from VERCEL_URL and NEXTAUTH_URL: The client now falls back to VERCEL_URL and NEXTAUTH_URL environment variables when no explicit baseURL is configured, making server-side rendering on Vercel work out of the box.

OAuth & Providers

  • Railway OAuth provider: New social provider.
  • Trusted providers callback: Dynamic trusted provider resolution.
  • Case-insensitive email matching: For social account linking.
  • Legacy OAuth clients without PKCE: Backward compatibility support.

Stripe Plugin

  • seatPriceId and lineItems enable flexible subscription checkouts, supporting modern pricing models like per-seat and usage-based billing.
  • scheduleAtPeriodEnd: Defer plan changes to end of billing period.
  • Subscription schedule tracking: Monitor upcoming subscription changes.
  • Organization customer support: Organization as a Stripe customer.
  • Flexible cancellation and termination: More control over subscription lifecycle.

SCIM

  • SCIM ownership model: Link SCIM provider connections to users.
  • SCIM connection management: List, get, and delete SCIM provider connections.
  • Microsoft Entra ID compatibility: Full support for Entra provisioning.

OAuth Provider Plugin

  • HTTPS enforcement for redirect URIs: HTTP only allowed for localhost.
  • RFC 9207 iss parameter: Authorization response issuer identifier.
  • Per-client PKCE configuration: Opt-out of PKCE for admin-created clients.
  • Scope narrowing at consent: Users can reduce requested scopes.
  • prompt=none support: Silent authentication for OIDC.
  • Configurable rate limiting: Per-endpoint rate limit configuration.

Plugin Improvements

  • magic-link: allowedAttempts option: Limit verification attempts.
  • email-otp: Change email flow with OTP: Users can change their email address via OTP verification, with optional current-email confirmation for added security.
  • email-otp: Name, image, and additional fields in sign-in: Richer OTP sign-in.
  • phone-number: Additional fields in signUpOnVerification: Pass extra data.
  • two-factor: twoFactorCookieMaxAge and server-side trust device expiration.
  • one-tap: Button mode for Google sign-in.
  • anonymous: Delete anonymous user endpoint.
  • admin: Optional password on user creation.
  • api-keys: Pagination for list endpoint, organization reference via metadata.
  • organization: Function support for membershipLimit, reject expired invites.

Core & Infrastructure

  • BetterAuthPluginRegistry type system: Typed plugin discovery via getPlugin() and hasPlugin().
  • Version in AuthContext: Access the Better Auth version at runtime.
  • Redis secondary storage: Extracted to @better-auth/redis-storage. – @better-auth
  • Session ID handling for secondary storage: Proper ID generation when database is not used.

πŸ”’ Security Improvements

  • Prevent OTP reuse via race condition: Atomically invalidate OTPs on use.
  • Prevent user enumeration: In email-otp when sign-up is disabled, and on sign-up with required email verification.
  • Prevent email enumeration on /change-email: Always returns { status: true } and simulates token generation for timing safety (#8097).
  • Stricter default rate limits: For password reset and phone number verification endpoints.
  • Separate CSRF and origin checks: More granular request validation.
  • Prevent trial abuse: Check all user subscriptions before granting a free trial.
  • SAML ACS error redirect hardening: Prevent open redirect in error flows.
  • IPv6 address normalization and subnet support: For rate limiting and IP-based rules.
  • XML parser hardening: Configurable size limits for SAML responses and metadata.

⚠️ Breaking Changes

We recommend going through each breaking change to ensure a smooth upgrade.

Deprecated API Removal

The /forget-password/email-otp endpoint has been removed. Use the standard password reset flow instead.

Adapter Imports

The better-auth/adapters/test export has been removed. Use the testUtils plugin instead.

API Key Plugin Moved to @better-auth/api-key

The api-key plugin has been extracted into its own package. Install it separately:

npm install @better-auth/api-key
- import { apiKey } from "better-auth/plugins"+ import { apiKey } from "@better-auth/api-key";

Schema changes:

  • The userId field on the ApiKey table has been renamed to referenceId.
  • A new configId field has been added (defaults to "default").

Plugin options changes:

The permissions.defaultPermissions callback's first argument is now referenceId instead of userId:

export const auth = betterAuth({
    plugins: [
        apiKey({
            permissions: {
-               defaultPermissions: async (userId, ctx) => {
+               defaultPermissions: async (referenceId, ctx) => {
                    return {
                        files: ["read"],
                        users: ["read"],
                    };
                },
            }
        })
    ]
})

Client SDK changes:

- const ownerId = apiKey.userId+ const ownerId = apiKey.referenceId;
+ const ownerType = apiKey.references; // "user" or "organization"
+ const configId = apiKey.configId;

πŸ›  Developer Changes

If you are building a plugin on top of Better Auth, there are a few things you should know.

@deprecated APIs Are Removed

All previously deprecated APIs have been removed. This includes deprecated adapter types, client types, helper types, and plugin options. If you were relying on any @deprecated methods or options, you'll need to migrate to their replacements:

RemovedReplacement
createAdaptercreateAdapterFactory
AdapterDBAdapter
TransactionAdapterDBTransactionAdapter
Store (client)ClientStore
AtomListener (client)ClientAtomListener
ClientOptionsBetterAuthClientOptions
LiteralUnion, DeepPartial (from better-auth/types/helper)Import from @better-auth/core
onEmailVerificationafterEmailVerification
sendChangeEmailVerificationsendChangeEmailConfirmation
advanced.database.useNumberIdadvanced.database.generateId: "serial"
Organization permission fieldpermissions (plural)

@better-auth/core/utils Barrel Export Removed

The @better-auth/core/utils barrel export has been split into individual subpath exports to improve tree-shaking:

- import { generateId, safeJSONParse, defineErrorCodes } from "@better-auth/core/utils" – [![@better-auth](https://github.com/better-auth.png)](https://github.com/better-auth)+ import { generateId } from "@better-auth/core/utils/id";
+ import { safeJSONParse } from "@better-auth/core/utils/json";
+ import { defineErrorCodes } from "@better-auth/core/utils/error-codes";

$ERROR_CODES Type Changed to RawError Objects

The $ERROR_CODES field on plugins now expects Record<string, RawError> instead of Record<string, string>. Use defineErrorCodes() which now returns RawError objects with { code, message } instead of plain strings:

- $ERROR_CODES: {
-     MY_ERROR: "My error message",
- },
+ $ERROR_CODES: defineErrorCodes({
+     MY_ERROR: "My error message",
+ }),
+ // Returns: { MY_ERROR: { code: "MY_ERROR", message: "My error message" } }

Use the new APIError.from() static method to throw errors with error codes:

import { APIError } from "@better-auth/core/error";

throw APIError.from("BAD_REQUEST", MY_ERROR_CODES.MY_ERROR);

PluginContext Is Now Generic

PluginContext is now parameterized with Options:

type PluginContext<Options extends BetterAuthOptions> = {
    getPlugin: <ID extends string>(pluginId: ID) => /* inferred from registry */ | null;
    hasPlugin: <ID extends string>(pluginId: ID) => boolean; // narrows to `true` when plugin is registered
};

Plugins can register themselves via module augmentation for type-safe getPlugin() and hasPlugin():

declare module "@better-auth/core" {
    interface BetterAuthPluginRegistry<AuthOptions, Options> {
        "my-plugin": { creator: typeof myPlugin };
    }
}

InferUser / InferSession Types Removed

The InferUser<O> and InferSession<O> types have been removed. Use the generic User and Session types instead:

- import type { InferUser, InferSession } from "better-auth/types"- type MyUser = InferUser<typeof auth>+ import type { User, Session } from "better-auth";
+ type MyUser = User<typeof auth.$options["user"], typeof auth.$options["plugins"]>;

After Hooks Now Run Post-Transaction

Database "after" hooks (create.after, update.after, delete.after) now execute after the transaction commits, not during it. This prevents issues where hooks interacting with external systems (sending emails, calling APIs) could fail and roll back the entire transaction.

If your plugin relies on after hooks running inside the transaction for additional atomic database writes, you'll need to use the adapter directly within the main operation instead.

getMigrations Moved to better-auth/db/migration Subpath

We found that the getMigrations function includes many third-party dependencies, which caused some bundlers to unexpectedly include extra dependencies and increase output size. It's now available from a dedicated subpath:

- import { getMigrations } from "better-auth"+ import { getMigrations } from "better-auth/db/migration";

id Field Removed from Session in Secondary Storage

The id field is used to determine relationships between structures in database models. We've removed it from secondary storage since it's not necessary there, simplifying the storage logic. If your plugin reads sessions from secondary storage and relies on the id field, you'll need to update your code accordingly.

Plugin init() Context Is Now Mutable

The context object passed to a plugin's init() callback is now the same reference used throughout the auth lifecycle. init() can also return arbitrary keys via Record<string, unknown>, enabling plugins to inject custom context values that other plugins can access.

πŸ› Bug Fixes & Improvements

This release includes over 220 bug fixes addressing issues across all areas:

  • Drizzle Adapter: Fixed date transformation crashes and input handling.
  • Cookie Handling: Centralized parsing, fixed Expo leading semicolons, secure detection fallbacks.
  • Database Hooks: Delayed execution until after transaction commits to prevent inconsistencies.
  • Transaction Deadlock Prevention: Improved locking strategies across adapters.
  • Prisma Adapter: Fixed null condition handling and unique where field detection.
  • OAuth: Fixed refresh_token_expires_in handling, callback routing, and token encryption.
  • Organization: Fixed role deletion prevention, active member refetch, and dynamic access control inference.
  • Expo: Fixed immutable headers on Cloudflare Workers, cookie injection wildcards, and skipped cookie/expo-origin headers for ID token requests.
  • Kysely Adapter: Fixed edge case with aliased joined table names.
  • Last Login Method: Fixed handling of multiple Set-Cookie headers.
  • Session Listing: Fixed endpoints returning empty arrays when more than 100 inactive sessions exist.
  • Organization: Fixed path matching for active member signals.
  • OAuth: Fixed preserving refresh tokens when provider omits them on refresh.
  • Secondary Storage: Synced updateSession changes and removed duplicate writes.
  • And many more!

A lot of refinements to make everything smoother, faster, and more reliable. πŸ‘‰ Check the full changelog

❀️ Contributors

Thanks to all the contributors for making this release possible!

   🐞 Bug Fixes

  • Update workspace dependency ranges to workspace:* – @himself65
  • ci:
    • Use version-specific npm tag for version branches – @himself65
    • Prefix version branch npm tag with "v" to avoid SemVer conflict – @himself65
    • Use "release-X.Y" npm tag format for version branches – @himself65
  • expo:
    • Support Expo SDK 55 new versioning scheme – @himself65
Β Β Β Β View changes on GitHub

   🐞 Bug Fixes

  • cookie: Add deprecated options alias for backward compatibility – @himself65
Β Β Β Β View changes on GitHub

Β Β Β πŸš€ Features

  • adapter:
    • Improve select support – @jslno
  • oauth-provider:
    • Add iss parameter to authorization responses (RFC 9207) – @Paola3stefania
    • Add configurable rate limiting for OAuth endpoints – @Paola3stefania
    • Enforce HTTPS for redirect URIs – @Paola3stefania
  • phone-number:
    • Support user additionalFields in signUpOnVerification flow – @bytaesu @himself65

   🐞 Bug Fixes

  • Skip sending email verification to already verified users without a session – @bytaesu
  • Improve Headers detection with instanceof check and cross-realm fallback – @bytaesu
  • Safely coerce date values from DB in OAuth provider plugin – @himself65
  • Correct error redirect URL construction – @bytaesu
  • Encode callbackURL in delete-user verification email – @Paola3stefania
  • Add error handling for id token verification in Apple and Google providers – @Paola3stefania
  • access:
    • Allow passing statements directly into newRole – @jslno
  • adapter:
    • Use getCurrentAdapter for user lookup to avoid transaction deadlock – @sakamoto-wk
  • admin:
    • Change list type from never[] to UserWithRole[] – @LovelessCodes
    • Apply listUsers filter when filterValue is defined – @coderrshyam @bytaesu
    • Optional chain user in hooks – @jslno
  • api-key:
    • Error details not passed to response – @ping-maxwell @himself65
  • cli:
    • Add .env.local to dotenv – @himself65
  • cookie:
    • Relax cookie retrieval for getSessionCookie – @jslno
  • custom-session:
    • Use getSetCookie() to preserve individual Set-Cookie headers – @thomaspeklak
  • db:
    • Infer default value for required attr properly – @jslno
  • email-otp:
    • Typo in OpenAPI response metadata – @smsunarto
  • expo:
    • Construct the new Request to avoid immutable headers error on Cloudflare Workers – @bytaesu
    • Avoid a leading β€œ
    • Support wildcard trusted origins in deep link cookie injection – @bytaesu
  • generic-oauth:
    • Emit duplicate id warning – @himself65
  • microsoft:
    • Add verifyIdToken support for Microsoft Entra ID provider – @bytaesu
  • oauth:
    • Support case-insensitive email matching for social account linking – @karuppusamy-d
  • oauth-provider:
    • Return url instead of uri in continue and consent endpoints – @bytaesu
    • Add missing oauthClient createdAt/updatedAt values – @dvanmali
    • Return "invalid_client" on encrypted secret verification failure – @bytaesu
  • organization:
    • Prevent deletion of roles assigned to members – @bytaesu
    • Remove unreachable null check in acceptInvitation – @Saurav3004 @himself65
  • passkey:
    • Compute expirationTime per-request instead of at init – @bytaesu
    • Use deleteVerificationByIdentifier for secondary-storage cleanup – @bytaesu
  • sso:
    • Add better-call peerDeps – @bytaesu
    • Allow custom organization roles in provisioning types – @MuzzaiyyanHussain
    • Resolve TXT record at verification subdomain instead of root domain – @Paola3stefania
    • Correct IdentityProvider configuration in signInSSO – @theNailz
    • Fix broken relay state redirect on SAML ACS route – @rbayliss
    • Validate aud claim in OpenID Connect ID tokens – @Paola3stefania
    • Harden SAML ACS error redirects and add regression test for #7777 – @Paola3stefania
  • stripe:
    • Restore better-call peerDeps – @bytaesu
    • Use correct stripeCustomerId on /subscription/cancel/callback endpoint – @bytaesu
    • Clarify error when authorizeReference is missing – @bytaesu
Β Β Β Β View changes on GitHub