[GH-ISSUE #3215] better-auth works with the wrong baseURL #18154

Closed
opened 2026-04-15 16:32:23 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @Achuttarsing on GitHub (Jun 29, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3215

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I have an express backend served under the https://app.mydomain.org/api/ route that runs better-auth. I configure this with this simple apache lines:

# Reverse-proxy to Express
    ProxyPreserveHost On
    ProxyPass        "/api/" "http://localhost:7001/"
    ProxyPassReverse "/api/" "http://localhost:7001/"

Here is how I init better-auth in express

import Bun from "bun";
import express from "express";
import compression from 'compression';
import bodyParser from "body-parser";
import cors from 'cors';
import mongoose from "mongoose";
import fs from 'fs';
import path from 'path';
import { parse as csvParse } from 'csv-parse';
import { auth } from "./auth";
import { toNodeHandler, fromNodeHeaders } from "better-auth/node";
import { sendEmail } from "./mailer";
import { postMessageToSlack } from "./utils"

const app = express();
const port = 7001;

const MONGO_URI = Bun.env.MONGO_URI;

// Define the list of allowed origins
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'https://mydomain.org', 'https://www.mydomain.org', 'http://192.168.1.37:3000'];

const corsOptions = {
	origin: (origin, callback) => {
		if (!origin || allowedOrigins.includes(origin)) {
			callback(null, true); // Allow the origin
		} else {
			callback(new Error('Not allowed by CORS')); // Disallow the origin
		}
	},
	optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
	credentials: true, // Credentials are supported
	methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
	allowedHeaders: ['Content-Type', 'Authorization'],
}; 


// Set up express framework
app.use(compression());
app.use(cors(corsOptions));
app.options('*', cors(corsOptions));

app.all("/auth/*", toNodeHandler(auth));
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));

Current vs. Expected behavior

The issue

better-auth works without errors (but with the wrong base_url) when:

  • backend baseURL is set to https://app.mydomain.org/auth
  • client baseURL is set to https://app.mydomain.org/api/auth

But the real url is https://app.mydomain.org/api/auth so all the urls the better-auth backend generates (redirection, etc..) are wrong.

The right configuration should be:

  • backend baseURL is set to https://app.mydomain.org/api/auth
  • client baseURL is set to https://app.mydomain.org/api/auth

But here, the request returns 404 and we have the following backend log:

2025-06-29T09:00:06.274Z ERROR [Better Auth]: Error 4816 |       continue;
4817 |     }
4818 |     if (endpoint.options?.metadata?.SERVER_ONLY) continue;
4819 |     const methods = Array.isArray(endpoint.options?.method) ? endpoint.options.method : [endpoint.options?.method];
4820 |     for (const method of methods) {
4821 |       addRoute(router, method, endpoint.path, endpoint);
                              ^
error: NOT_FOUND
      at <anonymous> (/home/theapp/backend/node_modules/better-call/dist/index.js:4821:24)
      at processRequest (/home/theapp/backend/node_modules/better-call/dist/index.js:4818:50)
      at <anonymous> (/home/theapp/backend/node_modules/better-call/dist/index.js:4887:8)

I'm totally lost.
I appreciate your futur help
Thank you in advance

What version of Better Auth are you using?

1.2.10

Provide environment information

Distributor ID:	Debian
Description:	Debian GNU/Linux 12 (bookworm)
Release:	12
Codename:	bookworm
---
Bun: v1.2.17

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)

import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import { MongoClient } from "mongodb";
import { sendEmail } from "./mailer";

const client = new MongoClient(Bun.env.MONGO_URI);
await client.connect();
const db = client.db();

export const auth = betterAuth({
    baseURL: "https://app.mydomain.org/api/auth",
    trustedOrigins: ["http://localhost:3000", "https://mydomain.org"],
    secret: Bun.env.BETTER_AUTH_SECRET,
    database: mongodbAdapter(db),

    emailAndPassword: {
        enabled: true,
    },
    emailVerification: {
        sendOnSignUp: true,
        autoSignInAfterVerification: true,
        async sendVerificationEmail({ user, url }) {
            await sendEmail({
                to: user.email,
                subject: "Confirm your email address",
                html: `<p>Hello!</p> <p>Click to verify your email address: <a href="${url}">${url}</a></p><p>See you soon!</p>`
            });
        }
    },

    socialProviders: {
        google: { 
            clientId: Bun.env.GOOGLE_CLIENT_ID, 
            clientSecret: Bun.env.GOOGLE_CLIENT_SECRET, 
        }, 
    },

});

Additional context

No response

