Skip to main content

Command Palette

Search for a command to run...

Convex Auth — GitHub & Google OAuth Reference

Published
13 min read
Convex Auth — GitHub & Google OAuth Reference
T

Just a guy who loves to write code and watch anime.

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:

  1. User clicks a sign-in button in your app

  2. User gets redirected to the provider (GitHub/Google)

  3. User authenticates on the provider's site

  4. Provider redirects back to your Convex backend callback URL

  5. Convex handles the token exchange and creates/links the user

  6. 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:

  1. profile function — runs first. Picks fields from raw OAuth data. Saves to users table automatically.

  2. afterUserCreatedOrUpdated callback — runs after. You have the userId. 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:

  1. Your app runs on localhost:5173

  2. User clicks "Sign in with GitHub"

  3. Browser redirects to GitHub

  4. User authenticates on GitHub

  5. GitHub redirects to https://your-deployment.convex.site/api/auth/callback/github

  6. Convex handles the token exchange

  7. Convex redirects user back to your localhost app

  8. 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

  1. Create new OAuth apps on GitHub and Google with your production URLs

  2. Set env variables on your production Convex deployment (not dev)

  3. Update callback URLs to use your production Convex deployment URL

  4. 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

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 backend

  • Session documents are stored in the authSessions table

  • Sessions 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 authTables to convex/schema.ts

  • [ ] Replace ConvexProvider with ConvexAuthProvider in main.tsx

  • [ ] Register GitHub OAuth app with dev callback URL

  • [ ] Set AUTH_GITHUB_ID and AUTH_GITHUB_SECRET env variables

  • [ ] Register Google OAuth client with dev callback URL

  • [ ] Add test users in Google Cloud Console

  • [ ] Set AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET env variables

  • [ ] Set SITE_URL to http://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_URL to your production frontend URL

  • [ ] Optionally set CUSTOM_AUTH_SITE_URL for custom domain