{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-based-auth-tanstack", "title": "Password-Based Authentication (TanStack)", "description": "Complete password-based authentication flow for TanStack Start with Convex Auth. Includes login, sign-up, password reset, protected routes, and full Convex backend.", "dependencies": [ "lucide-react", "@convex-dev/auth@latest", "convex@latest", "@tanstack/react-router", "@auth/core", "@convex-dev/react-query@latest", "@tanstack/react-query@latest" ], "registryDependencies": [ "button", "input", "label", "card", "alert", "avatar", "skeleton" ], "files": [ { "path": "src/registry/convex/blocks/password-based-auth-tanstack/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 { useState } from \"react\";\nimport { Link, useNavigate } from \"@tanstack/react-router\";\n\nexport function LoginForm() {\n const { signIn } = useAuthActions();\n const navigate = useNavigate();\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 navigate({ to: \"/protected\" });\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 className=\"h-11\"\n />\n
\n
\n
\n \n \n Forgot password?\n \n
\n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n
\n
\n \n
\n
\n or\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": "components/login-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/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 { useState } from \"react\";\nimport { Link, useNavigate } from \"@tanstack/react-router\";\n\nexport function SignUpForm() {\n const { signIn } = useAuthActions();\n const navigate = useNavigate();\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 navigate({ to: \"/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 className=\"h-11\"\n />\n
\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n
\n
\n \n
\n
\n or\n
\n
\n

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

\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/sign-up-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/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\";\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, Mail } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Link, useNavigate } from \"@tanstack/react-router\";\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 navigate = useNavigate();\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 \n Check your email\n \n \n We've sent a password reset code to {email}\n \n
\n \n

\n Enter the 8-digit code from your email to reset your password.\n

\n
\n \n \n navigate({\n to: \"/auth/update-password\",\n search: { email },\n })\n }\n >\n Enter reset code\n \n \n \n
\n );\n }\n\n return (\n \n \n Forgot password?\n \n Enter your email and we'll send you a reset code\n \n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n \n \n
\n
\n );\n}\n", "type": "registry:component", "target": "components/forgot-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/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\";\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, CheckCircle, Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { Link, useSearch } from \"@tanstack/react-router\";\n\n/**\n * UpdatePasswordForm component for completing password reset.\n *\n * Supports OTP code flow per Convex Auth docs.\n * User enters the 8-digit code received via email + new password.\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 search = useSearch({ strict: false }) as { email?: string };\n const emailParam = search.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 [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\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 setSuccess(true);\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 if (success) {\n return (\n \n \n
\n \n
\n \n Password updated\n \n \n Your password has been successfully updated\n \n
\n \n \n \n
\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 className=\"h-11\"\n />\n
\n )}\n
\n \n setCode(e.target.value)}\n required\n disabled={loading}\n maxLength={8}\n pattern=\"[0-9]{8}\"\n className=\"h-11\"\n />\n
\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n \n \n
\n
\n );\n}\n", "type": "registry:component", "target": "components/update-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/components/logout-button.tsx", "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { LogOut, Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { useNavigate } from \"@tanstack/react-router\";\n\ninterface LogoutButtonProps {\n variant?:\n | \"default\"\n | \"destructive\"\n | \"outline-solid\"\n | \"secondary\"\n | \"ghost\"\n | \"link\";\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n className?: string;\n showIcon?: boolean;\n children?: React.ReactNode;\n}\n\nexport function LogoutButton({\n variant = \"outline\",\n size = \"default\",\n className,\n showIcon = true,\n children,\n}: LogoutButtonProps) {\n const { signOut } = useAuthActions();\n const navigate = useNavigate();\n const [loading, setLoading] = useState(false);\n\n const handleLogout = async () => {\n setLoading(true);\n try {\n await signOut();\n navigate({ to: \"/auth/login\" });\n } catch (error) {\n console.error(\"Failed to sign out:\", error);\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n {loading ? (\n \n ) : (\n <>\n {showIcon && }\n {children || \"Sign out\"}\n \n )}\n \n );\n}\n", "type": "registry:component", "target": "components/logout-button.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/auth/login.tsx", "content": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { LoginForm } from \"../../components/login-form\";\n\nexport const Route = createFileRoute(\"/auth/login\")({\n component: LoginPage,\n});\n\nfunction LoginPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "app/routes/auth/login.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/auth/sign-up.tsx", "content": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { SignUpForm } from \"../../components/sign-up-form\";\n\nexport const Route = createFileRoute(\"/auth/sign-up\")({\n component: SignUpPage,\n});\n\nfunction SignUpPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "app/routes/auth/sign-up.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/auth/forgot-password.tsx", "content": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { ForgotPasswordForm } from \"../../components/forgot-password-form\";\n\nexport const Route = createFileRoute(\"/auth/forgot-password\")({\n component: ForgotPasswordPage,\n});\n\nfunction ForgotPasswordPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "app/routes/auth/forgot-password.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/auth/update-password.tsx", "content": "import { createFileRoute } from \"@tanstack/react-router\";\nimport { UpdatePasswordForm } from \"../../components/update-password-form\";\n\nexport const Route = createFileRoute(\"/auth/update-password\")({\n component: UpdatePasswordPage,\n validateSearch: (search: Record) => {\n return {\n token: search.token as string | undefined,\n };\n },\n});\n\nfunction UpdatePasswordPage() {\n return (\n
\n \n
\n );\n}\n", "type": "registry:page", "target": "app/routes/auth/update-password.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/auth/sign-up-success.tsx", "content": "import { createFileRoute } from \"@tanstack/react-router\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { CheckCircle } from \"lucide-react\";\nimport { Link } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/auth/sign-up-success\")({\n component: SignUpSuccessPage,\n});\n\nfunction SignUpSuccessPage() {\n return (\n
\n \n \n
\n \n
\n \n Account created!\n \n \n Your account has been successfully created\n \n
\n \n

