feat(auth): replace Supabase Auth with self-hosted Better Auth - #24
Conversation
Moves identity into the app's own Neon database, removing the last service that could pause underneath the product. Email/password with mandatory verification plus Google OAuth, sessions via cookies rather than bearer tokens. Also fixes a cross-database defect the Neon migration introduced: getUserTier read `profiles` through the retired Supabase client while quota usage came from Neon, so every user silently resolved to FREE. Tier now lives on the Better Auth user table and is read via Prisma. Verified end to end against Neon: signup, pre-verification sign-in rejected 403, verification, sign-in, and a protected route returning 401 without a cookie and 200 with one. Refs #6
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe pull request replaces Supabase Auth with Better Auth, adding Prisma-backed authentication tables, backend session handling, frontend cookie-authenticated requests, Google OAuth configuration, email verification and reset hooks, user locale persistence, and updated authentication and service tests. ChangesBetter Auth migration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant authClient
participant Backend
participant BetterAuth
participant Prisma
Browser->>authClient: submit sign-in or sign-up
authClient->>Backend: send request to /api/auth
Backend->>BetterAuth: delegate authentication request
BetterAuth->>Prisma: create or read user and session
Prisma-->>BetterAuth: return authentication data
BetterAuth-->>Browser: set session cookie and return session
Browser->>Backend: send API request with credentials included
Backend->>BetterAuth: get session from request headers
BetterAuth-->>Backend: return session user
Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@backend/src/services/emailService.ts`:
- Around line 15-26: Update sendVerificationEmail and sendPasswordResetEmail to
deliver the links through the configured email provider instead of logging them.
Remove console.info calls and ensure neither the recipient address nor the URL
is written to production logs, while preserving each function’s existing
parameters and async behavior.
In `@frontend/src/hooks/useAuth.tsx`:
- Around line 96-99: Update signInWithGoogle to pass errorCallbackURL:
'/sign-in' alongside the existing Google provider and callbackURL options, then
update the related mock assertion to require this new option.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6fb538e8-9811-45b3-a67d-7cd895e60d47
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (25)
backend/package.jsonbackend/src/app.tsbackend/src/config/auth.tsbackend/src/config/env.tsbackend/src/middleware/auth.tsbackend/src/services/__tests__/userService.test.tsbackend/src/services/emailService.tsbackend/src/services/userService.tsfrontend/package.jsonfrontend/src/api/client.tsfrontend/src/components/auth/GoogleSignInButton.tsxfrontend/src/components/chat/__tests__/ChatLauncher.test.tsxfrontend/src/hooks/__tests__/useAuth.test.tsxfrontend/src/hooks/__tests__/useChat.test.tsxfrontend/src/hooks/useAuth.tsxfrontend/src/hooks/useChat.tsfrontend/src/hooks/useLocale.tsxfrontend/src/lib/auth-client.tsfrontend/src/lib/supabase.tsfrontend/src/pages/__tests__/SignInPage.test.tsxfrontend/src/pages/__tests__/SignUpPage.test.tsxfrontend/vite.config.tsprisma/migrations/20260730081714_add_better_auth_tables/migration.sqlprisma/migrations/20260730083243_add_user_locale/migration.sqlprisma/schema.prisma
💤 Files with no reviewable changes (1)
- frontend/src/lib/supabase.ts
| export async function sendVerificationEmail(email: string, url: string): Promise<void> { | ||
| console.info(`[Email] verification for ${email}: ${url}`) | ||
| } | ||
|
|
||
| /** | ||
| * Sends a password-reset link. | ||
| * | ||
| * @param email - Recipient address | ||
| * @param url - Better Auth password-reset link | ||
| */ | ||
| export async function sendPasswordResetEmail(email: string, url: string): Promise<void> { | ||
| console.info(`[Email] password reset for ${email}: ${url}`) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
Replace token logging with real email delivery before enabling this flow.
requireEmailVerification makes new accounts unusable without delivery, while logging raw reset URLs exposes bearer credentials to anyone with log access and enables account takeover. Send these links through the email provider and never log the URL or recipient email in production.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 15-15: Avoid logging sensitive data
Context: console.info([Email] verification for ${email}: ${url})
Note: [CWE-532] Insertion of Sensitive Information into Log File.
(log-sensitive-data-typescript)
[warning] 25-25: Avoid logging sensitive data
Context: console.info([Email] password reset for ${email}: ${url})
Note: [CWE-532] Insertion of Sensitive Information into Log File.
(log-sensitive-data-typescript)
🤖 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 `@backend/src/services/emailService.ts` around lines 15 - 26, Update
sendVerificationEmail and sendPasswordResetEmail to deliver the links through
the configured email provider instead of logging them. Remove console.info calls
and ensure neither the recipient address nor the URL is written to production
logs, while preserving each function’s existing parameters and async behavior.
Source: Linters/SAST tools
| const signInWithGoogle = useCallback(async () => { | ||
| const { error } = await supabase.auth.signInWithOAuth({ | ||
| provider: 'google', | ||
| options: { redirectTo: `${window.location.origin}/dashboard` } | ||
| }) | ||
| return { error } | ||
| const { error } = await authClient.signIn.social({ provider: 'google', callbackURL: '/dashboard' }) | ||
| return { error: toAuthError(error) } | ||
| }, []) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does better-auth signIn.social redirect to the app's errorCallbackURL or its own default error page when errorCallbackURL is not provided?
💡 Result:
Yes, better-auth signIn.social redirects to a default error page when an error occurs and a specific errorCallbackURL is not provided [1][2]. By design, if you do not specify an errorCallbackURL, the authentication flow falls back to a default error page, which is typically located at /error relative to your application's base URL [3][2][4]. In scenarios where an errorCallbackURL is provided, the framework is intended to use that URL to redirect the user [5][4]. However, historically, there have been reported cases and complexities where the errorCallbackURL might be ignored—particularly if the OAuth state (which stores the callback information) is lost, expired, or fails validation before the redirect can occur [6][2]. Recent updates and patches have been introduced to improve this behavior, such as ensuring that the per-flow errorCallbackURL is honored even during certain types of state validation failures [6]. When the system cannot recover or identify an errorCallbackURL due to missing state, it reverts to the global default error page (or the URL configured via onAPIError.errorURL in your server-side auth configuration) [2][4][7].
Citations:
- 1: https://www.better-auth.com/docs/basic-usage
- 2: the errorCallbackURL isnt workin in the oauth, even after updating the latest version v1.3.11 better-auth/better-auth#4694
- 3: Oauth Error callback URL discrepencies better-auth/better-auth#5467
- 4:
onAPIError.errorURLis ignored in OAuth callback flow better-auth/better-auth#5518 - 5: https://mintlify.wiki/better-auth/better-auth/authentication/social-providers
- 6: fix(oauth): honor per-flow errorCallbackURL when state validation fails better-auth/better-auth#9789
- 7: feat: support callback function in onAPIError's errorURL better-auth/better-auth#5721
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
fd -a 'useAuth\.tsx|better-auth|auth' . | sed 's#^\./##' | head -100
echo "== locate authClient and sign in.social =="
rg -n "authClient|signInWithGoogle|errorCallbackURL|callbackURL|\\?error=|error_description|AuthProvider" .
echo "== useAuth excerpt if exists =="
if [ -f frontend/src/hooks/useAuth.tsx ]; then
wc -l frontend/src/hooks/useAuth.tsx
sed -n '1,140p' frontend/src/hooks/useAuth.tsx | cat -n
fi
echo "== package/deps mentioning better-auth =="
for f in package.json frontend/package.json pnpm-lock.yaml frontend/pnpm-lock.yaml; do
[ -f "$f" ] && { echo "--- $f"; rg -n "better-auth|`@better-auth`" "$f" | head -50 || true; }
done
echo "== source files with authClient if any =="
rg -n "createAuthClient|betterAuth|signIn\.social\(" -S .Repository: AlaskanTuna/SolarSim
Length of output: 15951
🌐 Web query:
better-auth signIn.social errorCallbackURL /api/auth/error OAuth error redirect default error page
💡 Result:
The errorCallbackURL parameter in better-auth is designed to redirect users to a custom URL when an OAuth authentication process fails [1][2]. However, users have frequently reported issues where this parameter is ignored, leading to a fallback to the library's default error page at /api/auth/error [3][4][5]. If you are encountering this issue, consider the following technical context and workarounds: 1. Use Absolute URLs: Ensure that your errorCallbackURL is a full, absolute URL (e.g., https://yourdomain.com/login) rather than a relative path [4]. 2. Configure Global Error Handling: As a fallback, configure the onAPIError.errorURL option in your server-side Better Auth initialization [4][6]. This sets a global destination for errors when a per-flow errorCallbackURL is missing or fails to be parsed [4][7]. typescript // Server-side auth config export const auth = betterAuth({ onAPIError: { errorURL: "/your-custom-error-page", }, }); 3. Known Limitations and Fixes: The handling of errorCallbackURL has undergone several updates to address race conditions and state parsing issues [8][9][10]. Ensure your version of better-auth is up to date, as recent PRs have focused on improving the consistency of redirecting to the per-flow URL instead of the default [8][10]. 4. Middleware Strategy: If inconsistencies persist, a common robust workaround is to use middleware (e.g., in Next.js) to intercept requests to /api/auth/error and perform a programmatic redirect to your desired custom error page [7]. The library is actively being improved to better support dynamic and per-flow error handling, including support for functions in errorURL configurations [10].
Citations:
- 1: https://www.better-auth.com/docs/basic-usage
- 2: https://better-auth-better-auth-16.mintlify.app/authentication/social-providers
- 3:
errorCallbackURLis not working it is ignored on error better-auth/better-auth#1580 - 4: the errorCallbackURL isnt workin in the oauth, even after updating the latest version v1.3.11 better-auth/better-auth#4694
- 5: Oauth Error callback URL discrepencies better-auth/better-auth#5467
- 6: https://www.better-auth.com/docs/reference/options
- 7:
onAPIError.errorURLis ignored in OAuth callback flow better-auth/better-auth#5518 - 8: fix: respect
errorCallbackURLin failed oauth flows better-auth/better-auth#4650 - 9: PR: Fix — OAuth per-flow errorCallbackURL on state-mismatch (#5467) better-auth/better-auth#5521
- 10: feat: support callback function in onAPIError's errorURL better-auth/better-auth#5721
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== backend auth config =="
wc -l backend/src/config/auth.ts
cat -n backend/src/config/auth.ts
echo "== frontend auth client =="
cat -n frontend/src/lib/auth-client.ts
echo "== routes likely mounted at /sign-in =="
rg -n "Route\(|path=|sign-in|/auth|AuthProvider" frontend/src -S | head -120Repository: AlaskanTuna/SolarSim
Length of output: 10471
🌐 Web query:
better-auth 1.6 signIn.social errorCallbackURL documentation
💡 Result:
In Better Auth 1.6, the signIn.social method accepts an errorCallbackURL parameter intended to define the redirect path if an error occurs during the social sign-in process [1][2][3]. Usage: You can pass errorCallbackURL as a property within the object provided to authClient.signIn.social: await authClient.signIn.social({ provider: "google", callbackURL: "/dashboard", errorCallbackURL: "/error", // Redirect if an error occurs newUserCallbackURL: "/welcome", }); Context and Known Limitations: While documented as a standard parameter, the handling of errorCallbackURL has been a subject of development and community discussion regarding its reliability in specific OAuth error scenarios [4][5][6]. 1. Intended Behavior: The parameter is designed to override the default error redirection behavior when a social sign-in flow fails [1][7]. 2. Global Configuration: If you need a fallback or a default behavior for all authentication errors, you can also configure the onAPIError option in your server-side auth initialization [8]: export const auth = betterAuth({ onAPIError: { errorURL: "/auth/error", // Global default error page }, }); 3. Technical Note: Developers have historically reported instances where OAuth provider-level errors (such as access_denied) might bypass the custom errorCallbackURL in favor of the global default error page [4][6]. Recent updates, such as those discussed in project pull requests, have aimed to ensure that errorCallbackURL is correctly parsed from the OAuth state to provide more consistent behavior [5]. If you find the parameter is not being respected in your specific implementation, ensure your version of better-auth is up to date, as this has been an area of active refinement [5].
Citations:
- 1: https://better-auth-better-auth-16.mintlify.app/authentication/social-providers
- 2: https://better-auth-better-auth-16.mintlify.app/basic-usage
- 3: https://raw.githubusercontent.com/better-auth/better-auth/main/docs/content/docs/basic-usage.mdx
- 4: Oauth Error callback URL discrepencies better-auth/better-auth#5467
- 5: fix: respect
errorCallbackURLin failed oauth flows better-auth/better-auth#4650 - 6:
errorCallbackURLis not working it is ignored on error better-auth/better-auth#1580 - 7: https://www.better-auth.com/docs/basic-usage
- 8: https://www.better-auth.com/docs/reference/options
Redirect OAuth errors back to the app.
Without errorCallbackURL, signIn.social falls back to Better Auth’s default endpoint for sign-in errors, so Google errors won’t return to /sign-in where the URL-param toast/cleanup logic is mounted. Add an in-app error return URL, such as /sign-in, and update the related mock assertion.
🤖 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 `@frontend/src/hooks/useAuth.tsx` around lines 96 - 99, Update signInWithGoogle
to pass errorCallbackURL: '/sign-in' alongside the existing Google provider and
callbackURL options, then update the related mock assertion to require this new
option.
Neon's free tier scales compute to zero when idle. A cold start measured 5.4s, past Prisma's 5s default, so the first request after a quiet period failed with "Can't reach database server" — reproduced as a 500 on the Google sign-in path against a force-suspended compute.
Google SSO verified ✅The two redirect URIs are registered and working. Verified with a negative control so the result means something:
The control proves the test can detect a mismatch and doesn't fire for either real URI. Better Auth's generated request also carries PKCE ( Full interactive login isn't verifiable from here — it needs real Google credentials — but every step up to the consent screen is confirmed. Found and fixed a production bug while testingThe first SSO attempt returned Not a fluke. Neon's free tier scales compute to zero when idle; the wake measured 5.4 s, past Prisma's 5 s default connect timeout. So the first user to hit the site after a quiet period gets a 500 on sign-in. Reproduced deterministically by force-suspending the compute via the Neon API rather than waiting for idle:
Fixed in Worth noting for issue #9: this interacts with the Neon CU-hour budget, so the Render cold-start mitigation still cannot be a database pinger. |
Replaces Supabase Auth with self-hosted Better Auth, backed by the app's own Neon database. This removes the last third-party service that could pause underneath the product — the failure that took
solarsim.techdown.Implemented by two parallel Codex workers on disjoint file sets (backend seam / frontend client), with the shared surfaces (env schema, Prisma models,
config/auth.ts) written up front to prevent collision.What changed
backend/src/config/auth.ts(new) — Better Auth server config: email/password withrequireEmailVerification: true(matching the retired Supabaseenable_confirmations), Google OAuth, and account linking so an email user who later signs in with Google lands on the same account.middleware/auth.ts—supabase.auth.getUser(token)→auth.api.getSession(...). Thereq.user = { id, email }contract is byte-identical, so no route handler changed.app.ts— mountsapp.all('/api/auth/*splat', toNodeHandler(auth))beforeexpress.json()(Better Auth needs the raw body) and after CORS.lib/auth-client.ts,lib/supabase.tsdeleted. Auth is now cookie-based, soapi/client.tsanduseChat.tsdrop bearer-token plumbing forcredentials: 'include'.User/Session/Account/Verificationadded and migrated to Neon.tierfolded ontoUser; theprofilestable and the entireauthschema are dropped.encryptOAuthTokens: true) rather than Better Auth's plaintext default.Fixes a defect introduced by the Neon migration
getUserTierreadprofilesthrough the retired Supabase client whileProjectQuotaUsagecame from Neon — quota was split across two databases and every user silently resolved toFREE, throttling PRO users.profileswasn't modelled in Prisma, which is why the earlier migration missed it. Tier now lives on the Better Auth user table and is read via Prisma.Proven, not assumed: promoting the probe user to
PROin Neon flipped/api/quotafromFREE/limit 5 toPRO/limit 20.Preserved behaviours
user_metadata; now alocalefield on the user, so the feature survives rather than being silently dropped.Test plan
pnpm typecheck— exit 0pnpm test— 216 frontend + 124 backend passingeslint . --max-warnings 68— exit 0 (61 warnings, down from 68)prettier --check .— exit 0tier: FREE; sign-in before verification →403 EMAIL_NOT_VERIFIED; verification →emailVerified: true; sign-in → session cookie; protected route401without cookie /200with; probe user deleted afterwardsNot in this PR
emailService.tslogs verification/reset links instead of sending. Issue Send verification and password-reset email directly via Resend #7 wires Resend and ports the branded templates.<baseURL>/api/auth/callback/googleregistered in Google Cloud Console. Config is in place; the redirect URI is a manual step.Refs #6
Summary by CodeRabbit