Convex-Gate: Better Auth Adapter for Convex
Convex-Gate is an alpha-stage package designed to integrate the Better Auth authentication system with Convex applications. It provides an isolated authentication data store within a Convex component, separating auth concerns from application data. Key features include a dedicated hot-path for rapid session resolution, server-side JWT caching with configurable TTL, client-side token expiry awareness, and support for static JWKS. It also offers React bindings via `ConvexBetterAuthProvider` and is compatible with all Better Auth plugins (2FA, SSO, organizations, passkeys). The current version is 0.1.3, and due to its alpha status, APIs are subject to change. Release cadence is currently irregular as it's under active development.
Common errors
-
Error: Auth config provider must be passed via `convex({ authConfig })`cause The Convex plugin for Better Auth was not provided with the `authConfig` object.fixEnsure your `createAuthOptions` function correctly includes `convex({ authConfig }) as any,` within the plugins array. -
TypeError: Cannot read properties of undefined (reading 'betterAuth')
cause The `betterAuth` component was not properly defined or imported into the `_generated/api.ts` file, or the component registration in `convex.config.ts` is missing.fixVerify that `convex/convex.config.ts` includes `app.use(betterAuth);` and that `convex/convex.config.ts` is correctly imported and run. -
HTTP 401 Unauthorized during client-side authentication attempts.
cause CORS settings might be misconfigured on the Convex HTTP routes, or the `trustedOrigins` in `createAuthOptions` does not match the client's URL.fixCheck that `authComponent.registerRoutes(http, createAuth, { cors: true });` is present and that `trustedOrigins: [siteUrl]` includes all necessary client origins, especially in development (`http://localhost:XXXX`).
Warnings
- breaking This package is currently in alpha. APIs are subject to change between releases, and it is not yet recommended for production use.
- gotcha Social OAuth logins were broken by the Better Auth Proxy in v0.1.1. This was fixed in v0.1.2.
- gotcha Security hardening was introduced in v0.1.3, addressing CORS, CSRF, cache TTL, and debug lockdown settings.
- breaking The `authComponent` adapter requires `components.betterAuth` from the Convex generated API. Ensure the Convex component is correctly registered and named `betterAuth`.
Install
-
npm install convex-gate -
yarn add convex-gate -
pnpm add convex-gate
Imports
- betterAuth
import { betterAuth } from 'convex-gate/convex.config';import betterAuth from 'convex-gate/convex.config';
- getAuthConfigProvider
import getAuthConfigProvider from 'convex-gate/auth-config';
import { getAuthConfigProvider } from 'convex-gate/auth-config'; - createClient
const createClient = require('convex-gate').createClient;import { createClient } from 'convex-gate'; - ConvexBetterAuthProvider
import ConvexBetterAuthProvider from 'convex-gate/react';
import { ConvexBetterAuthProvider } from 'convex-gate/react';
Quickstart
import { ConvexReactClient } from "convex/react";
import { ConvexBetterAuthProvider } from "convex-gate/react";
import { authClient } from "./lib/auth-client";
import { createAuthClient } from "better-auth/react";
import { convexClient, crossDomainClient } from "convex-gate/client/plugins";
import React from 'react';
import ReactDOM from 'react-dom/client';
// --- Client Auth Setup (src/lib/auth-client.ts) ---
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_CONVEX_SITE_URL ?? '', // Use empty string if undefined for SSR compatibility
plugins: [
crossDomainClient(),
convexClient(),
],
});
// --- Main App Component (src/App.tsx) ---
import { Authenticated, Unauthenticated } from "convex/react";
function App() {
return (
<>
<h1>Convex-Gate Demo</h1>
<Authenticated>
<p>You are authenticated!</p>
<button onClick={() => authClient.signOut()}>Sign Out</button>
</Authenticated>
<Unauthenticated>
<p>Please sign in.</p>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get('email')?.toString();
const password = formData.get('password')?.toString();
if (email && password) {
authClient.signIn.email({ email, password });
}
}}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
</Unauthenticated>
</>
);
}
// --- Root Render (src/main.tsx) ---
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL ?? '');
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<App />
</ConvexBetterAuthProvider>
</React.StrictMode>
);