[GH-ISSUE #6400] Email/password sign-in fails when experimental.joins: true is enabled #19136

Closed
opened 2026-04-15 17:56:06 -05:00 by GiteaMirror · 14 comments
Owner

Originally created by @codelonesomest on GitHub (Nov 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6400

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure Better-Auth with email/password sign-in enabled and set experimental.joins: true.
  2. Attempt to sign in using valid credentials (existing user account).
  3. The server log prints Hash password: ****** and the API responds with 401 INVALID_EMAIL_OR_PASSWORD even though the password is correct.

Current vs. Expected behavior

  • Current: When joins are enabled, the flow calls emailAndPassword.password.hash during sign-in (log clearly shows Hash password: ******) and returns 401 INVALID_EMAIL_OR_PASSWORD for every login, so valid credentials never succeed.
  • Expected: The sign-in flow should call emailAndPassword.password.verify, so the provided password is compared to the stored hash and valid credentials authenticate successfully when experimental.joins is true.

What version of Better Auth are you using?

1.4.3

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:45 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8103",
    "release": "25.0.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.85 GB"
  },
  "node": {
    "version": "v22.21.1",
    "env": "production"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.3"
  },
  "frameworks": [
    {
      "name": "hono",
      "version": "4.10.7"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "1.4.3",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { db } from '@root/db'
import { kyselyAdapter } from '@root/db/helpers/adapter/kysely'
import { betterAuth } from 'better-auth'
import { haveIBeenPwned, jwt, openAPI } from 'better-auth/plugins'
import convert from 'convert'
import dayjs from 'dayjs'
import { isEmpty } from 'es-toolkit/compat'
import { ulid } from 'ulid'
import env from './env'
import { redis } from './redis'
....

const auth = betterAuth({
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ['google', 'facebook']
    }
  },
  advanced: {
    cookiePrefix: env.get('APP_NAME'),
    crossSubDomainCookies: {
      domain: env.get('AUTH_COOKIE_DOMAIN'),
      enabled: true
    },
    database: { generateId: () => ulid() },
    defaultCookieAttributes: { httpOnly: true, partitioned: true, sameSite: 'lax', secure: true },
    ipAddress: { disableIpTracking: false, ipAddressHeaders: ['X-IP', ...headersToCheck] }
  },
  appName: env.get('APP_NAME'),
  basePath: '/auth/v1',
  baseURL: env.get('BASE_URL'),
  database: kyselyAdapter(db, { type: 'postgres', usePlural: true }),
  disabledPaths: ['/sign-in/email', '/sign-up/email', '/get-session', '/sign-out', '/forget-password', '/reset-password', '/verify-email', '/send-verification-email', '/change-email', '/change-password', '/update-user', '/delete-user', '/reset-password/', '/request-password-reset', '/list-sessions', '/revoke-session', '/revoke-sessions', '/revoke-other-sessions', '/list-accounts', '/delete-user/callback', '/refresh-token', '/get-access-token', '/account-info'],
  emailAndPassword: {
    enabled: true,
    password: {
      hash(password) {
        return Bun.password.hash(password, { algorithm: 'argon2id', memoryCost: 19456, timeCost: 2 })
      },
      verify(data) {
        return Bun.password.verify(data.password, data.hash, 'argon2id')
      }
    },
    requireEmailVerification: true,
    resetPasswordTokenExpiresIn: convert(...env.get('AUTH_RESET_PASSWORD_TOKEN_EXPIRES_IN')).to('seconds'),
    revokeSessionsOnPasswordReset: true,
    async sendResetPassword(data) {
      // ...
    }
  },
  emailVerification: {
    autoSignInAfterVerification: true,
    expiresIn: convert(...env.get('AUTH_EMAIL_VERIFICATION_TOKEN_EXPIRES_IN')).to('seconds'),
    sendOnSignIn: false,
    sendOnSignUp: true,
    async sendVerificationEmail(data) {
      // ...
    }
  },
  logger: {
    level: 'warn',
    log(level, message, ...args) {
      logger.getChild('better-auth')[level](message, { ...args })
    }
  },
  onAPIError: { errorURL: `${env.get('BASE_URL')}/error` },
  plugins: [
    openAPI({ disableDefaultReference: env.get('PROD') }),
    jwt({
      jwks: {
        gracePeriod: convert(30, 'days').to('seconds'),
        rotationInterval: convert(30, 'days').to('seconds')
      },
      jwt: {
        audience: env.get('APP_NAME'),
        issuer: env.get('BASE_URL')
      }
    }),
    ...(env.get('PROD') ? [haveIBeenPwned({ customPasswordCompromisedMessage: 'Please use a secure password.' })] : [])
  ],
  secondaryStorage: {
    delete(key) {
      redis.del(key)
    },
    get(key) {
      return redis.get(key)
    },
    set(key, value, ttl) {
      if (ttl) {
        redis.set(key, value, 'EX', ttl)
      } else {
        redis.set(key, value)
      }
    }
  },
  secret: env.get('AUTH_SECRET'),
  session: {
    cookieCache: {
      enabled: true,
      maxAge: convert(...env.get('AUTH_SESSION_MAX_AGE')).to('seconds'),
      strategy: 'jwe'
    },
    expiresIn: convert(...env.get('AUTH_SESSION_EXPIRES_IN')).to('seconds'),
    freshAge: convert(...env.get('AUTH_SESSION_FRESH_AGE')).to('seconds'),
    storeSessionInDatabase: true,
    updateAge: convert(...env.get('AUTH_SESSION_UPDATE_AGE')).to('seconds')
  },
  socialProviders: {
    facebook: {
      clientId: env.get('AUTH_FACEBOOK_CLIENT_ID'),
      clientSecret: env.get('AUTH_FACEBOOK_CLIENT_SECRET'),
      enabled: true,
      prompt: 'select_account'
    },
    google: {
      clientId: env.get('AUTH_GOOGLE_CLIENT_ID'),
      clientSecret: env.get('AUTH_GOOGLE_CLIENT_SECRET'),
      enabled: true,
      prompt: 'select_account'
    }
  },
  trustedOrigins: env.get('TRUSTED_ORIGINS'),
  user: {
    changeEmail: {
      enabled: true,
      async sendChangeEmailConfirmation(data) {
        // ...
      }
    },
    deleteUser: {
      deleteTokenExpiresIn: convert(...env.get('AUTH_DELETE_TOKEN_EXPIRES_IN')).to('seconds'),
      enabled: true,
      async sendDeleteAccountVerification(data) {
        // ...
      }
    }
  },
  verification: {
    disableCleanup: false
  }
})

Additional context

  • Logs:
01:26:05.193 ◌ [app·better-auth] Hash password: ******
01:26:05.213 ✖ [app·signInWithPassword] (no message)
APIError: Invalid email or password
status: 401 │ type: INVALID_EMAIL_OR_PASSWORD
body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"}
  • The issue only arises while experimental.joins: true; toggling it off allows email/password sign-in to work correctly.
  • Logs clearly show the handler hashing passwords during sign-in instead of verifying them (Hash password: ******).
  • This config references environment values, so no secrets are exposed.
Originally created by @codelonesomest on GitHub (Nov 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6400 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure Better-Auth with email/password sign-in enabled and set `experimental.joins: true`. 2. Attempt to sign in using valid credentials (existing user account). 3. The server log prints `Hash password: ******` and the API responds with `401 INVALID_EMAIL_OR_PASSWORD` even though the password is correct. ### Current vs. Expected behavior - **Current:** When joins are enabled, the flow calls `emailAndPassword.password.hash` during sign-in (log clearly shows `Hash password: ******`) and returns `401 INVALID_EMAIL_OR_PASSWORD` for every login, so valid credentials never succeed. - **Expected:** The sign-in flow should call `emailAndPassword.password.verify`, so the provided password is compared to the stored hash and valid credentials authenticate successfully when `experimental.joins` is `true`. ### What version of Better Auth are you using? 1.4.3 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:45 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8103", "release": "25.0.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "16.00 GB", "freeMemory": "0.85 GB" }, "node": { "version": "v22.21.1", "env": "production" }, "packageManager": { "name": "bun", "version": "1.3.3" }, "frameworks": [ { "name": "hono", "version": "4.10.7" } ], "databases": null, "betterAuth": { "version": "1.4.3", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { db } from '@root/db' import { kyselyAdapter } from '@root/db/helpers/adapter/kysely' import { betterAuth } from 'better-auth' import { haveIBeenPwned, jwt, openAPI } from 'better-auth/plugins' import convert from 'convert' import dayjs from 'dayjs' import { isEmpty } from 'es-toolkit/compat' import { ulid } from 'ulid' import env from './env' import { redis } from './redis' .... const auth = betterAuth({ account: { accountLinking: { enabled: true, trustedProviders: ['google', 'facebook'] } }, advanced: { cookiePrefix: env.get('APP_NAME'), crossSubDomainCookies: { domain: env.get('AUTH_COOKIE_DOMAIN'), enabled: true }, database: { generateId: () => ulid() }, defaultCookieAttributes: { httpOnly: true, partitioned: true, sameSite: 'lax', secure: true }, ipAddress: { disableIpTracking: false, ipAddressHeaders: ['X-IP', ...headersToCheck] } }, appName: env.get('APP_NAME'), basePath: '/auth/v1', baseURL: env.get('BASE_URL'), database: kyselyAdapter(db, { type: 'postgres', usePlural: true }), disabledPaths: ['/sign-in/email', '/sign-up/email', '/get-session', '/sign-out', '/forget-password', '/reset-password', '/verify-email', '/send-verification-email', '/change-email', '/change-password', '/update-user', '/delete-user', '/reset-password/', '/request-password-reset', '/list-sessions', '/revoke-session', '/revoke-sessions', '/revoke-other-sessions', '/list-accounts', '/delete-user/callback', '/refresh-token', '/get-access-token', '/account-info'], emailAndPassword: { enabled: true, password: { hash(password) { return Bun.password.hash(password, { algorithm: 'argon2id', memoryCost: 19456, timeCost: 2 }) }, verify(data) { return Bun.password.verify(data.password, data.hash, 'argon2id') } }, requireEmailVerification: true, resetPasswordTokenExpiresIn: convert(...env.get('AUTH_RESET_PASSWORD_TOKEN_EXPIRES_IN')).to('seconds'), revokeSessionsOnPasswordReset: true, async sendResetPassword(data) { // ... } }, emailVerification: { autoSignInAfterVerification: true, expiresIn: convert(...env.get('AUTH_EMAIL_VERIFICATION_TOKEN_EXPIRES_IN')).to('seconds'), sendOnSignIn: false, sendOnSignUp: true, async sendVerificationEmail(data) { // ... } }, logger: { level: 'warn', log(level, message, ...args) { logger.getChild('better-auth')[level](message, { ...args }) } }, onAPIError: { errorURL: `${env.get('BASE_URL')}/error` }, plugins: [ openAPI({ disableDefaultReference: env.get('PROD') }), jwt({ jwks: { gracePeriod: convert(30, 'days').to('seconds'), rotationInterval: convert(30, 'days').to('seconds') }, jwt: { audience: env.get('APP_NAME'), issuer: env.get('BASE_URL') } }), ...(env.get('PROD') ? [haveIBeenPwned({ customPasswordCompromisedMessage: 'Please use a secure password.' })] : []) ], secondaryStorage: { delete(key) { redis.del(key) }, get(key) { return redis.get(key) }, set(key, value, ttl) { if (ttl) { redis.set(key, value, 'EX', ttl) } else { redis.set(key, value) } } }, secret: env.get('AUTH_SECRET'), session: { cookieCache: { enabled: true, maxAge: convert(...env.get('AUTH_SESSION_MAX_AGE')).to('seconds'), strategy: 'jwe' }, expiresIn: convert(...env.get('AUTH_SESSION_EXPIRES_IN')).to('seconds'), freshAge: convert(...env.get('AUTH_SESSION_FRESH_AGE')).to('seconds'), storeSessionInDatabase: true, updateAge: convert(...env.get('AUTH_SESSION_UPDATE_AGE')).to('seconds') }, socialProviders: { facebook: { clientId: env.get('AUTH_FACEBOOK_CLIENT_ID'), clientSecret: env.get('AUTH_FACEBOOK_CLIENT_SECRET'), enabled: true, prompt: 'select_account' }, google: { clientId: env.get('AUTH_GOOGLE_CLIENT_ID'), clientSecret: env.get('AUTH_GOOGLE_CLIENT_SECRET'), enabled: true, prompt: 'select_account' } }, trustedOrigins: env.get('TRUSTED_ORIGINS'), user: { changeEmail: { enabled: true, async sendChangeEmailConfirmation(data) { // ... } }, deleteUser: { deleteTokenExpiresIn: convert(...env.get('AUTH_DELETE_TOKEN_EXPIRES_IN')).to('seconds'), enabled: true, async sendDeleteAccountVerification(data) { // ... } } }, verification: { disableCleanup: false } }) ``` ### Additional context - **Logs:** ``` 01:26:05.193 ◌ [app·better-auth] Hash password: ****** 01:26:05.213 ✖ [app·signInWithPassword] (no message) APIError: Invalid email or password status: 401 │ type: INVALID_EMAIL_OR_PASSWORD body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"} ``` - The issue only arises while `experimental.joins: true`; toggling it off allows email/password sign-in to work correctly. - Logs clearly show the handler hashing passwords during sign-in instead of verifying them (`Hash password: ******`). - This config references environment values, so no secrets are exposed.
GiteaMirror added the lockedbug labels 2026-04-15 17:56:06 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 29, 2025):

It looks like when experimental.joins is enabled the authentication flow is mistakenly calling your password.hash function at sign‐in rather than using password.verify, so even with valid credentials the verification never occurs correctly. As a next step, we recommend testing with experimental.joins disabled to confirm that password verification works as expected. You should also verify that your password.verify method is defined and operating correctly in your configuration. This appears to be an issue with the experimental flag altering the expected behavior of the sign-in flow, and we’re investigating a fix in an upcoming release. Does disabling the experimental flag resolve your sign-in problem, or do you have additional logs that might help us troubleshoot further? Tag @better-auth-agent if you would like an answer from the Agent.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3591009179 --> @better-auth-agent[bot] commented on GitHub (Nov 29, 2025): It looks like when experimental.joins is enabled the authentication flow is mistakenly calling your password.hash function at sign‐in rather than using password.verify, so even with valid credentials the verification never occurs correctly. As a next step, we recommend testing with experimental.joins disabled to confirm that password verification works as expected. You should also verify that your password.verify method is defined and operating correctly in your configuration. This appears to be an issue with the experimental flag altering the expected behavior of the sign-in flow, and we’re investigating a fix in an upcoming release. Does disabling the experimental flag resolve your sign-in problem, or do you have additional logs that might help us troubleshoot further? Tag @better-auth-agent if you would like an answer from the Agent. <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@ping-maxwell commented on GitHub (Dec 1, 2025):

Can you enable debug logs in the kysely adapter options to see what it logs when you do the flow with experimental joins?

<!-- gh-comment-id:3597342723 --> @ping-maxwell commented on GitHub (Dec 1, 2025): Can you enable debug logs in the kysely adapter options to see what it logs when you do the flow with experimental joins?
Author
Owner

@codelonesomest commented on GitHub (Dec 1, 2025):

Without experimental joins:

Query 1: 'select "primary".* from (select * from "users" where "users"."email" = $1) as "primary"'
Param 1: '$1=example@example.com'

Query 2: 'select "primary".* from (select * from "accounts" where "accounts"."user_id" = $1 limit $2) as "primary"'
Param 2: '$1=01KB5P7R40RK799WPERNWXADFC, $2=100'

WITH experimental joins query

Query: 'select "primary".*, "join_accounts"."account_id" as "_joined_accounts_account_id", "join_accounts"."provider_id" as "_joined_accounts_provider_id", "join_accounts"."user_id" as "_joined_accounts_user_id", "join_accounts"."access_token" as "_joined_accounts_access_token", "join_accounts"."refresh_token" as "_joined_accounts_refresh_token", "join_accounts"."id_token" as "_joined_accounts_id_token", "join_accounts"."access_token_expires_at" as "_joined_accounts_access_token_expires_at", "join_accounts"."refresh_token_expires_at" as "_joined_accounts_refresh_token_expires_at", "join_accounts"."scope" as "_joined_accounts_scope", "join_accounts"."password" as "_joined_accounts_password", "join_accounts"."created_at" as "_joined_accounts_created_at", "join_accounts"."updated_at" as "_joined_accounts_updated_at", "join_accounts"."id" as "_joined_accounts_id" from (select * from "users" where "users"."email" = $1) as "primary" left join "accounts" as "join_accounts" on "join_accounts"."user_id" = "primary"."id"'

Params '$1=example@example.com'

<!-- gh-comment-id:3597443551 --> @codelonesomest commented on GitHub (Dec 1, 2025): ### Without experimental joins: **Query 1**: 'select "primary".* from (select * from "users" where "users"."email" = $1) as "primary"' **Param 1**: '$1=example@example.com' **Query 2**: 'select "primary".* from (select * from "accounts" where "accounts"."user_id" = $1 limit $2) as "primary"' **Param 2**: '$1=01KB5P7R40RK799WPERNWXADFC, $2=100' ### WITH experimental joins query **Query:** 'select "primary".*, "join_accounts"."account_id" as "_joined_accounts_account_id", "join_accounts"."provider_id" as "_joined_accounts_provider_id", "join_accounts"."user_id" as "_joined_accounts_user_id", "join_accounts"."access_token" as "_joined_accounts_access_token", "join_accounts"."refresh_token" as "_joined_accounts_refresh_token", "join_accounts"."id_token" as "_joined_accounts_id_token", "join_accounts"."access_token_expires_at" as "_joined_accounts_access_token_expires_at", "join_accounts"."refresh_token_expires_at" as "_joined_accounts_refresh_token_expires_at", "join_accounts"."scope" as "_joined_accounts_scope", "join_accounts"."password" as "_joined_accounts_password", "join_accounts"."created_at" as "_joined_accounts_created_at", "join_accounts"."updated_at" as "_joined_accounts_updated_at", "join_accounts"."id" as "_joined_accounts_id" from (select * from "users" where "users"."email" = $1) as "primary" left join "accounts" as "join_accounts" on "join_accounts"."user_id" = "primary"."id"' **Params** '$1=example@example.com'
Author
Owner

@ping-maxwell commented on GitHub (Dec 1, 2025):

Hey, what I meant is to set this in your adapter options:
kyselyAdapter(db, {debugLogs: true})

It provides more information about the response.

<!-- gh-comment-id:3598069036 --> @ping-maxwell commented on GitHub (Dec 1, 2025): Hey, what I meant is to set this in your adapter options: `kyselyAdapter(db, {debugLogs: true})` It provides more information about the response.
Author
Owner

@codelonesomest commented on GitHub (Dec 1, 2025):

Hey, what I meant is to set this in your adapter options: kyselyAdapter(db, {debugLogs: true})

It provides more information about the response.

02:04:03.940 ✖ [app·better-auth] Credential account not found
 02:04:03.941 ✖ [app·signInWithPassword] (no message)
 {
    status: 'UNAUTHORIZED',
    body: {
      code: 'INVALID_EMAIL_OR_PASSWORD',
      message: 'Invalid email or password'
    },
    headers: {},
    statusCode: 401
  }
} 
  APIError: Invalid email or password
    status: 401 │ type: INVALID_EMAIL_OR_PASSWORD
    body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"}
    ╭─ stack trace
    │ 0. 📦 better-auth/dist/api-wsWZUpFT.mjs:2147:13
    ╰─
