Convex Auth for AI Apps (GitHub + Google OAuth)
Add GitHub and Google OAuth authentication to your AI app using Convex, with protected routes, session management, and role-based access for agents and users.
Every AI app that handles user data needs authentication. Whether you're building a chatbot, an autonomous agent, or a multi-tenant AI platform, you need to know who's making requests and what they're allowed to do.
In this tutorial, you'll add full OAuth authentication to a Next.js + Convex AI app using GitHub and Google as providers. If you're still deciding on your backend, our comparison of Convex vs Supabase for AI agent apps covers the trade-offs in detail.
Prerequisites
- Next.js 16 app with TypeScript
- Convex project initialized
- GitHub and Google OAuth apps created (we'll walk through this below)
- Basic familiarity with App Router
What you'll build
You'll add full authentication to an AI app using Convex:
- GitHub and Google OAuth sign-in
- Secure user sessions
- Protected API routes
- User-aware queries and mutations
- Role-based access for agents vs users
1) Install Convex Auth
Add Convex and auth packages:
pnpm add convex @convex-dev/auth @auth/core
If you don't have pnpm installed yet, run npm install -g pnpm first.
2) Create a GitHub OAuth app
Before configuring anything in code, you need to register your app with GitHub:
- Go to GitHub Developer Settings → OAuth Apps
- Click New OAuth App
- Fill in:
- Application name: Your app name (e.g., "AI Agent App")
- Homepage URL:
http://localhost:3000(for local dev) - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Click Register application
- Copy the Client ID
- Click Generate a new client secret and copy it immediately — you won't see it again
For production, you'll need a second OAuth app (or update the URLs) pointing to your live domain. See our deployment guide for the full setup.
3) Create a Google OAuth app
Google's setup is a bit more involved:
- Go to the Google Cloud Console
- Create a new project (or select an existing one)
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID
- Select Web application as the type
- Add authorized redirect URIs:
http://localhost:3000/api/auth/callback/google(development)https://your-domain.com/api/auth/callback/google(production)
- Copy the Client ID and Client Secret
You'll also need to configure the OAuth consent screen — set it to "External" if you want anyone with a Google account to sign in, or "Internal" for Google Workspace users only.
4) Configure environment variables
Create .env.local:
CONVEX_URL=YOUR_CONVEX_URL
AUTH_SECRET=YOUR_AUTH_SECRET
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=YOUR_GITHUB_CLIENT_SECRET
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
Generate AUTH_SECRET with:
openssl rand -base64 32
This secret is used to sign session tokens. Never commit it to version control.
5) Add Convex auth config
Create convex/auth.ts:
import { convexAuth } from "@convex-dev/auth/server";
import GitHub from "@auth/core/providers/github";
import Google from "@auth/core/providers/google";
export const { auth, signIn, signOut, store } = convexAuth({
providers: [
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!,
}),
],
});
6) Update Convex schema
Edit convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
image: v.optional(v.string()),
role: v.optional(v.union(v.literal("user"), v.literal("admin"), v.literal("agent"))),
}),
sessions: defineTable({
userId: v.id("users"),
expires: v.number(),
}).index("by_user", ["userId"]),
});
Notice the role field — this is how you'll differentiate between human users and AI agents making authenticated requests.
7) Add auth route handlers
Create app/api/auth/[...auth]/route.ts:
import { auth } from "@/convex/auth";
export const { GET, POST } = auth;
This enables /api/auth/* routes automatically — sign-in, sign-out, callbacks, and session endpoints are all handled by the auth library.
8) Create auth helpers
Add lib/auth.ts:
import { ConvexHttpClient } from "convex/browser";
import { api } from "@/convex/_generated/api";
const client = new ConvexHttpClient(process.env.CONVEX_URL!);
export async function getSession() {
return await client.query(api.authSession.getSession, {});
}
9) Add session queries and mutations
Create convex/authSession.ts:
import { query, mutation } from "convex/server";
import { v } from "convex/values";
export const getSession = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return {
subject: identity.subject,
email: identity.email,
name: identity.name,
picture: identity.pictureUrl,
};
},
});
export const requireUser = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return identity;
},
});
Session management: JWT vs cookies
Convex auth uses JWTs by default. Here's how the flow works:
- User signs in via OAuth → callback sets a session cookie
- The session cookie contains a JWT with the user's identity
- On each request, Convex validates the JWT server-side
- No database lookup needed for session validation (JWTs are self-contained)
This is faster than traditional cookie-session stores that require a database read on every request. The trade-off is that you can't revoke individual JWTs — they're valid until they expire. For most AI apps, this is an acceptable trade-off.
10) Protect agent routes
Update your agent API route to require a session:
import { ConvexHttpClient } from "convex/browser";
import { api } from "@/convex/_generated/api";
const convex = new ConvexHttpClient(process.env.CONVEX_URL!);
export async function POST(req: Request) {
const identity = await convex.query(api.authSession.requireUser, {});
if (!identity) return new Response("Unauthorized", { status: 401 });
// ... your existing agent logic
return Response.json({ ok: true });
}
For a complete agent implementation to protect, see our tutorial on building your first AI agent in TypeScript.
11) Role-based access for agents vs users
In many AI apps, you'll have both human users and AI agents making authenticated requests. You need different permission levels for each:
export const requireAdmin = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("email"), identity.email))
.first();
if (!user || user.role !== "admin") {
throw new Error("Insufficient permissions");
}
return user;
},
});
export const requireAgent = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("email"), identity.email))
.first();
if (!user || user.role !== "agent") {
throw new Error("Not an authorized agent");
}
return user;
},
});
Use requireAdmin for dashboard routes and requireAgent for API endpoints that only agents should access (like tool execution or heartbeat reporting).
12) Error handling for auth flows
OAuth can fail in surprising ways. Handle the common cases gracefully:
// app/auth/error/page.tsx
"use client";
import { useSearchParams } from "next/navigation";
export default function AuthErrorPage() {
const params = useSearchParams();
const error = params.get("error");
const errorMessages: Record<string, string> = {
OAuthCallback: "The OAuth provider returned an error. Try signing in again.",
OAuthAccountNotLinked: "This email is already linked to another account.",
AccessDenied: "You denied the authorization request.",
default: "An unexpected authentication error occurred.",
};
return (
<main className="p-8 max-w-md mx-auto">
<h1 className="text-2xl font-bold text-red-600">Sign-in Error</h1>
<p className="mt-4">{errorMessages[error ?? "default"]}</p>
<a href="/login" className="underline mt-4 block">
Try again
</a>
</main>
);
}
13) Add sign-in UI
Create app/login/page.tsx:
"use client";
export default function LoginPage() {
return (
<main className="p-8 max-w-md mx-auto">
<h1 className="text-2xl font-bold">Sign in</h1>
<div className="mt-6 space-y-3">
<a
href="/api/auth/signin/github"
className="block bg-black text-white p-3 rounded text-center"
>
Continue with GitHub
</a>
<a
href="/api/auth/signin/google"
className="block bg-red-600 text-white p-3 rounded text-center"
>
Continue with Google
</a>
</div>
</main>
);
}
14) Use session on the client
Add app/page.tsx:
import { getSession } from "@/lib/auth";
export default async function Home() {
const session = await getSession();
if (!session) {
return (
<main className="p-8">
<h1 className="text-2xl font-bold">AI App</h1>
<a className="underline" href="/login">
Sign in to continue
</a>
</main>
);
}
return (
<main className="p-8">
<h1 className="text-2xl font-bold">Welcome, {session.name}</h1>
<p>Your email: {session.email}</p>
</main>
);
}
15) Protected route middleware
For a cleaner approach across many routes, use Next.js middleware to protect entire path groups:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const token = req.cookies.get("session-token");
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/agent/:path*"],
};
This redirects unauthenticated users to the login page for any route under /dashboard or /api/agent. It's a lightweight check — the real authorization still happens in Convex.
16) Testing auth locally
Testing OAuth locally requires a few extra steps:
- Use
localhost:3000— both GitHub and Google support localhost callbacks without HTTPS - Check callback URLs — they must match exactly, including trailing slashes
- Clear cookies between tests — stale sessions cause confusing errors
- Check the Convex dashboard — the Convex logs show auth events in real time
# Quick way to clear auth cookies in Chrome
# Open DevTools → Application → Cookies → Clear all
If you're using the Next.js + Convex stack, auth is one of the pieces that "just works" once configured correctly.
17) Troubleshooting tips
- Callback mismatch: Ensure OAuth callback URLs match exactly — protocol, domain, port, and path.
- Missing AUTH_SECRET: Convex auth requires a strong secret. Generate one with
openssl rand -base64 32. - Session returns null: Check the Convex auth logs and make sure cookies are set. Try an incognito window.
- 401 errors: Confirm your API route is calling
requireUsercorrectly. - "OAuthAccountNotLinked" error: The user signed in with a different provider previously. Handle this in your error page.
- Google consent screen not showing: Make sure the consent screen is published (not in "Testing" mode) or add your email as a test user.
Final thoughts
You now have GitHub and Google auth working in Convex, with protected AI routes and role-based access. This is the foundation for any serious AI application where users need ownership, billing, and data privacy. When you're ready to ship, follow our guide on deploying AI apps to Vercel and Convex to take this to production. For the complete auth-to-deployment pipeline, see the AI Product Building course. The AI Product Building course ties all of these pieces together — auth, deployment, and monetization.
Related reading
Enjoyed this guide?
Get more actionable AI insights, automation templates, and practical guides delivered to your inbox.
No spam. Unsubscribe anytime.
Ready to ship an AI product?
We build revenue-moving AI tools in focused agentic development cycles. 3 production apps shipped in a single day.