| 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 | [object Object] |
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.
// 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
// 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
// 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
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
// 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
// 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
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
// app/layout.tsx
import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<ConvexAuthNextjsProvider>
<ConvexProvider>{children}</ConvexProvider>
</ConvexAuthNextjsProvider>
</body>
</html>
);
}
SignUp Form
// components/auth/sign-up-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { signUpSchema } from "@/lib/schemas";
import { useConvexAuth } from "@convex-dev/auth/react";
export function SignUpForm() {
const form = useForm({
resolver: zodResolver(signUpSchema),
defaultValues: {
email: "",
password: "",
username: "",
},
});
const { signUp } = useConvexAuth();
async function onSubmit(values: z.infer<typeof signUpSchema>) {
try {
await signUp("password", {
email: values.email,
password: values.password,
username: values.username,
});
// Navigate to success page
window.location.href = "/auth/sign-up-success";
} catch (error) {
form.setError("email", {
message: error instanceof Error ? error.message : "Signup failed",
});
}
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="email"
render={({ field }) => <EmailInput {...field} />}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => <UsernameInput {...field} />}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => <PasswordInput {...field} />}
/>
<button type="submit">Sign Up</button>
</form>
);
}
Login Form
// components/auth/login-form.tsx
export function LoginForm() {
const form = useForm({
resolver: zodResolver(loginSchema),
});
const { logIn } = useConvexAuth();
async function onSubmit(values: z.infer<typeof loginSchema>) {
try {
await logIn("password", {
email: values.email,
password: values.password,
});
window.location.href = "/gioca";
} catch (error) {
form.setError("email", {
message: "Invalid email or password",
});
}
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
<button type="submit">Log In</button>
</form>
);
}
Session Management
Check Auth Status
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthenticatedUser();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Redirect to="/auth/login" />;
}
return <>{children}</>;
}
Logout
export function LogoutButton() {
const { logOut } = useConvexAuth();
const handleLogout = async () => {
await logOut();
window.location.href = "/";
};
return <button onClick={handleLogout}>Logout</button>;
}
Password Reset Flow
Request Password Reset
export const requestPasswordReset = mutation({
args: { email: v.string() },
handler: async (ctx, args) => {
const user = await ctx.db
.query("users")
.withIndex("by_email", (q) => q.eq("email", args.email))
.first();
if (!user) {
// Don't reveal if user exists
return { success: true };
}
// Generate reset token (send via email)
// Store token in database with expiry
return { success: true };
},
});
Update Password
export const updatePassword = mutation({
args: {
currentPassword: v.string(),
newPassword: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
// Verify current password before updating
// Update with new password hash
return { success: true };
},
});
Environment Variables
# .env.local
CONVEX_DEPLOYMENT=dev
NEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud
# Auth specific
CONVEX_AUTH_CALLBACK=http://localhost:3000 # For development
Common Patterns
Get current user ID
const { userId } = useAuthContext();
Redirect non-authenticated users
useEffect(() => {
if (!isAuthenticated && !isLoading) {
router.push("/auth/login");
}
}, [isAuthenticated, isLoading, router]);
Update user score after game
const updateScore = useMutation(api.mutations.profiles.updateUserScore);
await updateScore({
user_id: userId,
points: 150,
});
Best Practices
✅ Always validate email format server-side ✅ Hash passwords (Convex Auth does this automatically) ✅ Check username uniqueness before signup ✅ Use protected routes for authenticated pages ✅ Log auth events for security monitoring ✅ Implement rate limiting on auth endpoints ✅ Use secure HTTP-only cookies for tokens ✅ Validate password strength requirements
See Also
convex/auth.ts- Auth configurationconvex/auth.config.ts- Auth providers setupconvex/queries/profiles.ts- User profile queriescomponents/auth/- Auth UI components- Convex Auth docs: https://docs.convex.dev/auth/overview