<!-- gh-comment-id:3598092651 --> @codelonesomest commented on GitHub (Dec 1, 2025): > Hey, what I meant is to set this in your adapter options: `kyselyAdapter(db, {debugLogs: true})` > > It provides more information about the response. ``` 02:04:03.940 ✖ [app·better-auth] Credential account not found 02:04:03.941 ✖ [app·signInWithPassword] (no message) { status: 'UNAUTHORIZED', body: { code: 'INVALID_EMAIL_OR_PASSWORD', message: 'Invalid email or password' }, headers: {}, statusCode: 401 } } APIError: Invalid email or password status: 401 │ type: INVALID_EMAIL_OR_PASSWORD body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"} ╭─ stack trace │ 0. 📦 better-auth/dist/api-wsWZUpFT.mjs:2147:13 ╰─ ```
Author
Owner

@codelonesomest commented on GitHub (Dec 1, 2025):

This is how i trigger the sign in.

auth.api.signInEmail({
      body: {
        callbackURL: input.callbackURL,
        email: input.email,
        password: input.password,
        rememberMe: input.rememberMe
      },
      headers: context.c.req.raw.headers,
      returnHeaders: true
 })
<!-- gh-comment-id:3598111729 --> @codelonesomest commented on GitHub (Dec 1, 2025): This is how i trigger the sign in. ```typescript auth.api.signInEmail({ body: { callbackURL: input.callbackURL, email: input.email, password: input.password, rememberMe: input.rememberMe }, headers: context.c.req.raw.headers, returnHeaders: true }) ```
Author
Owner

