Better Auth error
Invalid origin
"Invalid origin" (HTTP 403, code INVALID_ORIGIN) means Better Auth received a sign-in, sign-up or other POST whose Origin header is not one it trusts. By default it trusts only its baseURL, which BETTER_AUTH_URL usually sets to the production domain, so the same app opened on a Vercel preview URL fails. Add the preview origins to trustedOrigins with a pattern scoped to your project and team, such as https://my-app-*-my-team.vercel.app, or use the dynamic baseURL with allowedHosts that Better Auth documents for preview deployments. Avoid https://*.vercel.app: it matches every Vercel deployment, including other people's. Reproduced with better-auth 1.7.13 on 2026-10-26.

For the other problems a preview deployment hits, see the Vercel deploy questions by symptom.
What the server logs
The check is Better Auth's origin-check middleware, which runs on POST requests. It logs the origin it refused, then a hint with the list it compared against, and throws the 403. In the same run, a request with Origin: null got "Missing or null Origin" (MISSING_OR_NULL_ORIGIN), and a cross-site form navigation got "Cross-site navigation login blocked. This request appears to be a CSRF attack." A callbackURL on an untrusted domain gets "Invalid callbackURL".
ERROR [Better Auth]: Invalid origin: https://my-app-git-feature-x-team.vercel.app
INFO If it's a valid URL, please add https://my-app-git-feature-x-team.vercel.app to trustedOrigins in your auth config
{"message":"Invalid origin","code":"INVALID_ORIGIN"}Trust your preview URLs, and only yours
Vercel names branch previews <project-name>-git-<branch-name>-<scope-slug>.vercel.app and commit previews <project-name>-<unique-hash>-<scope-slug>.vercel.app. In a trustedOrigins pattern, * matches any characters except a slash, so one entry covers both and stays inside your team. Better Auth also reads a comma-separated BETTER_AUTH_TRUSTED_ORIGINS variable.
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL, // https://my-app.com
trustedOrigins: ['https://my-app.com', 'https://my-app-*-my-team.vercel.app'],
})The dynamic baseURL for previews
Better Auth's options reference documents a baseURL object for apps reached through several domains, such as preview deployments: it takes the host from the request, accepts it only if it matches allowedHosts, and adds those hosts to trustedOrigins. Its example lists "*.vercel.app"; scope that pattern to your project as above for the same reason. A fallback URL is used when the host does not match, and the docs warn it can hide a misconfiguration.
export const auth = betterAuth({
baseURL: {
allowedHosts: ['my-app.com', 'my-app-*-my-team.vercel.app'],
protocol: 'https',
},
})Checklist
How to fix it, then check the fix
- 01Copy the refused origin from the "Invalid origin:" line in the function logs.
- 02Add your production domain and a preview pattern scoped to your project and team to trustedOrigins, or switch to baseURL with allowedHosts.
- 03Keep BETTER_AUTH_URL on the production domain for Production, and check which value Preview gets.
- 04If the client calls the API on another domain, add that client origin too.
- 05Redeploy, sign in on a preview URL, and check the request returns 200, not 403.
Related
Related guides
Sources
Official docs behind this checklist
Browse all launch guides · Read resources · How to make and check your next change
FAQ
FAQ for builders before launch
Can I turn the origin check off?
There is an advanced disableOriginCheck option, and Better Auth's source warns that it currently disables CSRF checks too. Trusting the right origins keeps both protections.
Why does it work on localhost?
BETTER_AUTH_URL in .env.local is http://localhost:3000, so the browser origin matches the baseURL. On a preview, the origin is the preview URL and the baseURL is still production.