Skip to content

Instantly share code, notes, and snippets.

@teyfix
Last active December 21, 2025 03:14
Show Gist options
  • Select an option

  • Save teyfix/1b0144d1c8b1c5d8e84174e90043346b to your computer and use it in GitHub Desktop.

Select an option

Save teyfix/1b0144d1c8b1c5d8e84174e90043346b to your computer and use it in GitHub Desktop.
Better Auth with dynamic baseURL workaround
import { serverConfig } from "@/config/server.config";
import { db } from "@/db/drizzle"; // your drizzle instance
import * as schema from "@/db/schemas/auth.schema";
import { betterAuth, type BetterAuthOptions } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { admin } from "better-auth/plugins";
import { v7 } from "uuid";
/**
* Type-safe wrapper for defining Better Auth configuration objects.
* Provides type inference and validation without runtime overhead.
*
* @template Options - The Better Auth options type
* @param options - Configuration options for Better Auth
* @returns The same options object with proper typing
*/
const createConfig = <Options extends BetterAuthOptions>(
options: Options,
): Options => options;
/**
* Default base URL for Better Auth endpoints.
* Uses the first trusted origin from the server configuration.
*/
const [defaultBaseURL] = serverConfig.BETTER_AUTH_TRUSTED_ORIGINS;
/**
* Base configuration for Better Auth.
* Contains all shared settings that will be used across all auth instances.
*
* Configuration includes:
* - UUID v7 ID generation for better database performance
* - Secure cookies for production environments
* - PostgreSQL adapter with Drizzle ORM
* - Google OAuth provider
* - Admin plugin for user management
* - Next.js cookie integration
*/
const baseAuthConfig = createConfig({
advanced: {
database: {
generateId: () => v7(),
},
useSecureCookies: true,
},
baseURL: defaultBaseURL,
basePath: "/api/auth",
database: drizzleAdapter(db, {
provider: "pg",
schema,
transaction: false,
}),
emailAndPassword: {
enabled: false,
},
plugins: [
admin({
adminUserIds: serverConfig.BETTER_AUTH_ADMIN_USER_IDS,
}),
nextCookies(),
],
secret: serverConfig.BETTER_AUTH_SECRET,
socialProviders: {
google: {
clientId: serverConfig.GOOGLE_CLIENT_ID,
clientSecret: serverConfig.GOOGLE_CLIENT_SECRET,
},
},
trustedOrigins: serverConfig.BETTER_AUTH_TRUSTED_ORIGINS,
});
/**
* Type representing the base auth configuration.
* Used for type inference when extending configuration.
*/
type BaseAuthConfig = typeof baseAuthConfig;
/**
* Extends the base auth configuration with additional options.
* Allows overriding base configuration on a per-instance basis.
*
* @template Options - Additional options to merge with base config
* @param options - Custom configuration options to extend base config
* @returns Merged configuration combining base and custom options
*
* @example
* const customAuth = extendAuthConfig({
* baseURL: "https://custom-domain.com",
* });
*/
const extendConfig = <Options extends BetterAuthOptions>(
options: Options,
): BaseAuthConfig & Options => ({ ...baseAuthConfig, ...options });
/**
* Creates a Better Auth instance with extended configuration.
* Allows overriding base configuration on a per-instance basis.
*
* @param options - Additional options to merge with base configuration
* @returns Configured Better Auth instance
*
* @example
* // Create auth instance with custom baseURL
* const auth = createAuth({
* baseURL: "https://example.com"
* });
*
* @example
* // Create auth instance with default configuration
* const auth = createAuth({});
*/
export const createAuth = <Options extends BetterAuthOptions>(
options: Options,
) => betterAuth(extendConfig(options));
/**
* Default Better Auth instance using base configuration.
* This is the primary auth instance used when no custom configuration is needed.
*
* For dynamic baseURL support (e.g., behind reverse proxies), create new instances
* using `createAuth({ baseURL })` instead of using this default instance.
*/
export const auth = createAuth({});
/**
* Extracts the origin from proxy forwarded headers.
* Used when the application is behind a reverse proxy (e.g., Traefik, nginx, Cloudflare).
*
* Constructs the origin from the following headers:
* - `x-forwarded-proto`: The protocol (http/https)
* - `x-forwarded-host`: The hostname
* - `x-forwarded-port`: The port (defaults to 443 for https, 80 for http)
*
* @param request - The incoming request
* @returns The reconstructed origin, or null if required headers are missing or invalid
*
* @example
* // Request with forwarded headers
* fromProxyHeader(request) // => "https://example.com"
*
* @example
* // Request missing forwarded headers
* fromProxyHeader(request) // => null
*/
const fromProxyHeader = (request: Request): string | null => {
const headers = ["x-forwarded-proto", "x-forwarded-host", "x-forwarded-port"];
let [proto, host, port] = headers.map((header) =>
request.headers.get(header),
);
// Both protocol and host are required
if (proto == null || host == null) {
return null;
}
// Use standard ports if not explicitly set
if (port == null) {
port = proto === "https" ? "443" : "80";
}
try {
return new URL(`${proto}://${host}:${port}`).origin;
} catch {
// Invalid URL construction (malformed headers)
return null;
}
};
/**
* Extracts the origin from the `Origin` request header.
* The Origin header is automatically sent by browsers in cross-origin requests (CORS).
*
* @param request - The incoming request
* @returns The origin from the header, or null if not present
*
* @example
* // CORS request from a browser
* fromOriginHeader(request) // => "http://localhost:3000"
*
* @example
* // Server-to-server request (no Origin header)
* fromOriginHeader(request) // => null
*/
const fromOriginHeader = (request: Request): string | null =>
request.headers.get("origin");
/**
* Extracts the origin from the request URL itself.
* Used as a fallback when no proxy headers or Origin header are present.
*
* @param request - The incoming request
* @returns The origin from the request URL, or null if URL is malformed
*
* @example
* // Direct request to the server
* fromURL(request) // => "http://localhost:3000"
*
* @example
* // Malformed request URL
* fromURL(request) // => null
*/
const fromURL = (request: Request): string | null => {
try {
return new URL(request.url).origin;
} catch {
// Malformed request URL
return null;
}
};
/**
* Extracts the origin from a request using multiple fallback strategies.
*
* Attempts to determine the origin in the following priority order:
* 1. Proxy forwarded headers (x-forwarded-proto, x-forwarded-host, x-forwarded-port)
* 2. Origin request header (from CORS requests)
* 3. Request URL origin (direct requests)
*
* This approach ensures the correct origin is extracted regardless of whether the
* application is behind a reverse proxy, receiving CORS requests, or handling direct requests.
*
* @param request - The incoming request
* @returns The extracted origin, or null if none of the strategies succeed
*
* @example
* // Behind reverse proxy with forwarded headers
* extractOrigin(request) // => "https://example.com"
*
* @example
* // Browser CORS request without proxy
* extractOrigin(request) // => "http://localhost:3000"
*
* @example
* // Direct local request
* extractOrigin(request) // => "http://127.0.0.1:3000"
*/
export const extractOrigin = (request: Request): string | null =>
fromProxyHeader(request) || fromOriginHeader(request) || fromURL(request);
import { serverConfig } from "@/config/server.config";
import { createAuth } from "@/lib/auth";
import { extractOrigin } from "@/lib/http/extract-origin";
import { toNextJsHandler } from "better-auth/next-js";
type HttpMethod = "GET" | "POST";
type RequestHandler = (request: Request) => Response | Promise<Response>;
/**
* Cache for Better Auth handlers, keyed by baseURL.
* Each origin gets its own auth instance to support dynamic baseURL configuration.
*/
const handlerCache = new Map<string, ReturnType<typeof toNextJsHandler>>();
/**
* List of allowed origins for Better Auth requests.
* Requests from origins not in this list will be rejected.
*
* @example
* - "http://localhost:3000"
* - "https://example.com"
* - "https://example.com:4000"
*/
const trustedOrigins = serverConfig.BETTER_AUTH_TRUSTED_ORIGINS;
/**
* Handles Better Auth requests with dynamic baseURL support.
*
* Creates and caches a separate auth handler for each allowed origin,
* enabling the same auth instance to serve multiple domains correctly.
*
* @param method - The HTTP method (GET or POST)
* @param request - The incoming Next.js request
* @returns Response object from Better Auth or a 403 Forbidden error
*
* @throws Will return 403 if the request origin is not in the allowed list
*/
const handleRequest = async <T extends HttpMethod>(
method: T,
request: Request,
): Promise<Response> => {
const origin = extractOrigin(request);
if (!origin || !trustedOrigins.includes(origin)) {
return Response.json(
{ message: "Unknown origin", origin },
{ status: 403, statusText: "Forbidden" },
);
}
let handler = handlerCache.get(origin);
if (handler == null) {
handler = toNextJsHandler(createAuth({ baseURL: origin }));
handlerCache.set(origin, handler);
}
return handler[method](request);
};
/**
* Creates a route handler for the specified HTTP method.
*
* @param method - The HTTP method to handle
* @returns A Next.js route handler function
*/
const createHandler =
<T extends HttpMethod>(method: T): RequestHandler =>
(request: Request): Promise<Response> =>
handleRequest(method, request);
export const GET = createHandler("GET");
export const POST = createHandler("POST");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment