{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-based-auth-nextjs", "title": "Password-Based Authentication (Next.js)", "description": "Complete password-based authentication flow for Next.js with Convex Auth. Includes login, sign-up, password reset, protected routes, and full Convex backend.", "dependencies": [ "lucide-react", "@convex-dev/auth@latest", "convex@latest", "@auth/core" ], "registryDependencies": [ "button", "input", "label", "card", "alert", "avatar", "skeleton" ], "files": [ { "path": "src/registry/convex/blocks/password-based-auth-nextjs/components/login-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { useState } from \"react\";\n\nexport function LoginForm() {\n const { signIn } = useAuthActions();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"email\", email);\n formData.set(\"password\", password);\n formData.set(\"flow\", \"signIn\");\n\n await signIn(\"password\", formData);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to sign in\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Welcome back\n Sign in to your account to continue\n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n
\n \n \n Forgot password?\n \n
\n setPassword(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n \n

\n Don't have an account?{\" \"}\n \n Sign up\n \n

\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/login-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/components/sign-up-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\n\nexport function SignUpForm() {\n const { signIn } = useAuthActions();\n const router = useRouter();\n const [name, setName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\");\n return;\n }\n\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\");\n return;\n }\n\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"name\", name);\n formData.set(\"email\", email);\n formData.set(\"password\", password);\n formData.set(\"flow\", \"signUp\");\n\n await signIn(\"password\", formData);\n router.push(\"/auth/sign-up-success\");\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to create account\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Create an account\n Enter your details to get started\n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setName(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n \n

\n Already have an account?{\" \"}\n \n Sign in\n \n

\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/sign-up-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { ArrowLeft, CheckCircle, Loader2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useState } from \"react\";\n\n/**\n * ForgotPasswordForm component for requesting password reset.\n *\n * Per Convex Auth docs, submits with flow: \"reset\" which triggers\n * the ResendOTPPasswordReset provider to send an OTP code via email.\n *\n * After successful submission, redirects to the update password page\n * with the email pre-filled.\n */\nexport function ForgotPasswordForm() {\n const { signIn } = useAuthActions();\n const router = useRouter();\n const [email, setEmail] = useState(\"\");\n const [error, setError] = useState(null);\n const [success, setSuccess] = useState(false);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"email\", email);\n formData.set(\"flow\", \"reset\");\n\n await signIn(\"password\", formData);\n setSuccess(true);\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to send reset code\",\n );\n } finally {\n setLoading(false);\n }\n };\n\n if (success) {\n return (\n \n \n
\n \n
\n Check your email\n \n We've sent a password reset code to {email}\n \n
\n \n Enter the 8-digit code from your email to reset your password.\n \n \n \n router.push(\n `/auth/update-password?email=${encodeURIComponent(email)}`,\n )\n }\n className=\"w-full\"\n >\n Enter reset code\n \n \n \n \n \n
\n );\n }\n\n return (\n \n \n Reset your password\n \n Enter your email and we'll send you a code to reset your password\n \n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n \n \n \n \n \n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/forgot-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/components/update-password-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { ArrowLeft, Loader2 } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { useState } from \"react\";\n\n/**\n * UpdatePasswordForm component for completing password reset.\n *\n * Supports two flows:\n * 1. OTP Code: User enters the 8-digit code received via email + new password\n * 2. Email parameter: Email is passed via URL from the forgot password flow\n *\n * Per Convex Auth docs, the form submits:\n * - code: The verification code\n * - email: The user's email address\n * - newPassword: The new password\n * - flow: \"reset-verification\"\n */\nexport function UpdatePasswordForm() {\n const { signIn } = useAuthActions();\n const router = useRouter();\n const searchParams = useSearchParams();\n const emailParam = searchParams.get(\"email\");\n\n const [email, setEmail] = useState(emailParam ?? \"\");\n const [code, setCode] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!email) {\n setError(\"Email is required\");\n return;\n }\n\n if (!code) {\n setError(\"Verification code is required\");\n return;\n }\n\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\");\n return;\n }\n\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\");\n return;\n }\n\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"code\", code);\n formData.set(\"email\", email);\n formData.set(\"newPassword\", password);\n formData.set(\"flow\", \"reset-verification\");\n\n await signIn(\"password\", formData);\n router.push(\"/auth/login?message=password-updated\");\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to update password\",\n );\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Set new password\n \n Enter the code from your email and your new password\n \n \n
\n \n {error && (\n \n {error}\n \n )}\n {!emailParam && (\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n />\n
\n )}\n
\n \n setCode(e.target.value)}\n required\n disabled={loading}\n maxLength={8}\n pattern=\"[0-9]{8}\"\n />\n
\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n />\n
\n
\n \n \n \n \n \n \n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/update-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/components/logout-button.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { LogOut } from \"lucide-react\";\n\ninterface LogoutButtonProps extends React.ComponentProps {\n showIcon?: boolean;\n}\n\nexport function LogoutButton({\n showIcon = true,\n children,\n ...props\n}: LogoutButtonProps) {\n const { signOut } = useAuthActions();\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "src/components/logout-button.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/auth/login/page.tsx", "content": "import { LoginForm } from \"@/components/login-form\";\n\nexport default function LoginPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "src/app/auth/login/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/auth/sign-up/page.tsx", "content": "import { SignUpForm } from \"@/components/sign-up-form\";\n\nexport default function SignUpPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "src/app/auth/sign-up/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/auth/forgot-password/page.tsx", "content": "import { ForgotPasswordForm } from \"@/components/forgot-password-form\";\n\nexport default function ForgotPasswordPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "src/app/auth/forgot-password/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/auth/update-password/page.tsx", "content": "import { Suspense } from \"react\";\nimport { UpdatePasswordForm } from \"@/components/update-password-form\";\n\nexport default function UpdatePasswordPage() {\n return (\n
\n Loading...
}>\n \n \n \n );\n}\n", "type": "registry:page", "target": "src/app/auth/update-password/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/auth/sign-up-success/page.tsx", "content": "import {\n Card,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { CheckCircle } from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport default function SignUpSuccessPage() {\n return (\n
\n \n \n
\n \n
\n Account created!\n \n Your account has been successfully created. You can now sign in with\n your credentials.\n \n
\n \n \n \n \n \n
\n
\n );\n}\n", "type": "registry:page", "target": "src/app/auth/sign-up-success/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/app/protected/page.tsx", "content": "\"use client\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { LogoutButton } from \"@/components/logout-button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { api } from \"@/convex/_generated/api\";\nimport { useQuery } from \"convex/react\";\n\nexport default function ProtectedPage() {\n const user = useQuery(api.users.current);\n\n if (user === undefined) {\n return (\n
\n \n \n \n \n \n \n \n \n \n
\n );\n }\n\n if (!user) {\n return (\n
\n \n \n Not authenticated\n Please sign in to view this page.\n \n \n
\n );\n }\n\n return (\n
\n \n \n Protected Page\n \n You are signed in and can view this content.\n \n \n \n
\n \n \n \n {(user.name ?? \"U\").slice(0, 2).toUpperCase()}\n \n \n
\n

{user.name ?? \"Anonymous\"}

\n

{user.email}

\n
\n
\n \n
\n
\n
\n );\n}\n", "type": "registry:page", "target": "src/app/protected/page.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/convex/schema.ts", "content": "import { defineSchema, defineTable } from \"convex/server\";\nimport { authTables } from \"@convex-dev/auth/server\";\nimport { v } from \"convex/values\";\n\n/**\n * Schema for password-based authentication.\n * Includes the users table and all required auth tables from @convex-dev/auth.\n *\n * Security considerations:\n * - Email is indexed for fast lookups but never exposed in public queries\n * - emailVerificationTime tracks when email was verified\n * - isAnonymous flag supports gradual account upgrades\n */\nexport default defineSchema({\n // Auth tables from @convex-dev/auth (authAccounts, authSessions, authVerificationCodes, etc.)\n ...authTables,\n\n // Users table with profile information\n users: defineTable({\n // Profile fields\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n\n // Auth metadata\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n })\n .index(\"by_email\", [\"email\"])\n .index(\"by_anonymous\", [\"isAnonymous\"]),\n});\n", "type": "registry:file", "target": "convex/schema.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/convex/auth.ts", "content": "import { convexAuth } from \"@convex-dev/auth/server\";\nimport { Password } from \"@convex-dev/auth/providers/Password\";\nimport Resend from \"@auth/core/providers/resend\";\nimport GitHub from \"@auth/core/providers/github\";\nimport Google from \"@auth/core/providers/google\";\nimport { DataModel } from \"./_generated/dataModel\";\n\n/**\n * Custom Resend provider for email verification OTP.\n *\n * Sends an 8-digit OTP code for email verification during sign-up.\n * Configure AUTH_RESEND_KEY environment variable to enable.\n */\nconst ResendOTP = Resend({\n id: \"resend-otp-verification\",\n apiKey: process.env.AUTH_RESEND_KEY,\n async generateVerificationToken() {\n const code = Array.from({ length: 8 }, () =>\n Math.floor(Math.random() * 10),\n ).join(\"\");\n return code;\n },\n async sendVerificationRequest({ identifier: email, provider, token }) {\n const response = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${provider.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: process.env.AUTH_EMAIL_FROM ?? \"onboarding@resend.dev\",\n to: [email],\n subject: \"Verify your email\",\n text: `Your verification code is: ${token}\\n\\nThis code expires in 15 minutes.`,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\"Could not send verification email\");\n }\n },\n});\n\n/**\n * Custom Resend provider for password reset OTP.\n *\n * Sends an 8-digit OTP code for password reset.\n * Configure AUTH_RESEND_KEY environment variable to enable.\n */\nconst ResendOTPPasswordReset = Resend({\n id: \"resend-otp-reset\",\n apiKey: process.env.AUTH_RESEND_KEY,\n async generateVerificationToken() {\n const code = Array.from({ length: 8 }, () =>\n Math.floor(Math.random() * 10),\n ).join(\"\");\n return code;\n },\n async sendVerificationRequest({ identifier: email, provider, token }) {\n const response = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${provider.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: process.env.AUTH_EMAIL_FROM ?? \"onboarding@resend.dev\",\n to: [email],\n subject: \"Reset your password\",\n text: `Your password reset code is: ${token}\\n\\nThis code expires in 15 minutes.`,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\"Could not send password reset email\");\n }\n },\n});\n\n/**\n * Custom Password provider with:\n * - Email verification during sign-up (via ResendOTP)\n * - Password reset flow (via ResendOTPPasswordReset)\n * - Strong password requirements\n */\nconst CustomPassword = Password({\n // Require email verification during sign-up\n verify: ResendOTP,\n\n // Enable password reset via email\n reset: ResendOTPPasswordReset,\n\n // Custom password validation\n validatePasswordRequirements: (password: string) => {\n if (password.length < 8) {\n throw new Error(\"Password must be at least 8 characters\");\n }\n if (!/[a-z]/.test(password)) {\n throw new Error(\"Password must contain at least one lowercase letter\");\n }\n if (!/[A-Z]/.test(password)) {\n throw new Error(\"Password must contain at least one uppercase letter\");\n }\n if (!/[0-9]/.test(password)) {\n throw new Error(\"Password must contain at least one number\");\n }\n },\n\n // Customize profile to handle additional user fields\n profile(params) {\n return {\n email: params.email as string,\n name: (params.name as string) || undefined,\n };\n },\n});\n\n// Build providers list dynamically based on available env vars\nconst providers: Parameters[0][\"providers\"] = [\n CustomPassword,\n];\n\n// Only add GitHub if credentials are configured\nif (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) {\n providers.push(GitHub);\n}\n\n// Only add Google if credentials are configured\nif (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {\n providers.push(Google);\n}\n\n/**\n * Convex Auth configuration.\n *\n * Features:\n * - Password authentication with email verification\n * - Password reset via OTP codes\n * - Strong password requirements\n * - Optional GitHub OAuth (if AUTH_GITHUB_ID/SECRET configured)\n * - Optional Google OAuth (if AUTH_GOOGLE_ID/SECRET configured)\n *\n * Environment variables:\n * - AUTH_RESEND_KEY: Resend API key for sending emails (required)\n * - AUTH_EMAIL_FROM: Sender email address (optional)\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth (optional)\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth (optional)\n */\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers,\n});\n", "type": "registry:file", "target": "convex/auth.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/convex/auth.config.ts", "content": "import { AuthConfig } from \"convex/server\";\n\n/**\n * Auth configuration for Convex Auth with password + OAuth providers.\n *\n * The domain should match your Convex deployment URL.\n * This is automatically set via CONVEX_SITE_URL environment variable.\n *\n * Required environment variables:\n * - CONVEX_SITE_URL: Your Convex deployment URL\n * - AUTH_RESEND_KEY: Resend API key for email verification/reset\n *\n * Optional environment variables (for social login):\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\n * - AUTH_EMAIL_FROM: Sender email address (defaults to onboarding@resend.dev)\n *\n * Configure the OAuth callback URLs in your provider's settings:\n * - GitHub: https://YOUR_CONVEX_URL/api/auth/callback/github\n * - Google: https://YOUR_CONVEX_URL/api/auth/callback/google\n */\nexport default {\n providers: [\n {\n domain: process.env.CONVEX_SITE_URL!,\n applicationID: \"convex\",\n },\n ],\n} satisfies AuthConfig;\n", "type": "registry:file", "target": "convex/auth.config.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/convex/users.ts", "content": "import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { getAuthUserId } from \"@convex-dev/auth/server\";\nimport { ConvexError } from \"convex/values\";\n\n/**\n * User validator for internal use.\n * Includes all fields - use publicProfileValidator for external queries.\n */\nconst userValidator = v.object({\n _id: v.id(\"users\"),\n _creationTime: v.number(),\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n});\n\n/**\n * Public profile validator - excludes sensitive fields like email.\n * Use this when returning user data to other users.\n */\nconst publicProfileValidator = v.object({\n _id: v.id(\"users\"),\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n});\n\n/**\n * Get the current authenticated user's full profile.\n * Returns null if not authenticated.\n *\n * Security: Only returns data for the authenticated user themselves.\n */\nexport const current = query({\n args: {},\n returns: v.union(userValidator, v.null()),\n handler: async (ctx: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n return null;\n }\n return await ctx.db.get(userId);\n },\n});\n\n/**\n * Get a user's public profile by ID.\n * Only returns safe, public fields (name, image).\n *\n * Security: Never exposes email or other sensitive data.\n */\nexport const get = query({\n args: { userId: v.id(\"users\") },\n returns: v.union(publicProfileValidator, v.null()),\n handler: async (ctx: any, args: any) => {\n const user = await ctx.db.get(args.userId);\n if (!user) {\n return null;\n }\n // Only return public fields\n return {\n _id: user._id,\n name: user.name,\n image: user.image,\n };\n },\n});\n\n/**\n * Update the current user's profile.\n * Only allows updating safe fields (name, image).\n *\n * Security:\n * - Requires authentication\n * - Only allows updating own profile\n * - Validates input types\n */\nexport const updateProfile = mutation({\n args: {\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n },\n returns: v.null(),\n handler: async (ctx: any, args: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n throw new ConvexError({\n code: \"UNAUTHORIZED\",\n message: \"You must be logged in to update your profile\",\n });\n }\n\n // Build updates object with only provided fields\n const updates: any = {};\n if (args.name !== undefined) {\n // Sanitize name - trim whitespace, limit length\n const sanitizedName = args.name.trim().slice(0, 100);\n if (sanitizedName) {\n updates.name = sanitizedName;\n }\n }\n if (args.image !== undefined) {\n // Validate image URL format\n if (args.image && !isValidUrl(args.image)) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: \"Invalid image URL\",\n });\n }\n updates.image = args.image;\n }\n\n if (Object.keys(updates).length > 0) {\n await ctx.db.patch(userId, updates);\n }\n\n return null;\n },\n});\n\n/**\n * Helper to validate URL format.\n */\nfunction isValidUrl(string: string): boolean {\n try {\n const url = new URL(string);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n", "type": "registry:file", "target": "convex/users.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-nextjs/convex/http.ts", "content": "import { httpRouter } from \"convex/server\";\nimport { auth } from \"./auth\";\n\n/**\n * HTTP router for Convex Auth.\n *\n * This sets up the required HTTP endpoints for authentication:\n * - POST /api/auth/signin - Sign in\n * - POST /api/auth/signout - Sign out\n * - GET /api/auth/signin/* - OAuth callbacks\n * - POST /api/auth/callback/* - OAuth callbacks\n *\n * These routes are automatically configured by auth.addHttpRoutes().\n */\nconst http = httpRouter();\n\n// Add all auth HTTP routes\nauth.addHttpRoutes(http);\n\nexport default http;\n", "type": "registry:file", "target": "convex/http.ts" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/client.ts", "content": "\"use client\";\n\nimport { ConvexReactClient } from \"convex/react\";\n\nconst convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!;\n\nexport const convex = new ConvexReactClient(convexUrl);\n", "type": "registry:lib", "target": "src/lib/convex/client.ts" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/provider.tsx", "content": "\"use client\";\n\nimport { ConvexAuthNextjsProvider } from \"@convex-dev/auth/nextjs\";\nimport { ConvexReactClient } from \"convex/react\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n return (\n \n {children}\n \n );\n}\n", "type": "registry:lib", "target": "src/lib/convex/provider.tsx" }, { "path": "src/registry/convex/clients/nextjs/lib/convex/server.ts", "content": "import {\n fetchAction,\n fetchMutation,\n fetchQuery,\n preloadQuery,\n} from \"convex/nextjs\";\n\nimport { api } from \"@/convex/_generated/api\";\n\nexport { fetchQuery, fetchMutation, fetchAction, preloadQuery, api };\n", "type": "registry:lib", "target": "src/lib/convex/server.ts" }, { "path": "src/registry/convex/clients/nextjs/proxy.ts", "content": "import {\n convexAuthNextjsMiddleware,\n createRouteMatcher,\n} from \"@convex-dev/auth/nextjs/server\";\n\nconst isProtectedRoute = createRouteMatcher([\"/protected(.*)\"]);\n\n// Named export for Next.js 16+ (proxy.ts)\nexport const proxy = convexAuthNextjsMiddleware((request, { convexAuth }) => {\n if (isProtectedRoute(request) && !convexAuth.isAuthenticated()) {\n return Response.redirect(new URL(\"/auth/login\", request.url));\n }\n});\n\nexport const config = {\n matcher: [\"/((?!.*\\\\..*|_next).*)\", \"/\", \"/(api|trpc)(.*)\"],\n};\n", "type": "registry:lib", "target": "src/proxy.ts" } ], "envVars": { "CONVEX_DEPLOYMENT": "", "NEXT_PUBLIC_CONVEX_URL": "", "AUTH_RESEND_KEY": "", "AUTH_EMAIL_FROM": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env.local` file as `NEXT_PUBLIC_CONVEX_URL`.\n\n3. Set environment variables in the Convex dashboard (Settings > Environment Variables):\n - `AUTH_RESEND_KEY` - Your Resend API key (required for email verification/reset)\n - `AUTH_EMAIL_FROM` - Sender email address (optional, defaults to onboarding@resend.dev)\n\n4. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.\n\n## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env.local` file as `NEXT_PUBLIC_CONVEX_URL`.\n\n3. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.", "type": "registry:block" }