Skip to content

⬆️ Upgrade better-auth#2316

Merged
lukevella merged 3 commits intomainfrom
upgrade-better-auth
Apr 7, 2026
Merged

⬆️ Upgrade better-auth#2316
lukevella merged 3 commits intomainfrom
upgrade-better-auth

Conversation

@lukevella
Copy link
Copy Markdown
Owner

@lukevella lukevella commented Apr 7, 2026

Summary by CodeRabbit

  • Chores

    • Updated authentication library dependency to a newer version.
  • Refactor

    • Restructured authentication configuration and plugin setup for cleaner, more maintainable behavior.
  • New Features

    • Existing-user signup now triggers an OTP verification flow so returning users complete sign-in via a 6-digit code.
  • Tests

    • Adjusted test environment SMTP host and updated tests to validate the OTP-based existing-user flow.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Apr 7, 2026 0:41am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
landing Skipped Skipped Apr 7, 2026 0:41am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

Walkthrough

Replaced hardcoded app name with env.APP_NAME, restructured auth plugin wiring to use conditional spreads, moved createAuthMiddleware import, added OTP-on-existing-user sign-up flow, bumped better-auth to ^1.6.0, and changed test SMTP host to 0.0.0.0.

Changes

Cohort / File(s) Summary
Dependency
apps/web/package.json
Bumped better-auth from ^1.4.7 to ^1.6.0.
Auth configuration
apps/web/src/lib/auth.ts
Replaced hardcoded "Rallly" with env.APP_NAME; replaced mutable plugins array with conditionalPlugins (Turnstile captcha, generic OIDC) and spread into plugin list; moved createAuthMiddleware import to better-auth/api; simplified OIDC env logging; added emailAndPassword.onExistingUserSignUp hook to send verification OTP via authLib.api.sendVerificationOTP.
Test env
apps/web/.env.test
Changed SMTP_HOST from localhost to 0.0.0.0.
Tests
apps/web/tests/authentication.spec.ts
Updated existing-user registration test to expect OTP flow: wait for “Finish Logging In”, retrieve OTP via getCode(...), submit 6-digit code, and assert user is signed in.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Web as Web App
  participant Auth as better-auth
  participant Email as SMTP / Mailer
  participant DB as Database

  User->>Web: Attempt sign-up with existing email
  Web->>Auth: emailAndPassword.signUp (detect existing user)
  Auth->>Auth: invoke onExistingUserSignUp hook
  Auth->>Email: sendVerificationOTP(email)
  Email-->>Auth: OTP delivered
  Auth-->>Web: prompt Finish Logging In (OTP UI)
  User->>Web: Submit OTP
  Web->>Auth: verify OTP
  Auth->>DB: fetch/update session
  Auth-->>Web: authentication success (user info)
  Web-->>User: show signed-in UI ("Test User")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through lines of auth tonight,
Swapped a name, let plugins light,
Sent a code when emails clash,
Bumped the version—kept the cache,
Tiny hop, everything's alright ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main change: upgrading the better-auth dependency from 1.4.7 to 1.6.0 and implementing related auth configuration updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upgrade-better-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/tests/authentication.spec.ts (1)

58-66: Replace the narration with an assertion that the old error stays hidden.

This proves the OTP step renders, but it would still pass if the old "email already exists" UI also leaked alongside it. Swapping the new comments for a negative assertion on that copy would make the enumeration regression much tighter.

