#betterAuth Integration with Convex
36 messages · Page 1 of 1 (latest)
In a Convex and Better Auth integration, you generally do not call auth.api.getSession directly in your server components or server actions. Instead, because Convex acts as your backend and manages the database adapter, these server methods should be executed within Convex functions [Using Better Auth from the server; Usage].
Here is how you achieve session checking and user retrieval depending on your framework:
1. Within Convex Functions (Queries/Mutations)
To check the current user inside a Convex function, use the authComponent provided by the @convex-dev/better-auth package. This method validates the session via cookies automatically.
import { query } from "./_generated/server";
import { authComponent } from "./auth";
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
// This helper retrieves the authenticated user based on the session
return await authComponent.getAuthUser(ctx);
},
});
[Create a Better Auth instance; Authentication state in Convex functions]
2. In Next.js Server Components (SSR)
If you need to check the user in a Next.js Server Component to handle redirects or initial data, use the fetchAuthQuery or preloadAuthQuery utilities. These utilities automatically forward the necessary session cookies from the request headers to Convex.
// app/page.tsx
import { fetchAuthQuery } from "@/lib/auth-server";
import { api } from "@/convex/_generated/api";
export default async function Page() {
// Achieving the equivalent of await auth.api.getSession()
const user = await fetchAuthQuery(api.auth.getCurrentUser, {});
if (!user) {
// Handle unauthenticated state (e.g., redirect)
}
return <div>Welcome, {user.name}</div>;
}
[[SSR with server components](<https://labs.convex.dev/better-auth/framework-guides/next#usage>)]
### 3. Using Other `auth.api` Methods
If you need to call specific Better Auth API methods (like `changePassword` or other management APIs), you must do so inside a Convex mutation using `authComponent.getAuth(createAuth, ctx)`. This provides the `auth` object and the correct `headers` parsed from the Convex request.
```typescript
import { mutation } from "./_generated/server";
import { createAuth, authComponent } from "./auth";
export const someServerMethod = mutation({
args: {},
handler: async (ctx) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
// Use the Better Auth API directly
const session = await auth.api.getSession({ headers });
return session;
},
});
Why this change?
Better Auth methods require direct database access. Since Convex manages the database and runs in a separate environment, calling auth.api from your frontend server (like Next.js or SvelteKit) would require multiple network round-trips for every database interaction. Running these calls inside Convex functions ensures they are efficient and have direct access to your data [Answers].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Let me check the knowledge base for you again. One moment! :books:
The Unauthenticated error occurs because your getCurrentUser query is likely throwing an error when a user is not found, rather than returning null. In Next.js Server Components, when fetchAuthQuery encounters a thrown error from a Convex function, it bubbles up as a ConvexError, which prevents your "if (!user)" check from running.
Here is how to resolve this based on the knowledge sources:
1. Update your Convex Query
Ensure your getCurrentUser function in convex/auth.ts (or convex/users.ts) does not throw when unauthenticated. It should return null so that your Next.js server component can handle the redirect logic.
// convex/auth.ts
import { query } from "./_generated/server";
import { authComponent } from "./auth";
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
try {
return await authComponent.getAuthUser(ctx);
} catch (e) {
// Return null instead of letting the error bubble up
return null;
}
},
});
[Authentication state in Convex functions]
2. Verify Cookie Synchronization
If you just signed in but fetchAuthQuery still fails on the server, it may be because the convex_jwt cookie hasn't been set or synchronized yet. Better Auth and Convex require a specific cookie to be present for SSR to work.
- Ensure the Plugin is used: Your
authClientmust use theconvexClient()plugin to handle cookie management correctly [Create a Better Auth client instance]. - Version Check: Ensure you are using
@convex-dev/better-authversion0.9.2or later, as earlier versions had a known issue where the JWT cookie was not immediately available after sign-in without a manual refresh [No JWT Token when using better-auth with Next.js SSR on login].
3. Handle the error in your Page
In your app/page.tsx, you can wrap the call in a try/catch if you prefer to keep the query throwing errors, though returning null (Step 1) is the standard pattern for "check if user exists" queries.
// app/page.tsx
export default async function Home() {
try {
const user = await fetchAuthQuery(api.auth.getCurrentUser, {});
if (!user) {
// Show signed out view
return <LoggedOutView />;
}
return <LoggedInView user={user} />;
} catch (error) {
// If it's an unauthenticated error, show the logged out view
return <LoggedOutView />;
}
}
4. Client-side state
If you are trying to show different UI on the client, the knowledge sources recommend using the useConvexAuth() hook or the <Authenticated> and <Unauthenticated> components. These components wait for the Convex backend to validate the token, avoiding the race condition where Better Auth thinks you are logged in but Convex hasn't verified it yet [Showing UI based on authentication state].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@grizzled oak is there a server actions for useConvexAuth()? I want only some pages to check if the user is logged in without making them client-rendered
Let me check the knowledge base for you again. One moment! :books:
There is no "server action" version of useConvexAuth(), as hooks are strictly for client-side React. However, you can achieve the exact same result in Server Components by using the server-side utilities provided by the Convex Better Auth integration.
To check if a user is logged in on specific pages without converting them to client components, use the isAuthenticated helper or fetchAuthQuery.
Option 1: Using isAuthenticated
If you only need to check if the user is logged in (a boolean check) to handle redirects or conditional rendering on the server:
// app/protected-page/page.tsx
import { isAuthenticated } from "@/lib/auth-server";
import { redirect } from "next/navigation";
export default async function Page() {
const loggedIn = await isAuthenticated();
if (!loggedIn) {
redirect("/login");
}
return <div>This is a server-rendered protected page.</div>;
}
[Configure Next.js server utilities; isAuthenticatedNextjs()]
Option 2: Using fetchAuthQuery
If you need the actual user data (like their name or email) while rendering the server component:
// app/profile/page.tsx
import { fetchAuthQuery } from "@/lib/auth-server";
import { api } from "@/convex/_generated/api";
export default async function ProfilePage() {
// This performs the check and fetches data in one server-side call
const user = await fetchAuthQuery(api.auth.getCurrentUser, {});
if (!user) {
return <div>Please sign in to view this page.</div>;
}
return <div>Welcome, {user.name}</div>;
}
Key Differences from useConvexAuth()
- No Loading State: Unlike the client hook, these server utilities do not return an
isLoadingproperty. The server simply pauses execution (waits) until the authentication check is complete before sending the HTML to the browser [Benefits of SSR auth]. - Security: Both
isAuthenticatedandfetchAuthQueryautomatically look for the session cookies in the incoming request headers, ensuring the check is secure and happens before any content is rendered [Calling authenticated mutations and actions].
If you haven't set up lib/auth-server.ts yet, ensure you have configured it with convexBetterAuthNextJs to export these functions [Update framework server utilities].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@grizzled oak
Why is isAuthenticated() always showing false even though I had logged in?
Let me check the knowledge base for you again. One moment! :books:
I'm sorry, but the knowledge sources do not provide a single definitive reason why isAuthenticated() would always return false for your specific setup, but they do point to several common configuration and architectural issues that cause this behavior.
The knowledge sources suggest checking the following potential causes:
1. Missing Backend Sync
If you have recently added or changed your authentication provider configuration, your Convex backend may not be synchronized. You must run npx convex dev or npx convex deploy to sync the auth.config.ts file to your backend deployment [Debugging authentication].
2. Cookie Synchronization Race Condition
In Next.js with Better Auth, isAuthenticated() on the server relies on the convex_jwt cookie. There is a known issue (resolved in version 0.9.2 of @convex-dev/better-auth) where the session cookie is created, but the Convex-specific JWT cookie is not immediately available until a page refresh occurs [No JWT Token when using better-auth with Next.js SSR on login]. Ensure:
- You are using the latest version of
@convex-dev/better-auth[No JWT Token when using better-auth with Next.js SSR on login]. - Your
authClientincludes theconvexClient()plugin, which is responsible for setting these cookies [Create a Better Auth client instance].
3. Middleware vs. Layout Pitfalls
If you are checking isAuthenticated() inside a Next.js layout.tsx, the knowledge sources warn that layouts do not stop nested pages from rendering and may not always reflect the most current authentication state during navigation [isAuthenticatedNextjs()]. It is recommended to use convexAuthNextjsMiddleware to handle the authentication check upfront [Require authentication for certain routes].
4. Client-side Propagation Delay
If you are checking isAuthenticated on the client (via useConvexAuth()), there is a two-step process: the browser receives the token from the provider, and then it must be sent over the WebSocket to Convex [Answers]. Until the Convex backend validates this token, isAuthenticated will remain false and isLoading will be true [Showing UI based on authentication state].
5. Environment Variable Issues
Ensure NEXT_PUBLIC_CONVEX_URL and NEXT_PUBLIC_CONVEX_SITE_URL are correctly set in your .env.local. If the convexSiteUrl is missing or incorrect in your convexBetterAuthNextJs configuration, the server utilities will fail to validate the session correctly [SSR improvements].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Let me check the knowledge base for you again. One moment! :books:
I'm sorry, but the knowledge sources do not explicitly mention specific troubleshooting steps for self-hosted Convex deployments on a VPS, especially regarding authentication. However, based on the provided documents and the discrepancy you've noted between client-side and server-side authentication, the problem likely lies in how your Next.js server handles credentials during SSR compared to the browser.
Based on the knowledge sources, here are the most likely causes for your server functions failing with Unauthenticated while client hooks work:
1. Missing or Misconfigured CONVEX_SITE_URL on the Server
For server-side functions like fetchAuthQuery or isAuthenticated to work, the Convex backend must be able to validate the JWT. In a self-hosted environment, you must ensure that the environment variable CONVEX_SITE_URL (which should match your NEXT_PUBLIC_CONVEX_SITE_URL) is correctly set on your Convex deployment itself.
If the Convex backend does not know its own Site URL, it cannot correctly resolve the .well-known/openid-configuration and jwks.json endpoints required for token validation [Expose the public key; Set environment variables].
2. Cookie Forwarding and JWT Synchronization
When you use useConvexAuth() on the client, the browser handles the WebSocket connection and token passing automatically. On the server, fetchAuthQuery relies on the convex_jwt cookie.
- Version Compatibility: Ensure you are using
@convex-dev/better-authversion0.9.2or later. Earlier versions had issues where the JWT cookie was not immediately set or synced for SSR [No JWT Token when using better-auth with Next.js SSR on login]. - Plugin Setup: Verify that your
authClientinsrc/lib/auth-client.tsis using theconvexClient()plugin. Without this, the necessary cookies for server-side authentication will not be sent to your Next.js server [Create a Better Auth client instance].
3. JWKS Resolution in a Private Network
Since your Convex instance is self-hosted on a VPS, the Convex backend must be able to reach its own "Site URL" to fetch the public keys (JWKS).
- If your VPS has restricted outbound networking or uses a self-signed certificate that the backend doesn't trust, the backend will fail to fetch the JWKS and return
Unauthenticated. - Experimental Fix: You can bypass these network calls by using Static JWKS. This involves providing the JWKS directly in your
auth.config.tsso the backend doesn't have to make an HTTP request to itself [Static JWKS].
4. Middleware and Utilities Config
Ensure your lib/auth-server.ts is correctly using the environment variables you defined. It should look like this:
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
export const {
fetchAuthQuery,
isAuthenticated,
// ...
} = convexBetterAuthNextJs({
convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});
If `convexSiteUrl` is misconfigured here, the server utilities will attempt to validate tokens against the wrong issuer [[SSR improvements](<https://labs.convex.dev/better-auth/migrations/migrate-to-0-10#ssr-improvements>)].
### 5. Debugging Identity on Server
To confirm if the backend is receiving anything at all, add a log to your Convex function:
```typescript
console.log("server identity", await ctx.auth.getUserIdentity());
Check your VPS logs. If it shows null, the token is not being passed from your Next.js server to Convex. If it shows an error, the token is being passed but the backend cannot validate it (likely due to the JWKS/Site URL issue mentioned above) [Step 1: Check whether authentication works on the backend].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@grizzled oak I tried const token = await getToken() in my Next.js SSR page but I get undefined even after refreshing the page. It is the same (undefined) on the page that I literally logged in with Convex authClient.signIn.email function
Let me check the knowledge base for you again. One moment! :books:
I'm sorry, but the knowledge sources do not contain enough information to diagnose exactly why getToken() is returning underfined in your specific self-hosted environment, as most documentation assumes the standard Convex Cloud setup. However, based on the provided documents, there are several critical reasons why the server-side token utility might fail even after a successful client-side login:
1. Missing convex_jwt Cookie
The getToken() utility in lib/auth-server.ts does not look for the Better Auth session cookie; it specifically looks for a cookie named convex_jwt [No JWT Token when using better-auth with Next.js SSR on login].
- The Cause: If your
authClient(inlib/auth-client.ts) is missing theconvexClient()plugin, or if you are using a version earlier than0.9.2, the browser will not set the necessary JWT cookie that the server needs to see [Create a Better Auth client instance; No JWT Token when using better-auth with Next.js SSR on login]. - Verification: Open your browser's DevTools, go to the Application tab, and check Cookies. You should see both a
better-auth.session_tokenAND abetter-auth.convex_jwt. If the latter is missing,getToken()will always beundefined.
2. createAuth Requirement for getToken
In the latest versions of the Better Auth integration (0.10+), the getToken utility requires the createAuth configuration to be passed to it. Ensure your lib/auth-server.ts is configured correctly:
import { createAuth } from "@/convex/auth";
import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
export const getToken = () => {
return getTokenNextjs(createAuth);
};
[[Update framework server utilities](<https://labs.convex.dev/better-auth/migrations/migrate-to-0-10#ssr-improvements>)]
### 3. Self-Hosted Site URL Mismatch
Since you are self-hosting, the `getToken` function relies on `NEXT_PUBLIC_CONVEX_SITE_URL` to validate the token's issuer.
* The token issued by your self-hosted Convex instance must have an `iss` (issuer) field that matches exactly what your Next.js server expects [[SSR improvements](<https://labs.convex.dev/better-auth/migrations/migrate-to-0-10#ssr-improvements>)].
* If your `NEXT_PUBLIC_CONVEX_SITE_URL` is `<https://backend-convex.example.com`,> ensure your Convex backend environment variable `SITE_URL` is also set to that exact value [[Set environment variables](<https://labs.convex.dev/better-auth/framework-guides/next#installation>)].
### 4. JWT Validation Failure
If the server retrieves the cookie but fails to validate it (e.g., because it cannot reach the JWKS endpoint on your VPS), `getToken()` may return `undefined` as a fallback.
* **Check Network:** Your Next.js server must be able to make an outbound request to `NEXT_PUBLIC_CONVEX_SITE_URL/.well-known/openid-configuration` [[Expose the public key](<https://stack.convex.dev/nextauth-adapter#issue-the-jwt>)].
* **Static JWKS Workaround:** Because fetching keys over the network can be unreliable in custom VPS setups, you may need to use the **Static JWKS** experimental feature to provide the keys directly to your auth config, bypassing the need for the server to "fetch" a token validation key [[Static JWKS](<https://labs.convex.dev/better-auth/experimental>)].
If the cookie better-auth.convex_jwt is present in your browser but getToken() still returns undefined, the issue is almost certainly a validation mismatch between your Next.js environment variables and your self-hosted Convex deployment's SITE_URL configuration [SSR improvements].
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@inner thistle Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@grizzled oak
The cookies better-auth.session_token and better-auth.convex_jwt are present in my client
Let me check the knowledge base for you again. One moment! :books:
I'm sorry, but the knowledge sources do not provide a specific reason why getToken() or isAuthenticated() would return undefined when the better-auth.convex_jwt cookie is confirmed to be present in the browser. However, based on the provided documents, there are a few architectural reasons why the server might fail to read or validate that existing cookie, especially in a self-hosted environment.
The knowledge sources suggest checking the following potential causes:
1. Version and Implementation Mismatch
If you are using the latest version of the integration, getToken() requires the createAuth function to correctly parse the cookie. Ensure your utility is exported like this:
// lib/auth-server.ts
import { createAuth } from "@/convex/auth";
import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
export const getToken = () => {
return getTokenNextjs(createAuth);
};
Using an older version of the helper or failing to pass createAuth can result in the utility failing to extract the token from the headers [Update framework server utilities].
2. Token Validation Failure during SSR
When the better-auth.convex_jwt cookie is present, getToken() must still validate it against the Convex backend. In a self-hosted VPS setup, this validation can fail if:
- Issuer Mismatch: The
iss(issuer) field in the JWT stored in the cookie does not match theNEXT_PUBLIC_CONVEX_SITE_URLconfigured in your Next.js environment [SSR improvements]. - JWKS Accessibility: The Next.js server must be able to reach your self-hosted Convex Site URL (e.g.,
<https://backend-convex.example.com/.well-known/jwks.json>) to verify the token's signature. If your VPS has firewall restrictions or network issues preventing the Next.js server from reaching that URL, the token validation will fail and returnundefined[Static JWKS].
3. Debugging with JWT.io
To verify if the cookie content matches your server configuration:
- Copy the value of the
better-auth.convex_jwtcookie from your browser. - Paste it into jwt.io.
- Check that the
iss(issuer) field exactly matches yourNEXT_PUBLIC_CONVEX_SITE_URL[Step 3: Check that backend configuration matches frontend configuration].