@ping-maxwell commented on GitHub (Dec 1, 2025):

Hmm, those are not the right logs 🤔
Could it be that you didn't include the adapter debug logs it by accident?

This is an example from my test project of what you should be seeing when enabling debug logs:

Image
<!-- gh-comment-id:3598342920 --> @ping-maxwell commented on GitHub (Dec 1, 2025): Hmm, those are not the right logs 🤔 Could it be that you didn't include the adapter debug logs it by accident? This is an example from my test project of what you should be seeing when enabling debug logs: <img width="843" height="976" alt="Image" src="https://github.com/user-attachments/assets/52ee4c90-8c7a-4935-9c72-5a3a70d1e0be" />
Author
Owner

@ping-maxwell commented on GitHub (Dec 1, 2025):

(even if there are no results, or if the query fails, you should still see something)

<!-- gh-comment-id:3598349136 --> @ping-maxwell commented on GitHub (Dec 1, 2025): (even if there are no results, or if the query fails, you should still see something)
Author
Owner

@codelonesomest commented on GitHub (Dec 2, 2025):

No, only this result is out from better auth logger.

{
 ...
database: kyselyAdapter(db, { debugLogs: true, type: 'postgres', usePlural: true }),
logger: {
      level: 'debug',
      log(level, message, ...args) {
        logger.getChild('better-auth')[level](`${message} {args}`, { args })
      }
    },
}
13:25:57.449 ✖ [app·better-auth] Credential account not found [ { email: 'example@example.com' } ]
 13:25:57.450 ✖ [app·signInWithPassword] (no message)
 {
    status: 'UNAUTHORIZED',
    body: {
      code: 'INVALID_EMAIL_OR_PASSWORD',
      message: 'Invalid email or password'
    },
    headers: {},
    statusCode: 401
  }
} 
  APIError: Invalid email or password
    status: 401 │ type: INVALID_EMAIL_OR_PASSWORD
    body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"}
    ╭─ stack trace
    │ 0. 📦 better-auth/dist/api-wsWZUpFT.mjs:2147:13
    ╰─
