# 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:

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

```bash
npm install @convex-dev/auth @auth/core@0.37.0
```

## 2\. Run the init command

```bash
npx @convex-dev/auth
```

This configures your Convex project for auth.

## 3\. Add auth tables to your schema

```ts
// 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.

```tsx
// 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:

```plaintext
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.

```bash
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.

```bash
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.

```ts
// 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

```tsx
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

```tsx
import { useAuthActions } from "@convex-dev/auth/react";

function UserMenu() {
  const { signOut } = useAuthActions();

  return <button onClick={() => signOut()}>Sign out</button>;
}
```

## Checking auth state

```tsx
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`:

```tsx
signIn("github", { redirectTo: "/dashboard" });
```

* * *

# Auth Guard with TanStack Router

Use a pathless layout route to protect routes. Check auth in the component.

```tsx
// 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:

```plaintext
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:

```ts
// 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:

```ts
// 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.

```ts
// 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`.

```ts
// 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:

```ts
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:

```bash
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
    

```bash
# 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:

```bash
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.

```ts
// 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` | Getting the JWT for HTTP actions |

## useConvexAuth

```tsx
const { isAuthenticated, isLoading } = useConvexAuth();
```

## useAuthActions

```tsx
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:

```tsx
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
