Convex Auth — GitHub & Google OAuth Reference

Introduction
A practical step-by-step reference for setting up Convex Auth with GitHub and Google OAuth. Covers setup, provider config, sign-in UI, account linking, testing locally, and production deployment.
Overview
Convex Auth handles authentication using Auth.js provider configs. OAuth flow works like this:
User clicks a sign-in button in your app
User gets redirected to the provider (GitHub/Google)
User authenticates on the provider's site
Provider redirects back to your Convex backend callback URL
Convex handles the token exchange and creates/links the user
User is redirected back to your app, signed in
All of this works locally because the callback goes through your Convex deployment which is already on a public URL. Your local dev server just receives the final redirect.
Initial Setup
1. Install dependencies
npm install @convex-dev/auth @auth/core@0.37.0
2. Run the init command
npx @convex-dev/auth
This configures your Convex project for auth.
3. Add auth tables to your schema
// convex/schema.ts
import { defineSchema } from "convex/server";
import { authTables } from "@convex-dev/auth/server";
const schema = defineSchema({
...authTables,
// Your other tables...
});
export default schema;
4. Set up the React provider
Replace ConvexProvider with ConvexAuthProvider. This is the only provider you need. It handles both the Convex client and auth.
// src/main.tsx
import { ConvexAuthProvider } from "@convex-dev/auth/react";
import { ConvexReactClient } from "convex/react";
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { routeTree } from "./routeTree.gen";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<ConvexAuthProvider client={convex}>
<RouterProvider router={router} />
</ConvexAuthProvider>
</StrictMode>,
);
Finding Your Callback URL
Every OAuth provider needs a callback URL. For Convex Auth the base is your HTTP Actions URL.
How to find it. Go to your Convex dashboard. Navigate to Settings → URL & Deploy Key → Show development credentials. Your HTTP Actions URL matches your deployment URL but ends in .site instead of .cloud.
Example: if your deployment URL is https://fast-horse-123.convex.cloud then your HTTP Actions URL is https://fast-horse-123.convex.site.
The full callback URLs:
GitHub: https://fast-horse-123.convex.site/api/auth/callback/github
Google: https://fast-horse-123.convex.site/api/auth/callback/google
GitHub OAuth Setup
Step 1. Register a GitHub OAuth app
Go to: https://github.com/settings/developers → OAuth Apps → New OAuth App
Fill in the form:
| Field | Value |
|---|---|
| Application name | Your app name (shown to users) |
| Homepage URL | http://localhost:5173 |
| Authorization callback URL | https://YOUR-DEPLOYMENT.convex.site/api/auth/callback/github |
Click Register application.
Step 2. Set environment variables
From the GitHub app page, copy the Client ID. Then click "Generate a new client secret" and copy that too.
npx convex env set AUTH_GITHUB_ID yourgithubclientid
npx convex env set AUTH_GITHUB_SECRET yourgithubsecret
That is it for GitHub.
Google OAuth Setup
Google has more steps than GitHub.
Step 1. Create or open a Google Cloud project
Go to: https://console.cloud.google.com
Select an existing project or create a new one.
Step 2. Configure Google Auth Platform
Go to the Auth Platform overview. Click GET STARTED.
Enter your App name (shown to users)
Select a User support email
Click NEXT
Choose External audience (for public apps)
Click NEXT
Enter Contact Information email addresses
Click NEXT
Review and agree to the User Data Policy
Click CONTINUE → CREATE
Step 3. Add test users
For external apps in testing mode, Google requires you to add test users.
Go to Audience in the left menu. Add email addresses of users who will test your app. Only these users can sign in while the app is in testing mode.
Step 4. Create an OAuth client
Go to Clients in the left menu. Click Create client.
| Field | Value |
|---|---|
| Application type | Web Application |
| Name | Anything (organizational, not shown to users) |
| Authorized JavaScript origins | http://localhost:5173 |
| Authorized redirect URIs | https://YOUR-DEPLOYMENT.convex.site/api/auth/callback/google |
Click CREATE.
Step 5. Set environment variables
From the client config page, copy the Client ID and Client secret.
npx convex env set AUTH_GOOGLE_ID yourgoogleclientid
npx convex env set AUTH_GOOGLE_SECRET yourgooglesecret
Provider Configuration
Both providers go in the same convex/auth.ts file.
// convex/auth.ts
import GitHub from "@auth/core/providers/github";
import Google from "@auth/core/providers/google";
import { convexAuth } from "@convex-dev/auth/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub, Google],
});
The providers automatically read the environment variables you set. AUTH_GITHUB_ID, AUTH_GITHUB_SECRET for GitHub. AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET for Google. No need to pass them explicitly.
Sign-In UI
Basic sign-in buttons
import { useAuthActions } from "@convex-dev/auth/react";
function LoginPage() {
const { signIn } = useAuthActions();
return (
<div>
<button onClick={() => signIn("github")}>Sign in with GitHub</button>
<button onClick={() => signIn("google")}>Sign in with Google</button>
</div>
);
}
The first argument to signIn is the provider ID. Lowercase version of the provider name. "github" for GitHub. "google" for Google.
Sign out
import { useAuthActions } from "@convex-dev/auth/react";
function UserMenu() {
const { signOut } = useAuthActions();
return <button onClick={() => signOut()}>Sign out</button>;
}
Checking auth state
import { useConvexAuth } from "convex/react";
function SomeComponent() {
const { isAuthenticated, isLoading } = useConvexAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <LoginPage />;
return <div>Welcome back!</div>;
}
Custom redirect after sign-in
You can control where the user lands after OAuth by passing redirectTo:
signIn("github", { redirectTo: "/dashboard" });
Auth Guard with TanStack Router
Use a pathless layout route to protect routes. Check auth in the component.
// src/routes/_authenticated.tsx
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { useConvexAuth } from "convex/react";
export const Route = createFileRoute("/_authenticated")({
component: AuthGuard,
});
function AuthGuard() {
const { isAuthenticated, isLoading } = useConvexAuth();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <LoginPage />;
return <Outlet />;
}
File structure:
routes/
├── __root.tsx ← app shell
├── index.tsx ← / (public)
├── login.tsx ← /login (public)
├── _authenticated.tsx ← auth guard
├── _authenticated.dashboard.tsx ← /dashboard (protected)
├── _authenticated.settings.tsx ← /settings (protected)
Account Linking
You do not need to configure anything for GitHub + Google.
Both are trusted OAuth providers. If a user signs in with GitHub using user@example.com and later with Google using the same email, Convex Auth automatically links both to the same user document.
How it works
Trusted providers (OAuth, magic links, OTPs): verified email → auto-links to existing user with same email
Untrusted providers (passwords without email verification): creates a new user document, no linking
When to worry about it
Only if you mix trusted and untrusted methods. Recommendation from Convex: do not mix them. Stick to all trusted or a single untrusted method to avoid duplicate users.
Custom linking logic (optional)
If you need full control:
// convex/auth.ts
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub, Google],
callbacks: {
async createOrUpdateUser(ctx, args) {
if (args.existingUserId) {
// Returning existing user, optionally update fields
return args.existingUserId;
}
// Custom linking logic
const existingUser = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("email"), args.profile.email))
.first();
if (existingUser) return existingUser._id;
// Create new user
return ctx.db.insert("users", {
name: args.profile.name,
email: args.profile.email,
image: args.profile.image,
});
},
},
});
Writing extra data after sign-up
If defaults are fine but you want to create additional documents:
// convex/auth.ts
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [GitHub, Google],
callbacks: {
async afterUserCreatedOrUpdated(ctx, { userId }) {
// Create a profile, settings doc, etc.
const existing = await ctx.db
.query("profiles")
.filter((q) => q.eq(q.field("userId"), userId))
.first();
if (!existing) {
await ctx.db.insert("profiles", { userId, onboarded: false });
}
},
},
});
Retrieving Extra Profile Info
By default Convex Auth saves name, email, and image from the OAuth profile to the users table. The profile function on a provider lets you pick which fields to extract from the raw OAuth data. Whatever you return gets auto-saved to the users table. You do not insert the user yourself.
// convex/auth.ts
import GitHub from "@auth/core/providers/github";
import Google from "@auth/core/providers/google";
import { convexAuth } from "@convex-dev/auth/server";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
GitHub({
profile(githubProfile, tokens) {
// githubProfile is the raw data GitHub sends back
// Whatever you return here gets saved to the users table
return {
id: githubProfile.id, // required, unique identifier
name: githubProfile.name,
email: githubProfile.email,
image: githubProfile.avatar_url,
githubUsername: githubProfile.login, // extra field
};
},
}),
Google,
],
});
Important: your schema must include any custom fields you return. Convex enforces the schema strictly. If you return a field that is not in the schema it will throw an error. Custom fields should be optional because not every provider will have them. A Google user will not have a githubUsername.
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
const schema = defineSchema({
...authTables,
// Override the users table to add custom fields
users: defineTable({
name: v.optional(v.string()),
email: v.optional(v.string()),
image: v.optional(v.string()),
githubUsername: v.optional(v.string()), // extra field, optional
}),
});
export default schema;
The default fields (name, email, image) are already in authTables. You only need to override the users table when adding fields beyond the defaults.
How profile and callbacks relate
These are two different stages:
profilefunction — runs first. Picks fields from raw OAuth data. Saves touserstable automatically.afterUserCreatedOrUpdatedcallback — runs after. You have theuserId. Write to other tables like profiles or settings.
They can be used together:
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
GitHub({
profile(githubProfile, tokens) {
return {
id: githubProfile.id,
name: githubProfile.name,
email: githubProfile.email,
image: githubProfile.avatar_url,
githubUsername: githubProfile.login,
};
},
}),
Google,
],
callbacks: {
async afterUserCreatedOrUpdated(ctx, { userId }) {
const existing = await ctx.db
.query("profiles")
.filter((q) => q.eq(q.field("userId"), userId))
.first();
if (!existing) {
await ctx.db.insert("profiles", { userId, onboarded: false });
}
},
},
});
Testing Locally
You do not need separate Convex projects for local testing.
The flow works like this:
Your app runs on
localhost:5173User clicks "Sign in with GitHub"
Browser redirects to GitHub
User authenticates on GitHub
GitHub redirects to
https://your-deployment.convex.site/api/auth/callback/githubConvex handles the token exchange
Convex redirects user back to your localhost app
User is signed in
The callback goes through your Convex deployment which is already on a public URL. Your localhost just receives the final redirect. Everything works out of the box.
SITE_URL
Make sure your SITE_URL env variable is set correctly for local development:
npx convex env set SITE_URL http://localhost:5173
This tells Convex where to redirect the user after the OAuth flow completes.
Production Setup
For production you need separate OAuth apps on each provider. You cannot share the same OAuth app between dev and production because the callback URLs are different.
Steps
Create new OAuth apps on GitHub and Google with your production URLs
Set env variables on your production Convex deployment (not dev)
Update callback URLs to use your production Convex deployment URL
Set SITE_URL to your production frontend URL
# On your production Convex deployment
npx convex env set AUTH_GITHUB_ID prodgithubclientid --prod
npx convex env set AUTH_GITHUB_SECRET prodgithubsecret --prod
npx convex env set AUTH_GOOGLE_ID prodgoogleclientid --prod
npx convex env set AUTH_GOOGLE_SECRET prodgooglesecret --prod
npx convex env set SITE_URL https://yourapp.com --prod
Custom domain on consent screen
By default Google shows your Convex deployment name on the consent screen (like fast-horse-123.convex.site). If you have a Convex Pro account with a custom domain, set the CUSTOM_AUTH_SITE_URL env variable to use your domain instead:
npx convex env set CUSTOM_AUTH_SITE_URL https://convex.yourapp.com --prod
Then update the callback URL in your Google OAuth client config to match.
Sessions
How sessions work
Convex Auth issues JWTs for client authentication
The JWT is what
ctx.auth.getUserIdentity()uses on the backendSession documents are stored in the
authSessionstableSessions are created and deleted by Convex Auth automatically
Important to know
If you delete a session server-side, the user stays authenticated until the JWT expires. For security-critical operations, load the actual session document and check it directly rather than relying on the JWT alone.
// In a Convex query or mutation
const identity = await ctx.auth.getUserIdentity();
// For critical operations, also verify the session document exists
Session creation time
Use session._creationTime to check how recently the user signed in. Useful for requiring re-authentication on sensitive actions.
React Hooks Quick Reference
| Hook | Returns | Use for |
|---|---|---|
useConvexAuth() |
{ isAuthenticated, isLoading } |
Checking if user is signed in |
useAuthActions() |
{ signIn, signOut } |
Triggering sign-in and sign-out |
useAuthToken() |
`string | null` |
useConvexAuth
const { isAuthenticated, isLoading } = useConvexAuth();
useAuthActions
const { signIn, signOut } = useAuthActions();
await signIn("github"); // OAuth sign-in
await signIn("google"); // OAuth sign-in
await signOut(); // Sign out
useAuthToken
For authenticating Convex HTTP actions from the client:
const token = useAuthToken();
await fetch(`${CONVEX_SITE_URL}/someEndpoint`, {
headers: { Authorization: `Bearer ${token}` },
});
Checklist
Development
[ ]
npm install @convex-dev/auth @auth/core@0.37.0[ ]
npx @convex-dev/auth[ ] Add
authTablestoconvex/schema.ts[ ] Replace
ConvexProviderwithConvexAuthProviderinmain.tsx[ ] Register GitHub OAuth app with dev callback URL
[ ] Set
AUTH_GITHUB_IDandAUTH_GITHUB_SECRETenv variables[ ] Register Google OAuth client with dev callback URL
[ ] Add test users in Google Cloud Console
[ ] Set
AUTH_GOOGLE_IDandAUTH_GOOGLE_SECRETenv variables[ ] Set
SITE_URLtohttp://localhost:5173[ ] Add both providers to
convex/auth.ts[ ] Add sign-in buttons to your app
[ ] Test both sign-in flows locally
Production
[ ] Create separate GitHub OAuth app with production URLs
[ ] Create separate Google OAuth client with production URLs
[ ] Set all env variables on production Convex deployment
[ ] Set
SITE_URLto your production frontend URL[ ] Optionally set
CUSTOM_AUTH_SITE_URLfor custom domain






