--- name: convex-authentication description: Set up and manage user authentication using Convex Auth with login, signup, password reset, and user profile initialization. Use when implementing auth flows, managing user sessions, initializing user profiles, or handling authentication state. compatibility: Requires Convex Auth 0.0.90+, Next.js App Router, ConvexAuthNextjsProvider metadata: author: PictionAI category: backend, auth frameworks: Convex, @convex-dev/auth, Next.js 16 --- # Convex Authentication ## Overview This skill implements complete user authentication using Convex Auth built-in system, including signup with profile initialization, login, password reset, and session management integrated with Next.js 16. ## Architecture ### Auth Flow ``` 1. User signs up with email + password ↓ 2. Profile initialized in users table (username, email, avatar_url, total_score, games_played) ↓ 3. Auth token created ↓ 4. User logged in, can access protected routes ↓ 5. Can login/logout, update password ``` ### Convex Auth Integration **Note**: Uses `@convex-dev/auth` 0.0.90, NOT custom auth. ```typescript // convex/auth.ts import { ConvexAuth } from "@convex-dev/auth/server"; import { password } from "@convex-dev/auth/providers"; export const auth = new ConvexAuth({ providers: [password], }); ``` ### Database Schema ```typescript // Users table (extends Convex auth) export const users = defineTable({ // Auth fields (built-in): // - email: string (unique) // - password: string (hashed) // - isEmailVerified: boolean // Extended fields: username: v.string(), // Display name avatar_url: v.optional(v.string()), // Profile image total_score: v.number(), // Cumulative score games_played: v.number(), // Total games created_at: v.number(), // Signup timestamp }) .index("by_email", ["email"]) .index("by_username", ["username"]); ``` ## Authentication Provider Setup ### Convex Auth Config ```typescript // convex/auth.config.ts import { defineAuth } from "@convex-dev/auth/server"; import { password } from "@convex-dev/auth/providers"; export default defineAuth({ providers: [ password({ minPasswordLength: 8, maxPasswordLength: 128, }), ], callbacks: { async onSignUp(req) { // Called after successful signup // User record created automatically return req.identity; }, async onSignIn(req) { // Called on successful login return req.identity; }, }, }); ``` ## Signup Mutation with Profile Initialization ### signUpUser Mutation ```typescript export const signUpUser = mutation({ args: { email: v.string(), password: v.string(), username: v.string(), }, handler: async (ctx, args) => { // 1. Validate input if (args.username.length < 3 || args.username.length > 30) { throw new Error("Username must be 3-30 characters"); } // 2. Check username uniqueness const existing = await ctx.db .query("users") .withIndex("by_username", (q) => q.eq("username", args.username)) .first(); if (existing) { throw new Error("Username already taken"); } // 3. Create auth account (handled by Convex Auth) // 4. Initialize user profile const userId = (await ctx.auth.getUserIdentity())?.tokenIdentifier; if (!userId) { throw new Error("Failed to create user account"); } // 5. Store profile data const now = Date.now(); await ctx.db.insert("users", { email: args.email, username: args.username, avatar_url: null, total_score: 0, games_played: 0, created_at: now, }); return { success: true, userId, username: args.username, }; }, }); ``` ## Zod Validation Schema ```typescript // lib/schemas.ts import { z } from "zod"; export const signUpSchema = z.object({ email: z.string().email("Invalid email address"), password: z .string() .min(8, "Password must be at least 8 characters") .max(128, "Password too long"), username: z .string() .min(3, "Username must be at least 3 characters") .max(30, "Username must be at most 30 characters") .regex( /^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscore, hyphen" ), }); export const loginSchema = z.object({ email: z.string().email("Invalid email address"), password: z.string().min(1, "Password required"), }); export const updatePasswordSchema = z.object({ currentPassword: z.string(), newPassword: z .string() .min(8, "Password must be at least 8 characters") .refine( (pwd) => /[A-Z]/.test(pwd), "Password must contain uppercase letter" ) .refine((pwd) => /[0-9]/.test(pwd), "Password must contain number"), }); ``` ## Custom Hooks ### useAuthenticatedUser ```typescript // hooks/useAuth.ts import { useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; interface AuthProfile { user_id: string; username: string; email: string; avatar_url?: string; total_score: number; games_played: number; } export function useAuthenticatedUser() { const profile = useQuery(api.queries.profiles.getCurrentUserProfile); return { profile: profile as AuthProfile | null | undefined, isLoading: profile === undefined, isAuthenticated: profile !== null && profile !== undefined, }; } export function useAuthContext() { const profile = useAuthenticatedUser(); return { userId: profile.profile?.user_id, username: profile.profile?.username, email: profile.profile?.email, isAuthenticated: profile.isAuthenticated, isLoading: profile.isLoading, }; } ``` ### getCurrentUserProfile Query ```typescript export const getCurrentUserProfile = query({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return null; } const user = await ctx.db .query("users") .withIndex("by_email", (q) => q.eq("email", identity.email!)) .first(); return user ? { user_id: user._id, username: user.username, email: identity.email, avatar_url: user.avatar_url, total_score: user.total_score, games_played: user.games_played, } : null; }, }); ``` ## React Components ### AuthProvider Wrapper ```typescript // app/layout.tsx import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return (