<!-- gh-comment-id:3600252003 --> @codelonesomest commented on GitHub (Dec 2, 2025): No, only this result is out from better auth logger. ```typescript { ... database: kyselyAdapter(db, { debugLogs: true, type: 'postgres', usePlural: true }), logger: { level: 'debug', log(level, message, ...args) { logger.getChild('better-auth')[level](`${message} {args}`, { args }) } }, } ``` ``` 13:25:57.449 ✖ [app·better-auth] Credential account not found [ { email: 'example@example.com' } ] 13:25:57.450 ✖ [app·signInWithPassword] (no message) { status: 'UNAUTHORIZED', body: { code: 'INVALID_EMAIL_OR_PASSWORD', message: 'Invalid email or password' }, headers: {}, statusCode: 401 } } APIError: Invalid email or password status: 401 │ type: INVALID_EMAIL_OR_PASSWORD body: {"code":"INVALID_EMAIL_OR_PASSWORD","message":"Invalid email or password"} ╭─ stack trace │ 0. 📦 better-auth/dist/api-wsWZUpFT.mjs:2147:13 ╰─ ```
Author
Owner

@Livog commented on GitHub (Dec 4, 2025):

Here are my findings on this after updating our adapter to support joins.

After upgrading to 1.4 I started having issues with email/password login, even when join was set to false.

