I migrated BetterAuth from 0.7 to 0.10 and now am unable to authenticate. After the upgrade I was getting aMISSING_OR_NULL_ORIGIN error, and after adding baseUrl: siteUrl I am getting Invalid base URL: exp://192.168.68.66:8081. URL must include 'http://' or 'https://'. My trustedOrigins is setill setup with all of the URLs including the exp:// one, so what else could be going wrong.
#BetterAuth MISSING_OR_NULL_ORIGIN or Invalid Base URL
17 messages · Page 1 of 1 (latest)
Hi @regal grotto. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
The knowledge sources don’t mention this exact error string, but they do cover the relevant pieces for Better Auth 0.10 + Expo + Convex. Based on those:
-
baseURLmust be an HTTP(S) Convex URL, not your Expoexp://URLIn 0.10, the Better Auth options expect
baseURL(orbaseUrl) to be an HTTP(S) URL – typically your Convex site URL (ends in.convex.site), not the Expo dev URL. [Expo guide]For Expo, the docs show the client configured like this:
// src/lib/auth-client.ts import { createAuthClient } from "better-auth/react"; import { convexClient } from "@convex-dev/better-auth/client/plugins"; import { expoClient } from '@better-auth/expo/client' import Constants from 'expo-constants' import * as SecureStore from 'expo-secure-store' export const authClient = createAuthClient({ baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL, plugins: [ expoClient({ scheme: Constants.expoConfig?.scheme as string, storagePrefix: Constants.expoConfig?.scheme as string, storage: SecureStore, }), convexClient(), ], });Note that
baseURLisEXPO_PUBLIC_CONVEX_SITE_URL(e.g.<https://adjective-animal-123.convex.site>), notexp://…. [Expo guide]So if you set
baseUrl: siteUrlwheresiteUrlisexp://…, Better Auth will reject it with the “must include http/https” error. -
Where to use
exp://vs HTTP(S)For Expo native:
-
Better Auth server (
convex/auth.ts)
Use HTTP(S) URLs (Convex site URL) for anything likebaseURLorbaseUrl.
For Expo, the docs don’t setbaseURLon the server at all; they only configure plugins andtrustedOrigins. [Expo guide]Example from the Expo guide:
// convex/auth.ts import { createClient, type GenericCtx } from "@convex-dev/better-auth"; import { convex } from "@convex-dev/better-auth/plugins"; import { betterAuth } from "better-auth/minimal"; import { expo } from '@better-auth/expo' import { components } from "./_generated/api"; import { DataModel } from "./_generated/dataModel"; import { query } from "./_generated/server"; import authConfig from "./auth.config"; export const authComponent = createClient<DataModel>(components.betterAuth); export const createAuth = (ctx: GenericCtx<DataModel>) => { return betterAuth({ trustedOrigins: ["your-scheme://"], database: authComponent.adapter(ctx), emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ expo(), convex({ authConfig }), ], }) }Here,
trustedOriginsincludes the scheme ("your-scheme://"), which is where yourexp://-style origin belongs. [Expo guide]- Expo plugin / client
Theexp:/// custom scheme is handled via the Expo plugin (expo()on the server,expoClient()on the client) andtrustedOrigins, not viabaseURL. [Expo guide]
- Expo plugin / client
- Why you saw
MISSING_OR_NULL_ORIGIN
The docs don’t spell out that error, but in 0.10 the configuration became stricter and more explicit about URLs and origins. [Migration 0.10] If you were missing:
- a proper
baseURLon the client (Convex site URL), or - correct
trustedOriginsincluding your scheme,
Better Auth could not validate the origin and would complain.
The fix pattern in the docs is:
- Client:
baseURL = EXPO_PUBLIC_CONVEX_SITE_URL(HTTPS Convex site URL). [Expo guide] - Server:
trustedOrigins: ["your-scheme://"](and, for Expo Web, alsositeUrl+crossDomainplugin). [Expo Web support]
-
What to change in your setup
Based on the docs:
-
Remove
baseUrl: siteUrlifsiteUrlisexp://…from your Better Auth server config. -
Ensure your client uses the Convex site URL:
// .env for Expo EXPO_PUBLIC_CONVEX_SITE_URL=<https://your-project.convex.site>and in the client:
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL, -
Keep
trustedOriginsincluding your scheme (e.g."your-scheme://"or whatever you use for deep linking). [Expo guide] -
If you’re also using Expo Web, follow the Expo Web section: set
SITE_URLto your web URL, addcrossDomain({ siteUrl })andcrossDomainClient()for web only. [Expo Web support]
-
If you can share your current convex/auth.ts and client auth-client config, you can compare them line‑by‑line against the Expo guide linked above; your goal is to have:
- Only HTTP(S) URLs in
baseURL/baseUrl. - Your
exp:/// custom scheme only intrustedOriginsand the Expo plugin configuration.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@regal grotto Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@glad umbra even when setting it to the .convex.site value directly, I am still logging out an error like this: LOG Sign in response: {"data": null, "error": {"code": "MISSING_OR_NULL_ORIGIN", "message": "Missing or null Origin", "status": 403, "statusText": ""}}
```
export const createAuth = (ctx: GenericCtx<DataModel>) => {
return betterAuth({
baseURL: 'https://lovable-fox-123.convex.site',
session: {
expiresIn: 60 * 60 * 24 * 365, // 1 year
updateAge: 60 * 60 * 24 * 30, // 30 days
},
trustedOrigins: [
'my.app://',
'https://lovable-fox-123.convex.cloud',
'exp://192.168.68.66:8081',
'http://localhost:8081',
'exp+my-app://expo-development-client/?url=http%3A%2F%2F192.168.68.66%3A8081',
'https://appleid.apple.com',
],
database: authComponent.adapter(ctx),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
socialProviders: {
apple: {
appBundleIdentifier: appleClientId,
clientId: appleClientId,
clientSecret: appleClientSecret,
},
google: {
clientId: googleClientId,
clientSecret: googleClientSecret,
},
},
plugins: [
convex({
authConfig,
jwksRotateOnTokenGenerationError: true,
}),
expo(),
emailOTP({
async sendVerificationOTP({ email, otp, type }) {
if (type === 'sign-in') {
await sendOtpSignIn(requireMutationCtx(ctx), {
to: email,
code: otp,
})
}
// TODO: support others for 2FA
},
}),
],
})
}
I'm not really sure why this error would still occur. I am running `bunx convex dev` as well and waiting for the changes to apply
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t mention MISSING_OR_NULL_ORIGIN explicitly, but they do show the intended Expo + Better Auth 0.10 setup. Comparing your config to those, there are two likely issues:
-
Server config: don’t set
baseURLto the Convex site URL for ExpoIn the official Expo guide, the Convex-side Better Auth instance does not set
baseURLat all. It only setstrustedOrigins,database, and plugins. [Expo guide]// convex/auth.ts (from docs) export const createAuth = (ctx: GenericCtx<DataModel>) => { return betterAuth({ trustedOrigins: ["your-scheme://"], database: authComponent.adapter(ctx), emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [ expo(), convex({ authConfig }), ], }) }Your config:
return betterAuth({ baseURL: '<https://lovable-fox-123.convex.site',> // <-- extra trustedOrigins: [ ... ], ... })For Expo, the client is what should use the Convex site URL as
baseURL, not the server instance. The server-sidebaseURLis not required in the Expo guide and may be interacting badly with origin checks.I’d try removing
baseURLentirely fromcreateAuthand matching the docs more closely. -
Client config: ensure
baseURLis the.convex.siteURL and CORS is enabledThe Expo guide shows the client like this: [Expo guide; Expo Web support]
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
import { expoClient } from "@better-auth/expo/client";
import Constants from "expo-constants";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL, // <https://...convex.site>
plugins: [
expoClient({
scheme: Constants.expoConfig?.scheme as string,
storagePrefix: Constants.expoConfig?.scheme as string,
storage: SecureStore,
}),
convexClient(),
],
});
And the Convex HTTP routes:
// convex/http.ts
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";
const http = httpRouter();
// CORS handling is required for client side frameworks
authComponent.registerRoutes(http, createAuth, { cors: true });
export default http;
Double‑check:
EXPO_PUBLIC_CONVEX_SITE_URLis set to<https://lovable-fox-123.convex.site> in your.envused by Expo. [Expo guide]- Your
authClientuses that env var asbaseURL. authComponent.registerRoutes(http, createAuth, { cors: true })is present. [Expo Web support]
-
Trusted origins: keep only actual origins
In the Expo guide,
trustedOriginsis just the scheme (and, for web, the site URL): [Expo guide; Expo Web support]const siteUrl = process.env.SITE_URL!;
export const createAuthOptions = (ctx: GenericCtx<DataModel>) =>
({
trustedOrigins: [siteUrl, "your-scheme://"],
...
}) satisfies BetterAuthOptions;
Your list includes full URLs like `exp://192.168.68.66:8081` and `exp+my-app://expo-development-client/?url=...`. The docs don’t show using those; they just use the **scheme** (`"your-scheme://"`). I can’t say for sure from the docs that this is invalid, but to align with them I’d simplify to:
```ts
trustedOrigins: [
'my.app://',
'<https://lovable-fox-123.convex.site',> // if you also use Expo Web
],
and drop the exp://... and <http://localhost:8081> entries.
Concrete changes to try, based on docs:
-
In
convex/auth.ts:- Remove
baseURLfrombetterAuth({ ... }). - Reduce
trustedOriginsto:
trustedOrigins: ['my.app://'], // or ['my.app://', process.env.SITE_URL!] if you also support Expo Web- Keep
expo()andconvex({ authConfig, jwksRotateOnTokenGenerationError: true })plugins.
- Remove
-
In
convex/http.ts:authComponent.registerRoutes(http, createAuth, { cors: true }); -
In your Expo client:
-
Ensure:
EXPO_PUBLIC_CONVEX_SITE_URL=<https://lovable-fox-123.convex.site> -
And:
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL,
-
If, after matching the Expo guide exactly (minus your extra OTP/social config), you still see MISSING_OR_NULL_ORIGIN, then the knowledge sources don’t give more detail on that specific error, and you may need to share the exact client request (headers, URL) in Discord’s #better-auth channel for deeper debugging.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@regal grotto Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@glad umbra My code seems to follow the betterauth expo guide, the only issue I am seeing is a typescript error in my auth-client.ts
```
const _authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE!,
plugins: [
convexClient(),
expoClient({
scheme: 'my.app',
storagePrefix: 'my.app',
storage: SecureStore,
}),
emailOTPClient(),
],
})
The typescript error seems to be around the `expoClient()` plugin. The error is attached.
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t show the exact TypeScript error you’re seeing, but they do show the expected shape of auth-client.ts for Expo + Convex + Better Auth, which you can compare against.
From the Expo guide, the recommended client looks like this (note the order and set of plugins): [Expo guide]
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { convexClient } from "@convex-dev/better-auth/client/plugins";
import { expoClient } from "@better-auth/expo/client";
import Constants from "expo-constants";
import * as SecureStore from "expo-secure-store";
export const authClient = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL,
plugins: [
expoClient({
scheme: Constants.expoConfig?.scheme as string,
storagePrefix: Constants.expoConfig?.scheme as string,
storage: SecureStore,
}),
convexClient(),
],
});
A few things to double‑check against your code:
-
Import path and type of
expoClientIt should come from
@better-auth/expo/client:import { expoClient } from "@better-auth/expo/client";If it’s imported from anywhere else, TypeScript may not see it as a valid Better Auth client plugin.
-
Environment variable name
The docs use
EXPO_PUBLIC_CONVEX_SITE_URL, notEXPO_PUBLIC_CONVEX_SITE:baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL,While this is a runtime concern, mismatched env names can sometimes surface as type issues if you’ve added stricter typings around
process.env. -
Plugin list shape
The docs only show expoClient() and convexClient() in the Expo client. [Expo guide]
You have:
plugins: [
convexClient(),
expoClient({ ... }),
emailOTPClient(),
],
The knowledge sources don’t mention emailOTPClient() at all, so I can’t confirm its correct type. If it’s not typed as a valid Better Auth client plugin, it could cause a type error that appears “around” expoClient. To align with the docs, try temporarily removing emailOTPClient() and see if the error disappears.
-
Order of plugins
The guide shows
expoClient()first, thenconvexClient(). [Expo guide]While order shouldn’t normally cause a type error, you can match the docs exactly:
const _authClient = createAuthClient({ baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL, plugins: [ expoClient({ scheme: "my.app", storagePrefix: "my.app", storage: SecureStore, }), convexClient(), ], });
If, after:
- importing
expoClientfrom@better-auth/expo/client, - using
EXPO_PUBLIC_CONVEX_SITE_URL, - and removing
emailOTPClient(),