Auth
Doc Status: Excellent | ✓ Clear summary | ✓ Easy to read | ✓ Matches code | ✓ Good structure | ✓ Professional look | ✓ Visual components
Why This Matters
Convex Auth (@convex-dev/auth) replaced Clerk on 2026-05-08. It runs entirely inside the Convex deployment — no third-party JWT issuer, no external auth service to trust. The library owns the users / authAccounts / authSessions / authVerificationCodes / authVerifiers tables and exposes sign-in / sign-out hooks for the client.
Providers
Convex Auth supports three provider types. Configure them in convex/auth.ts:
// packages/backend/convex/auth.ts
import { Password } from '@convex-dev/auth/providers/Password';
import Google from '@auth/core/providers/google';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password, Google /* , ResendOTP */],
});
| Provider | Status | Setup Required |
|---|
| Password | Active | None |
| Google OAuth | Active | AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET via npx convex env set |
| Email OTP (Resend) | Disabled | Uncomment ResendOTP import + AUTH_RESEND_KEY |
The Google OAuth client is configured in Google Cloud Console with redirect URI https://<deployment>.convex.site/api/auth/callback/google. See .claude/rules/auth/auth-google-oauth.md for the full setup guide.
Files
convex/auth.ts — Provider Configuration
// packages/backend/convex/auth.ts
import { Password } from '@convex-dev/auth/providers/Password';
import Google from '@auth/core/providers/google';
import { convexAuth } from '@convex-dev/auth/server';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password, Google],
});
convex/auth.config.ts — App Identity
// packages/backend/convex/auth.config.ts
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: 'convex',
},
],
};
convex/http.ts — HTTP Routes
The httpRouter wires auth library routes (/api/auth/signin/..., /api/auth/callback/..., /.well-known/openid-configuration) automatically:
// packages/backend/convex/http.ts
import { httpRouter } from 'convex/server';
import { auth } from './auth';
const http = httpRouter();
auth.addHttpRoutes(http);
export default http;
convex/authEmailResend.ts — Email OTP Provider
Disabled by default. To enable, uncomment the import in auth.ts and set AUTH_RESEND_KEY. Uses Resend for transactional email with 15-minute OTP codes:
npx convex env set AUTH_RESEND_KEY 're_...'
npx convex env set AUTH_EMAIL_FROM 'WonderSound <noreply@wondersound.vn>'
Schema
Spread authTables into defineSchema and extend the users table with a role field:
// packages/backend/convex/schema.ts
import { defineSchema } from 'convex/server';
import { authTables } from '@convex-dev/auth/server';
import { v } from 'convex/values';
export default defineSchema({
...authTables,
users: defineTable({
// Fields from authTables.users (email, name, image, ...) plus:
role: v.optional(v.union(v.literal('admin'), v.literal('staff'), v.literal('student'))),
}).index('email', ['email']),
});
Server-Side
Get Current User ID
import { getAuthUserId } from '@convex-dev/auth/server';
export const myProfile = query({
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) return null;
return await ctx.db.get(userId);
},
});
Get Full Identity Claims
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error('UNAUTHENTICATED');
// identity.subject — stable user id (matches Id<'users'>)
// identity.email, identity.tokenIdentifier — claims
RBAC — Check Role
import { getAuthUserId } from '@convex-dev/auth/server';
export const adminOnly = mutation({
args: { ... },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error('UNAUTHENTICATED');
const me = await ctx.db.get(userId);
if (me?.role !== 'admin') throw new Error('FORBIDDEN');
// Admin work...
},
});
Client-Side
Web (Next.js)
// apps/admin/src/components/providers/ConvexClientProvider.tsx
'use client';
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
import { ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
return <ConvexAuthNextjsProvider client={convex}>{children}</ConvexAuthNextjsProvider>;
}
Mobile (Expo)
// apps/mobile/src/lib/ConvexClientProvider.tsx
import { ConvexAuthProvider } from '@convex-dev/auth/react';
import { ConvexReactClient } from 'convex/react';
import * as SecureStore from 'expo-secure-store';
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
const secureStorage = {
getItem: SecureStore.getItemAsync,
setItem: SecureStore.setItemAsync,
removeItem: SecureStore.deleteItemAsync,
};
export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
return (
<ConvexAuthProvider client={convex} storage={secureStorage}>
{children}
</ConvexAuthProvider>
);
}
Tokens are persisted in expo-secure-store (Keychain on iOS, EncryptedSharedPreferences on Android).
'use client';
import { useAuthActions } from '@convex-dev/auth/react';
import { useState } from 'react';
export function SignInForm() {
const { signIn, signOut } = useAuthActions();
const [flow, setFlow] = useState<'signIn' | 'signUp'>('signIn');
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await signIn('password', {
email: formData.get('email') as string,
password: formData.get('password') as string,
flow,
});
}}
>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">{flow === 'signIn' ? 'Sign in' : 'Sign up'}</button>
</form>
);
}
Webhook Auth (CF Workers)
Webhook workers (apps/lark-sync, apps/meta-conversions-worker) authenticate via shared secret, not Convex Auth:
app.post('/webhook/lark', async (c) => {
const secret = c.req.header('x-lark-signature');
if (secret !== env.LARK_WEBHOOK_SECRET) {
return c.json({ error: 'Unauthorized' }, 401);
}
return handleLarkWebhook(c);
});
Environment Variables
| Variable | Where set | Notes |
|---|
JWT_PRIVATE_KEY | Convex env (set by setup) | RSA private key for token signing |
JWKS | Convex env (set by setup) | Public JWKS for verification |
CONVEX_SITE_URL | Convex (auto-set) | Convex HTTP actions URL |
SITE_URL | Convex env (manual) | App URL for OAuth callbacks |
AUTH_GOOGLE_ID | Convex env | Google OAuth client ID |
AUTH_GOOGLE_SECRET | Convex env | Google OAuth client secret |
AUTH_RESEND_KEY | Convex env | Resend API key (for email OTP) |
AUTH_EMAIL_FROM | Convex env | Sender email address |
One-time setup: cd packages/backend && npx @convex-dev/auth