In my case, the behavior was caused by this line in sign-in.ts:

const user = await ctx.context.internalAdapter.findUserByEmail(email, {
  includeAccounts: true,
});

Passing includeAccounts: true adds a forced join in findOne, which then expects the result to match this destructuring:

const { account: accounts, ...user } = result;

I am not a fan of hardcoded strings like "account" here, I would rather have it connected to the modelKey of account or to a shared const, but that is just me.

So unless the adapter that devs use support join, it will return invalid response.

Relevant references:
f61a5f4be7/packages/better-auth/src/api/routes/sign-in.ts (L451-L453)
f61a5f4be7/packages/better-auth/src/db/internal-adapter.ts (L740-L760)

<!-- gh-comment-id:3614623187 --> @Livog commented on GitHub (Dec 4, 2025): Here are my findings on this after updating our adapter to support joins. After upgrading to 1.4 I started having issues with email/password login, even when join was set to false. In my case, the behavior was caused by this line in sign-in.ts: ```ts const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true, }); ``` Passing includeAccounts: true adds a forced join in findOne, which then expects the result to match this destructuring: ```ts const { account: accounts, ...user } = result; ``` I am not a fan of hardcoded strings like "account" here, I would rather have it connected to the modelKey of account or to a shared const, but that is just me. So unless the adapter that devs use support join, it will return invalid response. Relevant references: https://github.com/better-auth/better-auth/blob/f61a5f4be7748f1dca9d23263b62173b4dc1c378/packages/better-auth/src/api/routes/sign-in.ts#L451-L453 https://github.com/better-auth/better-auth/blob/f61a5f4be7748f1dca9d23263b62173b4dc1c378/packages/better-auth/src/db/internal-adapter.ts#L740-L760
Author
Owner