As per coding guidelines, "Only add comments when it is necessary to explain code that isn't self-explanatory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/tests/authentication.spec.ts` around lines 58 - 66, Replace the
comment that narrates the OTP prompt with a negative assertion ensuring the
prior "email already exists" error UI is not visible; after filling the OTP via
getCode(testUserEmail) and page.getByPlaceholder("Enter your 6-digit
code").fill(code), add an assertion like expecting page.getByText("Email already
exists") to not be visible so the test fails if the old error leaks alongside
the OTP step.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/lib/auth.ts`:
- Around line 97-105: The emailAndPassword config enables
requireEmailVerification but lacks customSyntheticUser, so synthetic sign-up
responses are missing admin and additionalFields and cause schema mismatches;
add a customSyntheticUser handler on the emailAndPassword object (near
requireEmailVerification and onExistingUserSignUp) that returns {...coreFields,
role: "user", banned: false, banReason: null, banExpires: null,
...additionalFields, id} (ensuring additionalFields like timeZone and locale are
included) to populate role, banned, banReason, banExpires and your custom
fields.

---

Nitpick comments:
In `@apps/web/tests/authentication.spec.ts`:
- Around line 58-66: Replace the comment that narrates the OTP prompt with a
negative assertion ensuring the prior "email already exists" error UI is not
visible; after filling the OTP via getCode(testUserEmail) and
page.getByPlaceholder("Enter your 6-digit code").fill(code), add an assertion
like expecting page.getByText("Email already exists") to not be visible so the
test fails if the old error leaks alongside the OTP step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1eddd52f-cc02-43d4-ac09-e21c907565a5

📥 Commits

Reviewing files that changed from the base of the PR and between 748d79f and d0cc67c.

📒 Files selected for processing (2)
  • apps/web/src/lib/auth.ts
  • apps/web/tests/authentication.spec.ts

Comment on lines 97 to +105
emailAndPassword: {
enabled: env.EMAIL_LOGIN_ENABLED !== "false",
requireEmailVerification: true,
onExistingUserSignUp: async ({ user }, request) => {
await authLib.api.sendVerificationOTP({
body: { email: user.email, type: "email-verification" },
request,
});
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/lib/auth.ts | head -120 | tail -30

Repository: lukevella/rallly

Length of output: 1162


🏁 Script executed:

rg "customSyntheticUser" --type ts --type tsx

Repository: lukevella/rallly

Length of output: 87


🏁 Script executed:

rg "Better Auth" --type ts --type tsx -i

Repository: lukevella/rallly

Length of output: 87


🏁 Script executed:

fd "auth" apps/web/src --type f

Repository: lukevella/rallly

Length of output: 274


🏁 Script executed:

cat -n apps/web/src/lib/auth.ts | head -200

Repository: lukevella/rallly

Length of output: 7882


🏁 Script executed:

rg "admin" apps/web/src/lib/auth.ts -i -A 3 -B 3

Repository: lukevella/rallly

Length of output: 499


🏁 Script executed:

rg "schema\|Schema" apps/web/src/lib/auth.ts -A 3 -B 1

Repository: lukevella/rallly

Length of output: 42


🏁 Script executed:

rg "user.*extend\|extend.*user" apps/web/src -i

Repository: lukevella/rallly

Length of output: 42


🏁 Script executed:

cat -n apps/web/src/lib/auth.ts | tail -100

Repository: lukevella/rallly

Length of output: 3291


🏁 Script executed:

grep -n "customSyntheticUser" apps/web/src/lib/auth.ts

Repository: lukevella/rallly

Length of output: 42


🏁 Script executed:

cat -n apps/web/src/lib/auth.ts | sed -n '198,250p'

Repository: lukevella/rallly

Length of output: 1620


🌐 Web query:

Better Auth customSyntheticUser admin plugin requireEmailVerification

💡 Result:

When you enable email enumeration protection—which happens if emailAndPassword.requireEmailVerification: true (or autoSignIn: false)—Better Auth may return a synthetic (fake) user shape on sign-up. If you’re using the admin plugin, you must extend that synthetic user to include the admin plugin’s added user fields (role, banned, banReason, banExpires), otherwise you’ll hit missing-field/type/schema issues. [1]

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

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    customSyntheticUser: ({ coreFields, additionalFields, id }) => ({
      ...coreFields,

      // Admin plugin fields (in schema order)
      role: "user",        // or your configured defaultRole
      banned: false,
      banReason: null,
      banExpires: null,

      ...additionalFields,
      id,
    }),
  },
  plugins: [admin()],
});

customSyntheticUser is specifically intended for this case (“Use when plugins add fields to the user table”). [2]

Sources: [1] [2]


🌐 Web query:

Better Auth 1.5 enumeration-safe sign-up customSyntheticUser

💡 Result:

In Better Auth v1.5, when you enable email enumeration protection (e.g. requireEmailVerification: true or autoSignIn: false), the sign-up endpoint may return a synthetic (fake) user response so the client can’t tell whether an email already exists. If you use plugins that add columns to user, you must use customSyntheticUser to include those plugin fields in the synthetic response (otherwise you’ll get missing-field/type/runtime issues). [1][2]

Example (Admin plugin adds role, banned, banReason, banExpires): [1]

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

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true, // (or autoSignIn: false)
    customSyntheticUser: ({ coreFields, additionalFields, id }) => ({
      ...coreFields,
      // Admin plugin fields (in schema order)
      role: "user",      // or your admin({ defaultRole: ... })
      banned: false,
      banReason: null,
      banExpires: null,

      // any other fields added via Better Auth "additionalFields"
      ...additionalFields,
      id,
    }),
  },
  plugins: [admin()],
});

If you have other plugins/custom fields, add their user-table fields into customSyntheticUser the same way (so the synthetic response matches your user schema). [2]


Add customSyntheticUser to the emailAndPassword block.

With requireEmailVerification: true enabled and the admin plugin active, Better Auth returns a synthetic user on sign-up to prevent email enumeration. You must include customSyntheticUser to populate the admin plugin's fields (role, banned, banReason, banExpires) plus your custom additionalFields (timeZone, locale) in the synthetic response. Without it, the synthetic user will be missing these fields, causing schema mismatches and potential runtime errors.

Example fix:
emailAndPassword: {
  enabled: env.EMAIL_LOGIN_ENABLED !== "false",
  requireEmailVerification: true,
  customSyntheticUser: ({ coreFields, additionalFields, id }) => ({
    ...coreFields,
    role: "user",
    banned: false,
    banReason: null,
    banExpires: null,
    ...additionalFields,
    id,
  }),
  onExistingUserSignUp: async ({ user }, request) => {
    // ...
  },
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/auth.ts` around lines 97 - 105, The emailAndPassword config
enables requireEmailVerification but lacks customSyntheticUser, so synthetic
sign-up responses are missing admin and additionalFields and cause schema
mismatches; add a customSyntheticUser handler on the emailAndPassword object
(near requireEmailVerification and onExistingUserSignUp) that returns
{...coreFields, role: "user", banned: false, banReason: null, banExpires: null,
...additionalFields, id} (ensuring additionalFields like timeZone and locale are
included) to populate role, banned, banReason, banExpires and your custom
fields.

@lukevella lukevella merged commit 3eb22cf into main Apr 7, 2026
13 checks passed
lukevella added a commit that referenced this pull request Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant