Uh oh!
There was an error while loading. Please reload this page.
feat: migrate auth POC to Better Auth - #421
Conversation
Entire-Checkpoint: 9b7fcac4a1b5
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe web application migrates authentication from NextAuth to Better Auth. It adds Better Auth server and client configuration, Prisma fields, environment variables, OTP email sign-in, Redis rate limiting, OAuth providers, session mapping, and a Next.js auth route. Dashboard, login, signup, waitlist, sidebar, and tRPC integrations now use Better Auth. New unit and E2E tests validate OTP sessions, sign-out revocation, cookie properties, and trusted-origin enforcement. Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
apps/web/package.jsonESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. apps/web/src/app/(dashboard)/layout.tsxESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. apps/web/src/app/(dashboard)/settings/team/team-members-list.tsxESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
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. Comment |
Deploying usesend with |
| Latest commit: | 5a7c1be |
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e17a3df6.usesend.pages.dev |
| Branch Preview URL: | https://agent-better-auth-poc.usesend.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)
40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd status check before asserting null on the post-sign-out session response.
Line 85 asserts the session body is null without first verifying the HTTP status. If the endpoint returns an error (non-200), the body might not be null, producing a confusing failure message rather than a clear status mismatch.
♻️ Proposed fix
const signedOutSession = await handleAuthRequest( new Request(`${baseUrl}/api/auth/get-session`, { headers: { cookie: clearedCookie ?? "" }, }), ); + expect(signedOutSession.status).toBe(200); await expect(signedOutSession.json()).resolves.toBeNull();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/server/auth.e2e.test.ts` around lines 40 - 86, Update the post-sign-out session flow in the “creates a session, reads it from its cookie, and revokes it” test to assert signedOutSession.status is 200 before checking that its JSON body is null. Keep the existing null-body assertion unchanged after the status check.
88-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winVerify sign-in succeeded before testing origin rejection.
The second test does not check the
send-verification-otpresponse (line 90–95) or thesignInResponse.status(line 96–102). If either fails,latestOtpretains the value from the first test (for a different email), and the subsequent sign-out request may receive a 403 for reasons unrelated to the untrusted origin check. Adding status assertions on the setup steps ensures the 403 is genuinely from origin rejection.♻️ Proposed fix
const secondEmail = "auth-csrf-e2e@example.com"; - await handleAuthRequest(+ const sendResponse = await handleAuthRequest( jsonRequest("/api/auth/email-otp/send-verification-otp", { email: secondEmail, type: "sign-in", }), ); + expect(sendResponse.status).toBe(200); const signInResponse = await handleAuthRequest( jsonRequest("/api/auth/sign-in/email-otp", { email: secondEmail, otp: latestOtp, name: "Auth CSRF E2E", }), ); + expect(signInResponse.status).toBe(200);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/server/auth.e2e.test.ts` around lines 88 - 118, In the “rejects browser requests from an untrusted origin” test, assert successful statuses for the email OTP request returned by the first handleAuthRequest call and the subsequent signInResponse before issuing sign-out. Keep the existing setup and final 403 assertion unchanged so the test specifically validates origin rejection.apps/web/src/app/signup/page.tsx (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMerge duplicate imports from
~/server/auth.Same as
login/page.tsx: lines 2 and 4 both import from~/server/auth.♻️ Proposed change
-import { getServerAuthSession } from "~/server/auth";+import { authProviders, getServerAuthSession } from "~/server/auth"; import LoginPage from "../login/login-page"; -import { authProviders } from "~/server/auth";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/signup/page.tsx` at line 4, Merge the duplicate imports from ~/server/auth in the signup page into a single import declaration, preserving all currently imported symbols and matching the consolidated import style used by login/page.tsx.apps/web/src/components/AppSideBar.tsx (1)
370-378: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSign-out has no failure feedback.
Unlike
handleLogoutinwaitlist-form.tsx, this relies solely onfetchOptions.onSuccess; ifauthClient.signOut()errors, the user gets no toast and stays on the page. Consider adding anonErrorhandler for consistency.♻️ Suggested tweak
onClick={() => authClient.signOut({ fetchOptions: { onSuccess: () => window.location.assign("/login"), + onError: () =>+ toast.error("Unable to log out. Please try again."), }, }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/components/AppSideBar.tsx` around lines 370 - 378, Add an onError handler to the authClient.signOut fetchOptions in the DropdownMenuItem, matching the failure-feedback behavior used by handleLogout in waitlist-form.tsx. Display an appropriate error toast and preserve the existing onSuccess redirect to /login.apps/web/src/app/login/page.tsx (1)
4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMerge duplicate imports from
~/server/auth.Lines 2 and 4 both import from
~/server/auth; combine into a single statement to avoid a likelyimport/no-duplicateslint failure.♻️ Proposed change
-import { getServerAuthSession } from "~/server/auth";+import { authProviders, getServerAuthSession } from "~/server/auth"; import LoginPage from "./login-page"; -import { authProviders } from "~/server/auth";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/app/login/page.tsx` at line 4, Combine the duplicate imports from ~/server/auth in the login page into one import statement, preserving all currently imported symbols and avoiding any other changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/server/auth.ts`:
- Around line 85-89: Update sendVerificationOTP to explicitly handle OTP types
other than "sign-in": when the type is unsupported, fail loudly or emit a clear
warning rather than silently returning. Preserve the existing sendSignInOtpEmail
behavior for sign-in requests.
In `@apps/web/src/server/mailer.ts`:
- Around line 27-29: Update the development-mode logging in the mailer flow
around the logger.info call to never include the live otp and to avoid exposing
the full email at info level. Omit otp entirely and use the centralized Pino
serializer for email redaction rather than adding ad-hoc masking; preserve the
existing development-mode early return.
In `@apps/web/vitest.e2e.config.ts`:
- Line 8: Update the e2e test include pattern in the Vitest configuration to
match both TypeScript extensions covered by the default config’s exclusion
pattern, including .e2e.test.ts and .e2e.test.tsx files.
---
Nitpick comments:
In `@apps/web/src/app/login/page.tsx`:
- Line 4: Combine the duplicate imports from ~/server/auth in the login page
into one import statement, preserving all currently imported symbols and
avoiding any other changes.
In `@apps/web/src/app/signup/page.tsx`:
- Line 4: Merge the duplicate imports from ~/server/auth in the signup page into
a single import declaration, preserving all currently imported symbols and
matching the consolidated import style used by login/page.tsx.
In `@apps/web/src/components/AppSideBar.tsx`:
- Around line 370-378: Add an onError handler to the authClient.signOut
fetchOptions in the DropdownMenuItem, matching the failure-feedback behavior
used by handleLogout in waitlist-form.tsx. Display an appropriate error toast
and preserve the existing onSuccess redirect to /login.
In `@apps/web/src/server/auth.e2e.test.ts`:
- Around line 40-86: Update the post-sign-out session flow in the “creates a
session, reads it from its cookie, and revokes it” test to assert
signedOutSession.status is 200 before checking that its JSON body is null. Keep
the existing null-body assertion unchanged after the status check.
- Around line 88-118: In the “rejects browser requests from an untrusted origin”
test, assert successful statuses for the email OTP request returned by the first
handleAuthRequest call and the subsequent signInResponse before issuing
sign-out. Keep the existing setup and final 403 assertion unchanged so the test
specifically validates origin rejection.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 280f646a-1e34-4749-8db9-c5b2b961f8ad
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (25)
apps/web/BETTER_AUTH_POC.mdapps/web/package.jsonapps/web/prisma/schema.prismaapps/web/src/app/(dashboard)/layout.tsxapps/web/src/app/(dashboard)/settings/team/team-members-list.tsxapps/web/src/app/api/auth/[...all]/route.tsapps/web/src/app/api/auth/[...nextauth]/route.tsapps/web/src/app/login/login-page.tsxapps/web/src/app/login/page.tsxapps/web/src/app/signup/page.tsxapps/web/src/app/wait-list/waitlist-form.tsxapps/web/src/components/AppSideBar.tsxapps/web/src/env.jsapps/web/src/lib/auth-client.tsapps/web/src/providers/auth-provider.tsxapps/web/src/providers/dashboard-provider.tsxapps/web/src/server/api/trpc.tsapps/web/src/server/auth.e2e.test.tsapps/web/src/server/auth.tsapps/web/src/server/auth.unit.test.tsapps/web/src/server/mailer.tsapps/web/src/test/setup/setup-env.tsapps/web/vitest.default.config.tsapps/web/vitest.e2e.config.tspackage.json
💤 Files with no reviewable changes (1)
- apps/web/src/app/api/auth/[...nextauth]/route.ts
| async sendVerificationOTP({ email, otp, type }) { | ||
| if (type === "sign-in") { | ||
| await sendSignInOtpEmail(email, otp, authBaseUrl); | ||
| } | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash# Check if the signup page or other flows use emailOTP for types other than "sign-in"
rg -n "emailOTP\|sendVerificationOTP\|otp.*sign-up\|otp.*email-verification" --type=ts -g '!**/node_modules/**' -g '!**/.next/**' apps/web/src/app/signup/ apps/web/src/app/login/ apps/web/src/server/auth.tsRepository: usesend/useSend
Length of output: 153
🏁 Script executed:
#!/bin/bashset -euo pipefail
echo'--- repo files of interest ---'
git ls-files 'apps/web/src/server/*''apps/web/src/app/*'| sed -n '1,200p'echoecho'--- search for Better Auth / OTP usage ---'
rg -n "sendVerificationOTP|emailOTP|sign-in|sign-up|email-verification|change-email|verificationOTP|better auth|better-auth|authBaseUrl" apps/web/src -g '!**/node_modules/**' -g '!**/.next/**'echoecho'--- inspect auth.ts around the callback ---'
nl -ba apps/web/src/server/auth.ts | sed -n '1,180p'Repository: usesend/useSend
Length of output: 13964
🏁 Script executed:
#!/bin/bashset -euo pipefail
echo'--- apps/web/src/server/auth.ts (relevant section) ---'
sed -n '1,160p' apps/web/src/server/auth.ts
echoecho'--- apps/web/src/server/auth.e2e.test.ts ---'
sed -n '1,180p' apps/web/src/server/auth.e2e.test.ts
echoecho'--- apps/web/src/app/login/login-page.tsx ---'
sed -n '1,140p' apps/web/src/app/login/login-page.tsxRepository: usesend/useSend
Length of output: 13661
🌐 Web query:
Better Auth emailOTP sendVerificationOTP type values sign-in sign-up email-verification change-email documentation
💡 Result:
In Better Auth, the sendVerificationOTP method is primarily used with the Email OTP plugin [1][2]. The type parameter specifies the purpose of the OTP being sent [3][2]. Default Type Values The default supported types are [4][5][2]: - sign-in - email-verification - forget-password Custom Types You can extend these types by providing a customTypes array when configuring the Email OTP plugin [4]. For example: const auth = betterAuth({ plugins: [ emailOTP({ customTypes: ["set-password", "your-own-type"], async sendVerificationOTP({ email, otp, type }) { // Handle logic for custom or default types }, }), ], }); When custom types are provided, the TypeScript definitions and the API client's sendVerificationOTP method will automatically include these new values in the type union, allowing for proper autocompletion and validation [4]. Usage Contexts - Sign-in/Sign-up: Use the sign-in type to authenticate users. If a user is not registered, they may be automatically registered depending on your configuration [3][5]. - Email Verification: Use the email-verification type to verify a user's email address [1][2]. - Change Email: The change-email flow is typically handled by the core user management features rather than the Email OTP plugin directly [6][7]. To initiate a change, use the client-side changeEmail method [7]. If you need an OTP for this specific flow, you would generally handle it via custom logic or by utilizing the plugin's infrastructure if custom types are configured [4][7]. It is recommended to implement the sendVerificationOTP function to handle the actual delivery of the OTP based on the provided type [5][2]. Do not await the email sending process to prevent timing attacks; consider using background execution if necessary [2].
Citations:
- 1: https://better-auth.com/docs/plugins/email-otp
- 2: https://raw.githubusercontent.com/better-auth/better-auth/main/docs/content/docs/plugins/email-otp.mdx
- 3: https://github.com/better-auth/better-auth/blob/9fed16b6/packages/better-auth/src/plugins/email-otp/routes.ts
- 4: Add
customTypessupport to Email OTP plugin better-auth/better-auth#2760 - 5: better-auth/better-auth@e91be5a3
- 6: https://better-auth.com/docs/concepts/users-accounts
- 7: https://beta.better-auth.com/docs/concepts/users-accounts
Handle unsupported OTP types explicitlysendVerificationOTP only handles "sign-in", so other OTP types silently no-op. If sign-in is the only intended flow, fail loudly or log an explicit unsupported-type warning.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/server/auth.ts` around lines 85 - 89, Update sendVerificationOTP
to explicitly handle OTP types other than "sign-in": when the type is
unsupported, fail loudly or emit a clear warning rather than silently returning.
Preserve the existing sendSignInOtpEmail behavior for sign-in requests.
| if (env.NODE_ENV === "development") { | ||
| logger.info({ email, url, token }, "Sending sign in email"); | ||
| logger.info({ email, otp }, "Sending sign in OTP"); | ||
| return; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Avoid logging the email address and OTP at info level.
Line 28 logs both email (PII) and otp (a live authentication credential) at info level. Even in development mode, the OTP should not be logged — it can end up in log aggregators or terminal scrollback. Based on learnings, logging of full email addresses at info level should be redacted via a centralized Pino serializer rather than ad-hoc fixes.
Consider redacting the email and omitting the OTP entirely, or downgrading to a trace/debug level with redacted values.
🛡️ Proposed fix
if (env.NODE_ENV === "development") {
- logger.info({ email, otp }, "Sending sign in OTP");+ logger.info({ email: email?.replace(/(.{2}).+/, "$1***") }, "Sending sign in OTP");
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if(env.NODE_ENV==="development"){ | |
| logger.info({ email, url, token },"Sending sign in email"); | |
| logger.info({ email, otp },"Sending sign in OTP"); | |
| return; | |
| if(env.NODE_ENV==="development"){ | |
| logger.info({email: email?.replace(/(.{2}).+/,"$1***")},"Sending sign in OTP"); | |
| return; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/server/mailer.ts` around lines 27 - 29, Update the
development-mode logging in the mailer flow around the logger.info call to never
include the live otp and to avoid exposing the full email at info level. Omit
otp entirely and use the centralized Pino serializer for email redaction rather
than adding ad-hoc masking; preserve the existing development-mode early return.
Source: Learnings
| baseConfig, | ||
| defineConfig({ | ||
| test: { | ||
| include: ["src/**/*.e2e.test.ts"], |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
E2E include glob should match the default config's exclude pattern.
The default config excludes src/**/*.e2e.test.{ts,tsx} (both .ts and .tsx), but this e2e config only includes src/**/*.e2e.test.ts. A future .e2e.test.tsx file would be silently skipped by both configurations.
🔧 Proposed fix
- include: ["src/**/*.e2e.test.ts"],+ include: ["src/**/*.e2e.test.{ts,tsx}"],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| include: ["src/**/*.e2e.test.ts"], | |
| include: ["src/**/*.e2e.test.{ts,tsx}"], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/vitest.e2e.config.ts` at line 8, Update the e2e test include pattern
in the Vitest configuration to match both TypeScript extensions covered by the
default config’s exclusion pattern, including .e2e.test.ts and .e2e.test.tsx
files.
What changed
User.iddomain model and maps Better Auth onto the existing Prisma auth tablesMath.random()email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attemptsWhy
Auth.js is now maintained by the Better Auth team and still receives critical/security fixes, so this is not an emergency migration. This POC evaluates the Better Auth path while improving the weakest part of the current implementation: the short
Math.random()email sign-in token and permissive OAuth email account linking.Impact and rollout notes
This is a POC and must not be deployed before a reviewed Prisma migration is generated and tested against a production snapshot. The schema changes are additive, but existing browser sessions will not survive the auth cookie/protocol cutover, so users should be expected to sign in once after rollout.
No database migration was generated or run in this PR.
Verification
prisma validate --schema prisma/schema.prismaunsubscribe/page.unit.test.tscampaign-security.trpc.test.tsSee
apps/web/BETTER_AUTH_POC.mdfor the cutover checklist and risk assessment.Summary by cubic
Migrated authentication from
next-authtobetter-auth@1.6.23, keeping numericUser.idand Prisma tables while upgrading email sign-in, session handling, and security.New Features
next-authwithbetter-auth, preserving existing Prisma auth tables and integerUser.id.HttpOnly/SameSite=Laxsession cookies.authClienthooks and a newgetServerAuthSessionfor server-side reads.Migration
BETTER_AUTH_SECRETandBETTER_AUTH_URL(temporary fallback toNEXTAUTH_*supported)./api/auth/callback/<provider>per environment.Written for commit 5a7c1be. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation