xiaohongshu
xiaohongshu
convex-authentication
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.
Full skill instructions
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.
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
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],
});
// 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"]);
// 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;
},
},
});
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,
};
},
});
// 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"),
});
// 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,
};
}
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;
},
});
// 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>
);
}
// 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>
);
}
// 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>
);
}
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthenticatedUser();
if (isLoading) {
return <LoadingSpinner />;
}
if (!isAuthenticated) {
return <Redirect to="/auth/login" />;
}
return <>{children}</>;
}
export function LogoutButton() {
const { logOut } = useConvexAuth();
const handleLogout = async () => {
await logOut();
window.location.href = "/";
};
return <button onClick={handleLogout}>Logout</button>;
}
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 };
},
});
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 };
},
});
# .env.local
CONVEX_DEPLOYMENT=dev
NEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud
# Auth specific
CONVEX_AUTH_CALLBACK=http://localhost:3000 # For development
const { userId } = useAuthContext();
useEffect(() => {
if (!isAuthenticated && !isLoading) {
router.push("/auth/login");
}
}, [isAuthenticated, isLoading, router]);
const updateScore = useMutation(api.mutations.profiles.updateUserScore);
await updateScore({
user_id: userId,
points: 150,
});
✅ 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
convex/auth.ts - Auth configurationconvex/auth.config.ts - Auth providers setupconvex/queries/profiles.ts - User profile queriescomponents/auth/ - Auth UI componentsxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience