Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: migrate auth POC to Better Auth by KMKoushik · Pull Request #421 · usesend/useSend · GitHub
Skip to content

feat: migrate auth POC to Better Auth - #421

Closed
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc
Closed

feat: migrate auth POC to Better Auth#421
KMKoushik wants to merge 1 commit into
mainfrom
agent/better-auth-poc

Conversation

@KMKoushik

@KMKoushikKMKoushik commented Jul 11, 2026

Copy link
Copy Markdown
Member

What changed

  • replaces NextAuth v4 with Better Auth 1.6.23 across the Next.js handler, React client, server sessions, sign-in, and sign-out flows
  • preserves the existing integer User.id domain model and maps Better Auth onto the existing Prisma auth tables
  • replaces the five-character Math.random() email token with a six-digit OTP that is hashed at rest, expires after five minutes, and locks after three failed attempts
  • adds Redis-backed atomic auth rate limiting, explicit CSRF/trusted-origin checks, and OAuth token encryption
  • adds a database-free HTTP E2E suite covering OTP issuance, session cookies, session reads, revocation, and cross-origin rejection
  • documents schema compatibility, rollout risks, rollback considerations, and the remaining PostgreSQL/browser E2E work

Why

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.prisma
  • targeted ESLint over all changed TypeScript/TSX files with zero warnings
  • Better Auth HTTP E2E: 2/2 passing
  • auth configuration unit tests: 3/3 passing
  • API suite: 20/20 passing
  • broader unit suite: 98 tests passing; one existing React JSX runtime resolution failure remains in unsubscribe/page.unit.test.ts
  • TRPC suite: 8 tests passing; the same existing React JSX runtime resolution failure remains in campaign-security.trpc.test.ts

See apps/web/BETTER_AUTH_POC.md for the cutover checklist and risk assessment.


Summary by cubic

Migrated authentication from next-auth to better-auth@1.6.23, keeping numeric User.id and Prisma tables while upgrading email sign-in, session handling, and security.

  • New Features

    • Replaced next-auth with better-auth, preserving existing Prisma auth tables and integer User.id.
    • Email sign-in now uses a six-digit OTP that’s hashed, expires in 5 minutes, and locks after 3 failed attempts.
    • Added Redis-backed rate limiting, CSRF and trusted-origin checks, and HttpOnly/SameSite=Lax session cookies.
    • Enabled OAuth token encryption and removed dangerous email-based account linking.
    • Added a lightweight HTTP E2E suite (OTP, sessions, revocation, cross-origin rejection) and updated unit tests.
    • Introduced authClient hooks and a new getServerAuthSession for server-side reads.
  • Migration

    • No DB migration is included; generate and review a Prisma migration for the additive Better Auth fields before rollout.
    • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL (temporary fallback to NEXTAUTH_* supported).
    • Re-verify GitHub/Google callback URLs under /api/auth/callback/<provider> per environment.
    • Expect a one-time sign-in for users after cutover due to cookie/protocol changes.

Written for commit 5a7c1be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Migrated sign-in and session handling to Better Auth.
    • Added email OTP sign-in with stronger verification and rate-limiting protections.
    • Added support for secure OAuth sign-in, session management, sign-out, and waitlist access.
    • Added end-to-end authentication testing and commands.
  • Bug Fixes

    • Improved authentication request validation, cookie security, and session handling.
    • Added clearer login progress and error feedback during email and social sign-in.
  • Documentation

    • Added migration, rollout, database compatibility, and verification guidance for the updated authentication system.

Entire-Checkpoint: 9b7fcac4a1b5
@vercel

vercelBot commented Jul 11, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
unsend-marketingReadyReadyPreview, CommentJul 11, 2026 11:15pm

@coderabbitai

coderabbitaiBot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The 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)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly matches the PR’s main change: migrating the auth proof of concept to Better Auth.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/package.json

ESLint 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.tsx

ESLint 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.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 19 others

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.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with Cloudflare Pages Cloudflare Pages

Latest commit:5a7c1be
Status: ✅ Deploy successful!
Preview URL:https://e17a3df6.usesend.pages.dev
Branch Preview URL:https://agent-better-auth-poc.usesend.pages.dev

View logs

@KMKoushik
KMKoushik marked this pull request as ready for review July 12, 2026 04:30

@coderabbitaicoderabbitaiBot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (5)
apps/web/src/server/auth.e2e.test.ts (2)

40-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add 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 win

Verify sign-in succeeded before testing origin rejection.

The second test does not check the send-verification-otp response (line 90–95) or the signInResponse.status (line 96–102). If either fails, latestOtp retains 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 win

Merge 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 value

Sign-out has no failure feedback.

Unlike handleLogout in waitlist-form.tsx, this relies solely on fetchOptions.onSuccess; if authClient.signOut() errors, the user gets no toast and stays on the page. Consider adding an onError handler 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 win

Merge duplicate imports from ~/server/auth.

Lines 2 and 4 both import from ~/server/auth; combine into a single statement to avoid a likely import/no-duplicates lint 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

📥 Commits

Reviewing files that changed from the base of the PR and between 28970c9 and 5a7c1be.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • apps/web/BETTER_AUTH_POC.md
  • apps/web/package.json
  • apps/web/prisma/schema.prisma
  • apps/web/src/app/(dashboard)/layout.tsx
  • apps/web/src/app/(dashboard)/settings/team/team-members-list.tsx
  • apps/web/src/app/api/auth/[...all]/route.ts
  • apps/web/src/app/api/auth/[...nextauth]/route.ts
  • apps/web/src/app/login/login-page.tsx
  • apps/web/src/app/login/page.tsx
  • apps/web/src/app/signup/page.tsx
  • apps/web/src/app/wait-list/waitlist-form.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/env.js
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/providers/auth-provider.tsx
  • apps/web/src/providers/dashboard-provider.tsx
  • apps/web/src/server/api/trpc.ts
  • apps/web/src/server/auth.e2e.test.ts
  • apps/web/src/server/auth.ts
  • apps/web/src/server/auth.unit.test.ts
  • apps/web/src/server/mailer.ts
  • apps/web/src/test/setup/setup-env.ts
  • apps/web/vitest.default.config.ts
  • apps/web/vitest.e2e.config.ts
  • package.json
💤 Files with no reviewable changes (1)
  • apps/web/src/app/api/auth/[...nextauth]/route.ts

Comment on lines +85 to +89
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await sendSignInOtpEmail(email, otp, authBaseUrl);
}
},

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.

🎯 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.ts

Repository: 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.tsx

Repository: 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:


Handle unsupported OTP types explicitly
sendVerificationOTP 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.

Comment on lines 27 to 29
if (env.NODE_ENV === "development") {
logger.info({ email, url, token }, "Sending sign in email");
logger.info({ email, otp }, "Sending sign in OTP");
return;

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.

🔒 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.

Suggested change
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"],

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.

📐 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.

Suggested change
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.

Sign up for freeto 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

@KMKoushik