Skip to content

fix: proxy Neon Auth through Express to resolve Better Auth CSRF origin errors - #83

Merged
Marcus-Mok-GH merged 3 commits into
mainfrom
fix/neon-auth-proxy
Jun 21, 2026
Merged

fix: proxy Neon Auth through Express to resolve Better Auth CSRF origin errors#83
Marcus-Mok-GH merged 3 commits into
mainfrom
fix/neon-auth-proxy

Conversation

@Marcus-Mok-GH

@Marcus-Mok-GH Marcus-Mok-GH commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Problem

Better Auth (Neon) validates the browser's Origin header as a CSRF guard. When the frontend runs on a different origin from the Neon Auth URL (e.g. localhost:5173 in dev, or a Vercel URL in prod), auth requests are rejected with an "Invalid origin" error.

Solution

Proxy all Better Auth calls through Express. Server-side fetch calls don't carry a browser Origin header, so Neon's CSRF check passes. The frontend now routes auth requests to the same origin (/api/auth/*), which Express proxies upstream to Neon.

Changes

server/index.js

  • Added _neonAuthProxy middleware that forwards all /api/auth/* requests to the Neon Auth URL upstream
  • Forwards cookie and authorization headers; intentionally omits Origin (the fix)
  • Skips /session and /signout which are handled by the custom authRouter
  • Falls back gracefully with a 502 + warning log if NEON_AUTH_URL is not set

src/services/neonAuth.js

  • createAuthClient now points to window.location.origin (same-origin proxy) instead of directly to VITE_NEON_AUTH_URL
  • Removes the env-guard if (!neonAuthUrl) block — client always initialises; proxy handles availability
  • Updated error messages to reflect the new setup

.env.example

  • VITE_NEON_AUTH_URLNEON_AUTH_URL (server-side only, no VITE_ prefix needed)
  • Added comment explaining the proxy pattern

README.md

  • Full rewrite to match the actual codebase: correct stack (Neon Auth, Kysely, Stockfish 18, Vercel serverless), accurate scripts table, complete repo structure tree
  • Env var table updated to NEON_AUTH_URL

Testing

  • npm run dev — OTP sign-in flow works end-to-end (no "Invalid origin" error)
  • NEON_AUTH_URL unset → server logs warning, auth gracefully unavailable
  • Production deploy on Vercel — auth proxy works through serverless function

Notes

The docs research confirmed createAuthClient({ baseURL: window.location.origin }) is the correct pattern for a same-origin proxy. The redirect: 'manual' on the upstream fetch prevents Express from following Neon redirects blindly and returning an opaque response to the browser.

Summary by CodeRabbit

  • Documentation

    • README updated with deployment-focused instructions, revised tech stack, configuration variables, and repository structure.
  • Chores / Configuration

    • Renamed the Neon Auth environment variable from VITE_NEON_AUTH_URL to NEON_AUTH_URL.
  • New Features

    • Added server-side proxying for Neon Auth requests to improve authentication reliability.
  • Chores / Improvements

    • Updated client-side auth configuration to route requests through the server proxy and handle missing proxy configuration more clearly.

…in errors

- Rename VITE_NEON_AUTH_URL → NEON_AUTH_URL (server-side only, no VITE_ prefix)
- Add Express proxy middleware in server/index.js that forwards all /api/auth/*
  requests upstream to Neon, omitting the Origin header so Better Auth's CSRF
  check does not reject them as cross-origin
- Update neonAuth.js client to route through window.location.origin (same-origin
  proxy) instead of calling Neon directly
- Update .env.example and README.md to reflect the new env var and architecture
- README: rewrite to match actual stack (Neon Auth, Kysely, Stockfish 18,
  Vercel serverless, correct scripts table, full repo structure tree)
- README: fix NEON_AUTH_URL env var (was incorrectly VITE_NEON_AUTH_URL)
- server/index.js: add Express proxy middleware for Better Auth /api/auth/*
  routes to resolve cross-origin CSRF errors
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
chess-com-app Ready Ready Preview, Comment Jun 20, 2026 10:04am
chess.com-app Ready Ready Preview, Comment Jun 20, 2026 10:04am

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b095bfd-4230-4fc4-a797-8eec1a0a7c05

📥 Commits

Reviewing files that changed from the base of the PR and between 1a44c21 and 5f7c72f.

📒 Files selected for processing (1)
  • server/index.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/index.js

📝 Walkthrough

Walkthrough

The PR renames VITE_NEON_AUTH_URL to NEON_AUTH_URL to keep the Neon Auth URL server-side only. An Express middleware proxy is added to server/index.js that forwards /api/auth/* requests to Neon while stripping the origin header to avoid Better Auth CSRF rejections. The client auth service is updated to derive its baseURL from window.location.origin. README.md is substantially rewritten.

Changes

Neon Auth server-side proxy

Layer / File(s) Summary
Express Neon Auth proxy middleware
.env.example, server/index.js
Adds conditional middleware under each API prefix to proxy /api/auth/* to NEON_AUTH_URL, forwarding cookie/authorization headers while omitting origin, relaying upstream status and headers with 30s timeout, and returning 502 on failure. .env.example renames the variable from VITE_NEON_AUTH_URL to NEON_AUTH_URL with an updated server-side-only comment.
Client auth service: same-origin proxy baseURL
src/services/neonAuth.js
Replaces import.meta.env.VITE_NEON_AUTH_URL with a neonAuthBaseUrl derived from window.location.origin (browser) or http://localhost:3001 (dev/server). Updates noOpStub error messages and simplifies initialization to a try/catch that falls back to noOpStub.
README and configuration docs
README.md
Rewrites product overview, technical stack, architecture section, scripts table, environment variable tables (renaming to NEON_AUTH_URL), and repository structure tree to reflect the current serverless/Express deployment shape.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Express as Express (/api/auth/*)
  participant NeonAuth as Neon Auth upstream

  rect rgba(135, 206, 250, 0.5)
    note over Browser,NeonAuth: Auth request via same-origin proxy
    Browser->>Express: POST /api/auth/sign-in<br/>(cookie, authorization headers)
    Express->>NeonAuth: Forward method, body, query params<br/>(origin header omitted)
    NeonAuth-->>Express: Response status + headers + body
    Express-->>Browser: Relay upstream response<br/>(strip transfer-encoding, connection)
  end

  rect rgba(255, 160, 122, 0.5)
    note over Browser,Express: Proxy failure path
    Express->>NeonAuth: Upstream fetch
    NeonAuth--xExpress: Network/timeout error
    Express-->>Browser: 502 JSON error
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • Marcus-Mok-GH/Chess.com-app#69: Introduced the initial VITE_NEON_AUTH_URL-based Neon Auth setup in src/services/neonAuth.js that this PR replaces with a server-side proxy.
  • Marcus-Mok-GH/Chess.com-app#70: Wired a toNodeHandler auth handler in server/index.js for /api/auth/*, which this PR supersedes with a direct HTTP proxy middleware.
  • Marcus-Mok-GH/Chess.com-app#73: Also modifies src/services/neonAuth.js and server/index.js auth wiring around the VITE_NEON_AUTH_URL → server-proxied NEON_AUTH_URL migration path.

Poem

🐇 Hoppity-hop, the origin's gone,
No more CSRF to fret upon!
NEON_AUTH_URL stays server-side,
Through Express proxy the requests ride.
The rabbit cheers — the CSRF beast has died! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing a server-side proxy for Neon Auth requests to resolve Better Auth CSRF origin validation errors.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a44c21103

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/index.js
? '?' + new URLSearchParams(req.query).toString()
: '';
const upstreamRes = await fetch(
`${_neonAuthProxyUrl}/api/auth${req.path}${qs}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the configured Neon Auth base path

When NEON_AUTH_URL is copied from the Neon Console as the updated docs/env comments instruct, it is already the Auth Base URL (for example ...neon.build/neondb/auth). Since the Better Auth client reaches Express at /api/auth/... and this middleware is mounted on /api/auth, req.path is already the remaining endpoint path; appending another /api/auth sends OTP/session requests to .../neondb/auth/api/auth/..., which Neon will not serve and leaves sign-in broken. Proxy to the configured base plus req.path (or normalize only true bare origins) instead.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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 (1)
README.md (1)

45-73: ⚡ Quick win

Verify environment variable naming and fallback behavior align with configuration guidance.

The Configuration section prescribes NEON_AUTH_URL as the required server-side variable (line 54), but server/index.js includes a fallback to VITE_NEON_AUTH_URL for backwards compatibility. Consider clarifying in the README whether developers migrating from the old setup should:

  1. Remove VITE_NEON_AUTH_URL and use only NEON_AUTH_URL, or
  2. Keep the old variable temporarily during migration.

This note would help prevent confusion for users upgrading to the new proxy architecture.

🤖 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 `@README.md` around lines 45 - 73, The README Configuration section documents
NEON_AUTH_URL as the required variable but does not mention that server/index.js
maintains a backwards compatibility fallback to VITE_NEON_AUTH_URL. Add a
clarification note in the Configuration section explaining the backwards
compatibility fallback behavior and guidance for users: either they can remove
the old VITE_NEON_AUTH_URL variable and migrate to NEON_AUTH_URL, or they can
keep the old variable temporarily during migration. This will prevent confusion
for users upgrading to the new architecture.
🤖 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 `@README.md`:
- Line 76: The fenced code block containing the repository structure in
README.md is missing a language specifier, which violates the markdownlint MD040
rule. Locate the opening triple backticks (```) that precede the repository
structure content (with directories like chess.com-app/, api/, etc.) and add a
language identifier such as "text" or "bash" immediately after the opening
backticks (e.g., ```text). This will ensure the code block is properly formatted
according to markdown linting standards.

In `@server/index.js`:
- Around line 229-234: The generic header iteration in the
upstreamRes.headers.forEach loop is collapsing multiple Set-Cookie headers into
a single value, breaking authentication flows that require multiple session
cookies. Add 'set-cookie' to the exclusion condition alongside
'transfer-encoding' and 'connection' in the if statement, then after the loop,
separately retrieve all Set-Cookie headers using upstreamRes.getSetCookie() and
properly set them on the response using res.setHeader or res.appendHeader to
preserve multiple cookie values.
- Around line 211-227: The fetch call to the Neon auth proxy (the upstreamRes
assignment) lacks a timeout mechanism, which can cause requests to hang
indefinitely if the upstream service stalls. Implement an abort timeout by
creating an AbortController, setting a timeout that calls abort() if the fetch
operation exceeds a reasonable duration (e.g., 30 seconds), and passing the
controller's signal to the fetch options. Additionally, handle the AbortError
that may be thrown when the timeout triggers, ensuring it is caught and logged
appropriately in the existing try-catch block.

---

Nitpick comments:
In `@README.md`:
- Around line 45-73: The README Configuration section documents NEON_AUTH_URL as
the required variable but does not mention that server/index.js maintains a
backwards compatibility fallback to VITE_NEON_AUTH_URL. Add a clarification note
in the Configuration section explaining the backwards compatibility fallback
behavior and guidance for users: either they can remove the old
VITE_NEON_AUTH_URL variable and migrate to NEON_AUTH_URL, or they can keep the
old variable temporarily during migration. This will prevent confusion for users
upgrading to the new architecture.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 946465e7-a943-4f8a-9e50-537205425abd

📥 Commits

Reviewing files that changed from the base of the PR and between 47827dc and 1a44c21.

📒 Files selected for processing (4)
  • .env.example
  • README.md
  • server/index.js
  • src/services/neonAuth.js

Comment thread README.md
## 📁 Repository Structure

*Confidential. Internal use only.*
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language specifier to fenced code block.

The repository structure code block is missing a language identifier required by markdownlint. Add a language specifier (e.g., ```text or ```bash) to satisfy the MD040 rule.

💡 Proposed fix
-```
+```text
 chess.com-app/
 ├── api/
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 76-76: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@README.md` at line 76, The fenced code block containing the repository
structure in README.md is missing a language specifier, which violates the
markdownlint MD040 rule. Locate the opening triple backticks (```) that precede
the repository structure content (with directories like chess.com-app/, api/,
etc.) and add a language identifier such as "text" or "bash" immediately after
the opening backticks (e.g., ```text). This will ensure the code block is
properly formatted according to markdown linting standards.

Source: Linters/SAST tools

Comment thread server/index.js
Comment thread server/index.js Outdated
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 2 unresolved review comments.

Files modified:

  • server/index.js

Commit: 5f7c72f1f9538a106665f62819e749ff05be15bd

The changes have been pushed to the fix/neon-auth-proxy branch.

Time taken: 5m 37s

Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant