OAuth
Google, GitHub, and Discord sign-in with server-side state and PKCE.
convex-auth supports Google, GitHub, and Discord OAuth out of the box. These are the initial built-in providers; more will be added over time. The flow uses server-side state and PKCE.
- The client calls
signInWithRedirect({ provider, callbackURL, errorURL }). convex-authreturns a provider authorization URL.- The browser redirects to the provider.
- The provider redirects to
https://<your-convex-site>/api/auth/callback/<provider>. convex-authexchanges the code and redirects the browser tocallbackURLcarrying the session —?token=...&refreshToken=...&sessionId=...plus alandingVerifierbinding the landing to the initiating browser.
Callback URLs
Register this callback URL in each provider’s developer console:
https://<CONVEX_SITE_URL>/api/auth/callback/<provider>
For an example deployment your-deployment:
- GitHub:
https://<your-deployment>.convex.site/api/auth/callback/github - Google:
https://<your-deployment>.convex.site/api/auth/callback/google - Discord:
https://<your-deployment>.convex.site/api/auth/callback/discord
The callback URI comes from AUTH_REDIRECT_URL ?? CONVEX_SITE_URL — one of them must be set (convex dev provides CONVEX_SITE_URL for your deployment automatically).
Get your provider credentials
GitHub
- Go to GitHub Developer settings → OAuth Apps.
- Click New OAuth App.
- Set the Authorization callback URL to
https://<your-site>/api/auth/callback/github. - Copy the Client ID and Client Secret.
- Go to Google Cloud Console → APIs & Services → Credentials.
- Click Create credentials → OAuth client ID.
- Choose Web application.
- Under Authorized redirect URIs, add
https://<your-site>/api/auth/callback/google. - Copy the Client ID and Client Secret.
Discord
- Go to the Discord Developer Portal.
- Create or select an application.
- In OAuth2 → Redirects, add
https://<your-site>/api/auth/callback/discord. - Copy the Client ID and Client Secret.
Set environment variables
Set the credentials on your Convex deployment:
pnpm dlx convex env set GITHUB_CLIENT_ID '...'
pnpm dlx convex env set GITHUB_CLIENT_SECRET '...'
pnpm dlx convex env set GOOGLE_CLIENT_ID '...'
pnpm dlx convex env set GOOGLE_CLIENT_SECRET '...'
pnpm dlx convex env set DISCORD_CLIENT_ID '...'
pnpm dlx convex env set DISCORD_CLIENT_SECRET '...'
Configure convex/auth.ts
import { components } from "./_generated/api";
import { convexAuth } from "@vortex-api/convex-auth/convex";
export const auth = convexAuth({
component: components.convexAuth,
emailAndPassword: { enabled: true },
oauth: {
github: {
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
},
discord: {
clientId: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
},
},
});
Start the OAuth flow from React
Use the signInWithRedirect action from api.auth:
import { useAction } from "convex/react";
function OAuthButtons() {
const start = useAction(api.auth.signInWithRedirect);
const startOAuth = async (provider: "google" | "github" | "discord") => {
const { url } = await start({
provider,
callbackURL: `${window.location.origin}/auth/callback`,
errorURL: `${window.location.origin}/auth/error`,
});
window.location.href = url;
};
return (
<>
<button onClick={() => startOAuth("google")}>Sign in with Google</button>
<button onClick={() => startOAuth("github")}>Sign in with GitHub</button>
<button onClick={() => startOAuth("discord")}>Sign in with Discord</button>
</>
);
}
signInWithRedirect returns a { url } object. Redirect the browser to that URL. From a server, call the action and redirect the browser to the returned URL yourself.
Callback handling
Convex already exposes the callback at /api/auth/callback/:provider through auth.addHttpRoutes(http). The provider redirects there, the token is issued, and the browser is redirected to callbackURL carrying ?token=...&refreshToken=...&sessionId=...&landingVerifier=....
You do not need a callback page. ConvexAuthProvider ingests the session triple from the URL automatically on mount and strips the credentials from the address bar. There is no updateSession({ token }) step — updateSession only takes { refreshToken } for rotation and is called for you.
Two details matter:
- The
landingVerifierparam binds the landing to the browser that started the flow. The provider compares it against a verifier cookie set at initiation; a landing URL missing it (or carrying one bound to a different browser) is rejected and the credentials are stripped. If you initiate OAuth outside the browser — e.g. a server callingsignInWithRedirect— disable the check or the landing will be rejected:requireLandingVerifier={false}onConvexAuthProviderin token mode, orrequireLandingVerifier: falseonconvexAuthNextjsMiddleware/convexAuthRequestMiddlewarein cookie (SSR) mode. - In cookie mode (
storageMode: "cookies") the session never reaches the URL’s JavaScript at all. The SSR middleware intercepts the triple, writes HttpOnly cookies, and redirects with the params stripped — see the SSR contract.
Redirect allowlist (trustedOrigins)
callbackURL, errorURL, and newUserURL are validated before the flow starts: each must resolve to the Convex site origin, SITE_URL, or an origin in trustedOrigins. This is load-bearing — the callback attaches session tokens to that URL, so an unchecked absolute URL would exfiltrate sessions.
Relative URLs like /dashboard always work; they resolve against SITE_URL (falling back to CONVEX_SITE_URL). Absolute URLs require the origin to be trusted. If your app lives on its own origin — app.example.com while Convex serves *.convex.site — add it:
export const auth = convexAuth({
component: components.convexAuth,
oauth: {
trustedOrigins: ["https://app.example.com"],
github: {/* ... */},
},
});
emailAndPassword.trustedOrigins is merged into the same allowlist, so a single entry under either field covers OAuth, magic-link, and email routes.
Server-side OAuth
From a Hono server:
import { Hono } from "hono";
import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
const app = new Hono();
app.post("/auth/oauth/:provider", async (c) => {
const provider = c.req.param("provider");
const { url } = await convex.action(api.auth.signInWithRedirect, {
provider,
callbackURL: `${process.env.APP_URL}/auth/callback`,
});
return c.json({ url });
});