Originally created by @Achuttarsing on GitHub (Jun 29, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3215 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I have an express backend served under the `https://app.mydomain.org/api/` route that runs better-auth. I configure this with this simple apache lines: ``` # Reverse-proxy to Express ProxyPreserveHost On ProxyPass "/api/" "http://localhost:7001/" ProxyPassReverse "/api/" "http://localhost:7001/" ``` Here is how I init better-auth in express ```js import Bun from "bun"; import express from "express"; import compression from 'compression'; import bodyParser from "body-parser"; import cors from 'cors'; import mongoose from "mongoose"; import fs from 'fs'; import path from 'path'; import { parse as csvParse } from 'csv-parse'; import { auth } from "./auth"; import { toNodeHandler, fromNodeHeaders } from "better-auth/node"; import { sendEmail } from "./mailer"; import { postMessageToSlack } from "./utils" const app = express(); const port = 7001; const MONGO_URI = Bun.env.MONGO_URI; // Define the list of allowed origins const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'https://mydomain.org', 'https://www.mydomain.org', 'http://192.168.1.37:3000']; const corsOptions = { origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); // Allow the origin } else { callback(new Error('Not allowed by CORS')); // Disallow the origin } }, optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 credentials: true, // Credentials are supported methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', allowedHeaders: ['Content-Type', 'Authorization'], }; // Set up express framework app.use(compression()); app.use(cors(corsOptions)); app.options('*', cors(corsOptions)); app.all("/auth/*", toNodeHandler(auth)); app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); ``` ### Current vs. Expected behavior ## The issue better-auth works without errors (but with the wrong base_url) when: - backend baseURL is set to `https://app.mydomain.org/auth` - client baseURL is set to `https://app.mydomain.org/api/auth` But the real url is `https://app.mydomain.org/api/auth` so all the urls the better-auth backend generates (redirection, etc..) are wrong. The right configuration should be: - backend baseURL is set to `https://app.mydomain.org/api/auth` - client baseURL is set to `https://app.mydomain.org/api/auth` But here, the request returns 404 and we have the following backend log: ``` 2025-06-29T09:00:06.274Z ERROR [Better Auth]: Error 4816 | continue; 4817 | } 4818 | if (endpoint.options?.metadata?.SERVER_ONLY) continue; 4819 | const methods = Array.isArray(endpoint.options?.method) ? endpoint.options.method : [endpoint.options?.method]; 4820 | for (const method of methods) { 4821 | addRoute(router, method, endpoint.path, endpoint); ^ error: NOT_FOUND at <anonymous> (/home/theapp/backend/node_modules/better-call/dist/index.js:4821:24) at processRequest (/home/theapp/backend/node_modules/better-call/dist/index.js:4818:50) at <anonymous> (/home/theapp/backend/node_modules/better-call/dist/index.js:4887:8) ``` I'm totally lost. I appreciate your futur help Thank you in advance ### What version of Better Auth are you using? 1.2.10 ### Provide environment information ```bash Distributor ID: Debian Description: Debian GNU/Linux 12 (bookworm) Release: 12 Codename: bookworm --- Bun: v1.2.17 ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { MongoClient } from "mongodb"; import { sendEmail } from "./mailer"; const client = new MongoClient(Bun.env.MONGO_URI); await client.connect(); const db = client.db(); export const auth = betterAuth({ baseURL: "https://app.mydomain.org/api/auth", trustedOrigins: ["http://localhost:3000", "https://mydomain.org"], secret: Bun.env.BETTER_AUTH_SECRET, database: mongodbAdapter(db), emailAndPassword: { enabled: true, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, async sendVerificationEmail({ user, url }) { await sendEmail({ to: user.email, subject: "Confirm your email address", html: `<p>Hello!</p> <p>Click to verify your email address: <a href="${url}">${url}</a></p><p>See you soon!</p>` }); } }, socialProviders: { google: { clientId: Bun.env.GOOGLE_CLIENT_ID, clientSecret: Bun.env.GOOGLE_CLIENT_SECRET, }, }, }); ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-15 16:32:23 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Jun 29, 2025):

Hi @Achuttarsing! I'm Dosu and I’m helping the better-auth team.

The root cause is that better-auth expects the baseURL in your config to exactly match the path where you mount its handler in Express—including any prefixes added by your reverse proxy. If you set baseURL to /api/auth but mount at /auth/*, or vice versa, you'll get NOT_FOUND errors and broken URL generation see discussion see also.

To fix this, update your Express setup to:

app.all("/api/auth/*", toNodeHandler(auth));

and make sure your better-auth config uses:

baseURL: "https://app.mydomain.org/api/auth"

Mount better-auth before any body parsing middleware (like bodyParser or express.json), as having those before can also cause NOT_FOUND errors reference.

Double-check that you use the correct config key: baseURL (case-sensitive), not baseUrl or basePath see here.

With these changes, better-auth should generate correct URLs and route requests properly behind your Apache reverse proxy.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3016444056 --> @dosubot[bot] commented on GitHub (Jun 29, 2025): <!-- Greeting --> Hi @Achuttarsing! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The root cause is that better-auth expects the baseURL in your config to exactly match the path where you mount its handler in Express—including any prefixes added by your reverse proxy. If you set baseURL to `/api/auth` but mount at `/auth/*`, or vice versa, you'll get NOT_FOUND errors and broken URL generation [see discussion](https://github.com/better-auth/better-auth/issues/2975) [see also](https://github.com/better-auth/better-auth/issues/2710). To fix this, update your Express setup to: ```js app.all("/api/auth/*", toNodeHandler(auth)); ``` and make sure your better-auth config uses: ```js baseURL: "https://app.mydomain.org/api/auth" ``` Mount better-auth before any body parsing middleware (like bodyParser or express.json), as having those before can also cause NOT_FOUND errors [reference](https://github.com/better-auth/better-auth/issues/2975). Double-check that you use the correct config key: `baseURL` (case-sensitive), not `baseUrl` or `basePath` [see here](https://github.com/better-auth/better-auth/issues/2875). With these changes, better-auth should generate correct URLs and route requests properly behind your Apache reverse proxy. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/34b383ae-f9e7-4b56-8ec8-a251612a62a9?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3215)
Author
Owner

@Achuttarsing commented on GitHub (Jun 29, 2025):

Hi @dosu. Your fix doesn't work because the /api part of the url is already managed by apache.

From the client, when you request the https://app.mydomain.org/api/auth, the Apache proxy converts it to http://localhost:7001/auth, then Express redirects it to better-auth (app.all("/auth/*", toNodeHandler(auth)); line).

The right configuration I mention in my previous message should work..

<!-- gh-comment-id:3016460343 --> @Achuttarsing commented on GitHub (Jun 29, 2025): Hi @dosu. Your fix doesn't work because the `/api` part of the url is already managed by apache. From the client, when you request the `https://app.mydomain.org/api/auth`, the Apache proxy converts it to `http://localhost:7001/auth`, then Express redirects it to better-auth (`app.all("/auth/*", toNodeHandler(auth));` line). The _right configuration_ I mention in my previous message should work..
Author
Owner

@dosubot[bot] commented on GitHub (Jun 29, 2025):

This is a limitation of how better-auth matches routes: it uses the baseURL to determine its routing basePath, and expects the path seen by Express to match exactly—including any prefixes. When Apache strips /api and proxies to /auth, Express only sees /auth/, so mounting at /auth/ with baseURL: https://app.mydomain.org/api/auth creates a mismatch. This causes NOT_FOUND errors because better-auth is looking for /api/auth/* but only receives /auth/* from Express see docs and issues see also.

There’s no built-in support in better-auth for automatic path adjustment or base path stripping based on proxy rewrites. To make this work, you have three options:

  • Change your Express mount to app.all("/api/auth/*", ...) so the path matches the baseURL (but this only works if Apache does not strip /api).
  • Adjust your Apache config to proxy /api/auth/* to /api/auth/* on the backend, so Express sees the full path.
  • Use a small Express middleware before better-auth to rewrite req.url, adding /api in front of the path before it reaches better-auth.

The key is: whatever path you set in baseURL, Express must see that same path when handling requests for better-auth to work correctly reference.

Let me know if you want an example of the middleware workaround!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

<!-- gh-comment-id:3016469260 --> @dosubot[bot] commented on GitHub (Jun 29, 2025): <!-- Answer --> This is a limitation of how better-auth matches routes: it uses the baseURL to determine its routing basePath, and expects the path seen by Express to match exactly—including any prefixes. When Apache strips /api and proxies to /auth, Express only sees /auth/*, so mounting at /auth/* with baseURL: https://app.mydomain.org/api/auth creates a mismatch. This causes NOT_FOUND errors because better-auth is looking for /api/auth/* but only receives /auth/* from Express [see docs and issues](https://github.com/better-auth/better-auth/issues/2710) [see also](https://github.com/better-auth/better-auth/issues/2975). There’s no built-in support in better-auth for automatic path adjustment or base path stripping based on proxy rewrites. To make this work, you have three options: - Change your Express mount to app.all("/api/auth/*", ...) so the path matches the baseURL (but this only works if Apache does not strip /api). - Adjust your Apache config to proxy /api/auth/* to /api/auth/* on the backend, so Express sees the full path. - Use a small Express middleware before better-auth to rewrite req.url, adding /api in front of the path before it reaches better-auth. The key is: whatever path you set in baseURL, Express must see that same path when handling requests for better-auth to work correctly [reference](https://github.com/better-auth/better-auth/pull/2844). Let me know if you want an example of the middleware workaround! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ae4552de-3b1a-4da6-a017-74fbef2933f9?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/3215)
Author
Owner

@Achuttarsing commented on GitHub (Jun 29, 2025):

Thank you, I resolved it modifying my apache config with

# Reverse-proxy to Express
    ProxyPreserveHost On
    ProxyPass        "/api/" "http://localhost:7001/api/"
    ProxyPassReverse "/api/" "http://localhost:7001/api/"

I updated express with app.all("/api/auth/*", toNodeHandler(auth));

And used an express router to add "/api" before all the already existing routes

<!-- gh-comment-id:3016502149 --> @Achuttarsing commented on GitHub (Jun 29, 2025): Thank you, I resolved it modifying my apache config with ``` # Reverse-proxy to Express ProxyPreserveHost On ProxyPass "/api/" "http://localhost:7001/api/" ProxyPassReverse "/api/" "http://localhost:7001/api/" ``` I updated express with `app.all("/api/auth/*", toNodeHandler(auth));` And used an express router to add "/api" before all the already existing routes
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18154