@ping-maxwell commented on GitHub (Dec 5, 2025):

@Livog can you open a new issue? looks separate to this if you're facing issues without join enabled.

<!-- gh-comment-id:3616180771 --> @ping-maxwell commented on GitHub (Dec 5, 2025): @Livog can you open a new issue? looks separate to this if you're facing issues without join enabled.
Author
Owner

@Livog commented on GitHub (Dec 5, 2025):

@Livog can you open a new issue? looks separate to this if you're facing issues without join enabled.

@ping-maxwell https://github.com/better-auth/better-auth/issues/6543

<!-- gh-comment-id:3616271894 --> @Livog commented on GitHub (Dec 5, 2025): > [@Livog](https://github.com/Livog) can you open a new issue? looks separate to this if you're facing issues without join enabled. @ping-maxwell https://github.com/better-auth/better-auth/issues/6543
Author
Owner

@ping-maxwell commented on GitHub (Dec 8, 2025):

Hey @codelonesomest I discovered a bug causing the adapter factory to not log any debugLogs, please run the following installation and run through the flow again with debugLogs enabled still.

npm i https://pkg.pr.new/better-auth/better-auth@6597

This time you should see debug logs.

<!-- gh-comment-id:3624265735 --> @ping-maxwell commented on GitHub (Dec 8, 2025): Hey @codelonesomest I discovered a bug causing the adapter factory to not log any debugLogs, please run the following installation and run through the flow again with `debugLogs` enabled still. ```bash npm i https://pkg.pr.new/better-auth/better-auth@6597 ``` This time you should see debug logs.
Author
Owner

@codelonesomest commented on GitHub (Dec 8, 2025):

@ping-maxwell Here you go.

2025-12-08T12:24:48.122Z INFO [Better Auth]: [Kysely Adapter] #0 [1/3] findOne: {
  model: "users",
  where: [
    {
      operator: "eq",
      connector: "AND",
      field: "email",
      value: "example@example.com",
    }
  ],
  select: undefined,
  join: {
    accounts: {
      on: [Object ...],
      limit: 100,
      relation: "one-to-many",
    },
  },
}
 20:24:48.226 ◌ [app·db·pool] Connection reserved from pool
 20:24:48.233 ◌ [app·db·pool] Query executed: command='SELECT' rows=1
 20:24:48.233 ● [app·db·kysely] 🟢 DB: undefined | undefined | '6.2 ms'
