Start the development server (runs both Convex backend and Next.js frontend):
pnpm dev
Other useful commands:
# Start only frontend (Convex must be running separately) pnpm dev:frontend # Start only Convex backend pnpm dev:backend # Run Convex once and exit pnpm convex dev --once # Build for production pnpm build # Run linting pnpm lint
src/app/ ├── (auth)/ # Protected routes │ ├── dashboard/ # Main dashboard │ └── settings/ # User settings ├── (unauth)/ # Public routes │ ├── sign-in/ # Login page │ └── sign-up/ # Registration page convex/ ├── auth.ts # Auth configuration ├── schema.ts # Database schema ├── http.ts # HTTP endpoints └── users.ts # Internal mutations
Create a new protected page:
// src/app/(auth)/my-page/page.tsx
"use client";
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { AppContainer } from "@/components/server";
export default function MyPage() {
const user = useQuery(api.auth.getCurrentUser);
return (
<AppContainer>
<h1>Hello {user?.name}</h1>
</AppContainer>
);
}All pages in the (auth) directory are automatically protected by the authentication proxy.
Query data from Convex:
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
const data = useQuery(api.myModule.myFunction);Mutate data:
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
const mutate = useMutation(api.myModule.myFunction);
await mutate({ arg: "value" });Get current user:
const user = useQuery(api.auth.getCurrentUser);
Sign out:
import { authClient } from "@/lib/auth-client";
await authClient.signOut();This template uses a dual-system authentication architecture:
src/lib/auth.ts): Configures providers, email verification, 2FA, magic linkssrc/lib/auth-client.ts): React hooks and client methods for auth operationsconvex/auth.ts): Connects Better Auth to Convex databasesrc/lib/auth.ts & convex/users.ts): Handles user synchronization via database hooks and internal mutationsconvex/http.ts): Registers Better Auth API endpointssrc/proxy.ts): Middleware that protects routes and redirects unauthenticated usersAuthentication supports: Email/Password, Google OAuth, GitHub OAuth, Slack OAuth, Magic Links, Email OTP, 2FA, and Anonymous authentication.
To add or remove OAuth providers:
1. Update server configuration:
// src/lib/auth.ts // Add to socialProviders or genericOAuth config
2. Update client configuration:
// src/lib/auth-client.ts // Add corresponding client plugin
3. Set environment variables:
# .env.local PROVIDER_CLIENT_ID=your-id PROVIDER_CLIENT_SECRET=your-secret # Convex pnpm convex env set PROVIDER_CLIENT_ID your-id pnpm convex env set PROVIDER_CLIENT_SECRET your-secret
4. Update UI components to add provider buttons
⚠️ Critical: Environment variables must be set in BOTH .env.local (for Next.js) AND Convex (for backend functions)
Required in .env.local:
# Convex (auto-generated after first deploy) CONVEX_DEPLOYMENT=automatic NEXT_PUBLIC_CONVEX_URL=https://example.convex.cloud NEXT_PUBLIC_CONVEX_SITE_URL=https://example.convex.site # Site URL SITE_URL=http://localhost:3000 # Better Auth Secret (generate with: openssl rand -base64 32) BETTER_AUTH_SECRET=your-secret-here # OAuth Providers (optional - only if using OAuth) GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret
Must also be set in Convex using these commands:
# Generate auth secret first openssl rand -base64 32 # Set in Convex (development) pnpm convex env set SITE_URL http://localhost:3000 pnpm convex env set BETTER_AUTH_SECRET your-secret-here # Optional: OAuth providers pnpm convex env set GOOGLE_CLIENT_ID your-google-client-id pnpm convex env set GOOGLE_CLIENT_SECRET your-google-client-secret pnpm convex env set GITHUB_CLIENT_ID your-github-client-id pnpm convex env set GITHUB_CLIENT_SECRET your-github-client-secret # For production, add --prod flag pnpm convex env set SITE_URL https://your-domain.com --prod pnpm convex env set BETTER_AUTH_SECRET your-prod-secret --prod
List all Convex environment variables:
pnpm convex env list
Deploying to Vercel:
1. Set Vercel build settings:
Build Command: npx convex deploy --cmd 'pnpm run build' Install Command: pnpm install
2. Add all environment variables from .env.local to Vercel
3. Set production environment variables in Convex:
pnpm convex env set SITE_URL https://your-domain.com --prod pnpm convex env set BETTER_AUTH_SECRET your-prod-secret --prod pnpm convex env set GOOGLE_CLIENT_ID your-id --prod pnpm convex env set GOOGLE_CLIENT_SECRET your-secret --prod # etc. for all required variables
4. Deploy to Vercel:
vercel deploy --prod
src/proxy.ts - Route protection middlewareconvex/auth.config.ts - Better Auth domain configurationconvex/schema.ts - Database schemaconvex/polyfills.ts - Required polyfills for Better Authconvex/email.tsx - Email templatesnext.config.ts - Next.js configurationCLAUDE.md - Detailed technical documentationℹ️ This section documents breaking changes from Better Auth v1.3.34+ and @convex-dev/better-auth v0.9.6+
The createClient function no longer accepts a configuration object with triggers.
❌ Before (Deprecated)
// convex/auth.ts
export const betterAuthComponent = createClient(
components.betterAuth,
{
verbose: false,
triggers: {
user: {
onCreate: async (ctx, user) => {
await ctx.db.insert("users", {
email: user.email,
});
},
onDelete: async (ctx, user) => {
// cleanup logic
},
},
},
}
);✅ After (Current)
// convex/auth.ts
export const betterAuthComponent =
createClient<DataModel>(
components.betterAuth
);
// Triggers moved to databaseHooks
// in src/lib/auth.tsUser lifecycle hooks are now configured in src/lib/auth.ts using Better Auth's databaseHooks feature, with internal mutations inconvex/users.ts for HTTP action contexts.
✅ New Pattern
// src/lib/auth.ts
const createOptions = (ctx: GenericCtx) => ({
// ... other options
databaseHooks: {
user: {
create: {
after: async (user) => {
// Use runMutation for HTTP actions
if ("runMutation" in ctx) {
await ctx.runMutation(
internal.users.syncUserCreation,
{ email: user.email }
);
} else if ("db" in ctx) {
await (ctx as MutationCtx).db.insert(
"users",
{ email: user.email }
);
}
},
},
delete: {
after: async (user) => {
// Similar pattern for deletion
},
},
},
},
});A new file convex/users.ts was added to handle user syncing via internal mutations. This is required because HTTP actions (OAuth callbacks) cannot directly access ctx.db.
// convex/users.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const syncUserCreation = internalMutation({
args: { email: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert("users", {
email: args.email,
});
},
});
export const syncUserDeletion = internalMutation({
args: { email: v.string() },
handler: async (ctx, args) => {
// Find and delete user + related data
},
});databaseHooks as the standard way to handle user lifecycle eventstriggersconfiguration from createClientctx.db, requiring runMutation calls<DataModel> generic tocreateClient ensures proper TypeScript inferenceCLAUDE.md for comprehensive technical documentation