\n You can now sign in with your email and password.\n

\n
\n \n \n \n
\n
\n );\n}\n", "type": "registry:page", "target": "app/routes/auth/sign-up-success.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/routes/_protected/index.tsx", "content": "import { createFileRoute, redirect } from \"@tanstack/react-router\";\nimport { useConvexAuth, useQuery } from \"convex/react\";\nimport { api } from \"../../convex/_generated/api\";\nimport { LogoutButton } from \"../../components/logout-button\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nexport const Route = createFileRoute(\"/_protected/\")({\n component: ProtectedPage,\n});\n\nfunction ProtectedPage() {\n const { isAuthenticated, isLoading: authLoading } = useConvexAuth();\n const user = useQuery(api.users.current);\n\n if (authLoading) {\n return (\n
\n \n \n
\n \n
\n \n \n
\n
\n
\n
\n
\n );\n }\n\n if (!isAuthenticated) {\n throw redirect({ to: \"/auth/login\" });\n }\n\n return (\n
\n \n \n
\n \n \n \n {user?.name?.charAt(0)?.toUpperCase() ||\n user?.email?.charAt(0)?.toUpperCase() ||\n \"U\"}\n \n \n
\n \n {user?.name || \"Welcome!\"}\n \n {user?.email}\n
\n
\n
\n \n

\n You're signed in and viewing a protected page. Only authenticated\n users can see this content.\n

\n \n
\n
\n
\n );\n}\n", "type": "registry:page", "target": "app/routes/_protected/index.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-tanstack/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-tanstack/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-tanstack/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-tanstack/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-tanstack/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/tanstack/lib/convex/client.ts", "content": "import { ConvexReactClient } from \"convex/react\";\n\nconst convexUrl = (import.meta as any).env.VITE_CONVEX_URL as string;\n\nexport const convex = new ConvexReactClient(convexUrl);\n", "type": "registry:lib", "target": "lib/convex/client.ts" }, { "path": "src/registry/convex/clients/tanstack/lib/convex/provider.tsx", "content": "import { ConvexAuthProvider } from \"@convex-dev/auth/react\";\nimport { ConvexReactClient } from \"convex/react\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(\n (import.meta as any).env.VITE_CONVEX_URL as string,\n);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n return {children};\n}\n", "type": "registry:lib", "target": "lib/convex/provider.tsx" }, { "path": "src/registry/convex/clients/tanstack/lib/convex/server.ts", "content": "import { convexQuery, useConvexMutation } from \"@convex-dev/react-query\";\nimport { api } from \"@/convex/_generated/api\";\n\nexport { convexQuery, useConvexMutation, api };\n", "type": "registry:lib", "target": "lib/convex/server.ts" } ], "envVars": { "CONVEX_DEPLOYMENT": "", "VITE_CONVEX_URL": "", "AUTH_RESEND_KEY": "", "AUTH_EMAIL_FROM": "", "AUTH_GITHUB_ID": "", "AUTH_GITHUB_SECRET": "", "AUTH_GOOGLE_ID": "", "AUTH_GOOGLE_SECRET": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. 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 - `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` - GitHub OAuth credentials (optional)\n - `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` - Google OAuth credentials (optional)\n\n3. For OAuth providers, configure callback URLs in their dashboards:\n - GitHub: `https:///api/auth/callback/github`\n - Google: `https:///api/auth/callback/google`\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` file as `VITE_CONVEX_URL`.\n\n3. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.", "type": "registry:block" }