2025-12-08T12:24:48.234Z INFO [Better Auth]: [Kysely Adapter] #0 [2/3] findOne (DB Result): {
  model: "users",
  data: {
    id: "01KBJ45BAB5X4H8Q8BZJKEZM8S",
    name: "example",
    email: "example@example.com",
    emailVerified: true,
    image: null,
    createdAt: 2025-12-03T12:49:26.346Z,
    updatedAt: 2025-12-03T12:51:37.753Z,
    _JoinedAccountsAccountId: "01KBJ45BAB5X4H8Q8BZJKEZM8S",
    _JoinedAccountsProviderId: "credential",
    _JoinedAccountsUserId: "01KBJ45BAB5X4H8Q8BZJKEZM8S",
    _JoinedAccountsAccessToken: null,
    _JoinedAccountsRefreshToken: null,
    _JoinedAccountsIdToken: null,
    _JoinedAccountsAccessTokenExpiresAt: null,
    _JoinedAccountsRefreshTokenExpiresAt: null,
    _JoinedAccountsScope: null,
    _JoinedAccountsPassword: "$argon2id$v=19$m=19456,t=2,p=1$bPLdFcj9Yq9KtG/zuM09WfZtr3MGa0qQ/V9chWe07qg$qec2cjj4XrFHE3nRloWNZtN4wqnuhDGpHBowPcsc+fE",
    _JoinedAccountsCreatedAt: 2025-12-03T12:49:26.375Z,
    _JoinedAccountsUpdatedAt: 2025-12-03T13:15:23.736Z,
    _JoinedAccountsId: "01KBJ45BB7YYVMRYXCCK5F3J0M",
    accounts: [],
  },
}
 20:24:48.234 ◌ [app·db·pool] Connection released to pool
2025-12-08T12:24:48.235Z INFO [Better Auth]: [Kysely Adapter] #0 [3/3] findOne (Parsed Result): {
  model: "users",
  data: {
    name: "example",
    email: "example@example.com",
    emailVerified: true,
    image: null,
    createdAt: 2025-12-03T12:49:26.346Z,
    updatedAt: 2025-12-03T12:51:37.753Z,
    id: "01KBJ45BAB5X4H8Q8BZJKEZM8S",
    account: [],
  },
}
 20:24:48.254 ✖ [app·better-auth] Credential account not found [ { email: 'example@example.com' } ]
 20:24:48.255 ✖ [app·orpc·error] (no message)
 {
    defined: true,
    code: 'INTERNAL_SERVER_ERROR',
    status: 500,
    data: undefined,
    [cause]: undefined
  }
} 
  Error: Invalid email or password
    status: 500 │ code: INTERNAL_SERVER_ERROR
    ╭─ stack trace
    │ 0. 📦 @orpc/server/dist/shared/server.Ds4HPpvH.mjs:78:20
    │ 1. apps/api-client/src/@orpc/routes/auth/route.ts:232:26
    ╰─
 20:24:48.264 ✖ [app·http]  P  500 /api/v1/auth/sign-in-with-password [MZECYPDK] ↓137.00B ↑115.00B 170.85ms
─────────────────────────────────────────────────────────────────────────────────────
<!-- gh-comment-id:3626675462 --> @codelonesomest commented on GitHub (Dec 8, 2025): @ping-maxwell Here you go. ```txt 2025-12-08T12:24:48.122Z INFO [Better Auth]: [Kysely Adapter] #0 [1/3] findOne: { model: "users", where: [ { operator: "eq", connector: "AND", field: "email", value: "example@example.com", } ], select: undefined, join: { accounts: { on: [Object ...], limit: 100, relation: "one-to-many", }, }, } 20:24:48.226 ◌ [app·db·pool] Connection reserved from pool 20:24:48.233 ◌ [app·db·pool] Query executed: command='SELECT' rows=1 20:24:48.233 ● [app·db·kysely] 🟢 DB: undefined | undefined | '6.2 ms' 2025-12-08T12:24:48.234Z INFO [Better Auth]: [Kysely Adapter] #0 [2/3] findOne (DB Result): { model: "users", data: { id: "01KBJ45BAB5X4H8Q8BZJKEZM8S", name: "example", email: "example@example.com", emailVerified: true, image: null, createdAt: 2025-12-03T12:49:26.346Z, updatedAt: 2025-12-03T12:51:37.753Z, _JoinedAccountsAccountId: "01KBJ45BAB5X4H8Q8BZJKEZM8S", _JoinedAccountsProviderId: "credential", _JoinedAccountsUserId: "01KBJ45BAB5X4H8Q8BZJKEZM8S", _JoinedAccountsAccessToken: null, _JoinedAccountsRefreshToken: null, _JoinedAccountsIdToken: null, _JoinedAccountsAccessTokenExpiresAt: null, _JoinedAccountsRefreshTokenExpiresAt: null, _JoinedAccountsScope: null, _JoinedAccountsPassword: "$argon2id$v=19$m=19456,t=2,p=1$bPLdFcj9Yq9KtG/zuM09WfZtr3MGa0qQ/V9chWe07qg$qec2cjj4XrFHE3nRloWNZtN4wqnuhDGpHBowPcsc+fE", _JoinedAccountsCreatedAt: 2025-12-03T12:49:26.375Z, _JoinedAccountsUpdatedAt: 2025-12-03T13:15:23.736Z, _JoinedAccountsId: "01KBJ45BB7YYVMRYXCCK5F3J0M", accounts: [], }, } 20:24:48.234 ◌ [app·db·pool] Connection released to pool 2025-12-08T12:24:48.235Z INFO [Better Auth]: [Kysely Adapter] #0 [3/3] findOne (Parsed Result): { model: "users", data: { name: "example", email: "example@example.com", emailVerified: true, image: null, createdAt: 2025-12-03T12:49:26.346Z, updatedAt: 2025-12-03T12:51:37.753Z, id: "01KBJ45BAB5X4H8Q8BZJKEZM8S", account: [], }, } 20:24:48.254 ✖ [app·better-auth] Credential account not found [ { email: 'example@example.com' } ] 20:24:48.255 ✖ [app·orpc·error] (no message) { defined: true, code: 'INTERNAL_SERVER_ERROR', status: 500, data: undefined, [cause]: undefined } } Error: Invalid email or password status: 500 │ code: INTERNAL_SERVER_ERROR ╭─ stack trace │ 0. 📦 @orpc/server/dist/shared/server.Ds4HPpvH.mjs:78:20 │ 1. apps/api-client/src/@orpc/routes/auth/route.ts:232:26 ╰─ 20:24:48.264 ✖ [app·http] P 500 /api/v1/auth/sign-in-with-password [MZECYPDK] ↓137.00B ↑115.00B 170.85ms ───────────────────────────────────────────────────────────────────────────────────── ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19136