Email + password
The email + password sign-in method implements password-based login where each account is identified by a validated email address. It handles:
- Sign-up with email validation: the account is created immediately, but the user can sign in only after they open the validation link.
- Sign-in.
- Change password and change email, which require the current password and send a security notification to the affected address.
- Password reset through a reset link received by email.
A validation link works only in the browser (or app) that started the flow: the client keeps a secret in its storage, and completion requires both that secret and the code from the link.
Emails are sent through the
@convex-dev/resend component.
Setting up email + password login in your app
- Follow the “Getting Started” guide
First, install Convex Auth and set up the core as described in the Getting Started guide.
- Set up Resend
This method uses Resend to send emails for account validation, password recovery, and security notifications.
You will need a domain to send these emails from.
Before going further:
Then connect Resend to your Convex deployment:
npm install @convex-dev/resendnpx convex env set RESEND_API_KEY # Press Enter then enter your Resend API keynpx convex env set SENDER_EMAIL no-reply@my-domain.example # Replace this with an email address in your verified domainnpx convex env set SENDER_NAME "My App" # Optional: replace this with the sender name of your emails - Register the components
Register the email component, the password component, and the Resend component alongside the core in
convex/convex.config.ts.convex/convex.config.tsimport { defineApp } from "convex/server";import { v } from "convex/values";import auth from "@convex-dev/auth/convex.config.js";import authPassword from "@convex-dev/auth/components/password/convex.config.js";import authEmail from "@convex-dev/auth/components/email/convex.config.js";import resend from "@convex-dev/resend/convex.config.js";const app = defineApp({env: {RESEND_API_KEY: v.string(),SENDER_EMAIL: v.string(),// Defaults in auth.ts.SENDER_NAME: v.optional(v.string()),SITE_URL: v.optional(v.string()),},});app.use(auth);app.use(authPassword);app.use(authEmail);app.use(resend);export default app; - Add a
userstable to the schemaAdd a
userstable to your schema inconvex/schema.ts. You don’t need to add any fields to it.convex/schema.tsimport { defineSchema, defineTable } from "convex/server";export default defineSchema({users: defineTable({}),}); - Set up user creation
Write a
createUsermutation that creates a row in youruserstable and returns its id. This function will be used every time someone creates a new account.convex/users.tsimport { internalMutation } from "./_generated/server";import { v } from "convex/values";export const createUser = internalMutation({args: {provider: v.object({name: v.literal("emailPassword"),accountId: v.string(),profile: v.object({}),}),},returns: v.id("users"),handler: async (ctx) => {return await ctx.db.insert("users", {});},}); - Set up the public backend functions
Set up the
emailPasswordmethod withsetupEmailPasswordand export the mutations it hands back once you attach the callback that creates your user rows, inconvex/auth.ts:convex/auth.tsimport { components, internal } from "./_generated/api";import { env } from "./_generated/server";import { setupConvexAuth } from "@convex-dev/auth/server";import { setupEmailPassword } from "@convex-dev/auth/methods/email-password/server";// The frontend origin the emailed links point at. Set SITE_URL on the// deployment when the frontend does not run on the Vite default.const SITE_URL = env.SITE_URL ?? "http://localhost:5173";const auth = setupConvexAuth({ component: components.auth });export const { signOut, refreshSession, isAuthenticated } = auth;export const {signUpWithEmailPassword,completeSignUpWithEmailPassword,signInWithEmailPassword,changePassword,startChangeEmail,completeChangeEmail,startPasswordRecovery,checkPasswordRecovery,completePasswordRecovery,} = setupEmailPassword(auth, {component: components.authEmail,passwordComponent: components.authPassword,emailSender: {kind: "resend",sendEmail: components.resend.lib.sendEmail,apiKey: env.RESEND_API_KEY,// SENDER_EMAIL must be on a domain you verified with Resend, or Resend's// onboarding sender (onboarding@resend.dev), which only delivers to the// email address of your own Resend account.from: `${env.SENDER_NAME ?? "My App"} <${env.SENDER_EMAIL}>`,},urls: {signUp: `${SITE_URL}/validate-email`,changeEmail: `${SITE_URL}/confirm-email-change`,recovery: `${SITE_URL}/reset-password`,},}).attachUserCallbacks({createUser: internal.users.createUser,}); - Set up the provider on the client
Wrap your app in
ConvexAuthProvider, passing it therefreshSessionandsignOutmutations and theisAuthenticatedquery insrc/main.tsx:src/main.tsximport { ConvexAuthProvider } from "@convex-dev/auth/react";import { ConvexReactClient } from "convex/react";import { StrictMode } from "react";import { createRoot } from "react-dom/client";import { BrowserRouter } from "react-router-dom";import { api } from "../convex/_generated/api";import { App } from "./App";const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL!);createRoot(document.getElementById("root")!).render(<StrictMode><ConvexAuthProviderclient={convex}api={{refreshSession: api.auth.refreshSession,signOut: api.auth.signOut,isAuthenticated: api.auth.isAuthenticated,}}><BrowserRouter><App /></BrowserRouter></ConvexAuthProvider></StrictMode>,); - Set up the sign-up page
In your front-end, set up a route for each flow, starting with creating an account. Copy the following components into your app. Feel free to customize them, for instance to use your app’s design system.
src/routes/signUp.tsximport { useSignUpWithEmailPassword } from "@convex-dev/auth/methods/email-password/react";import { useState } from "react";import { api } from "../../convex/_generated/api";export function SignUp() {const { signUp, pending } = useSignUpWithEmailPassword(api.auth);const [email, setEmail] = useState("");const [password, setPassword] = useState("");const [error, setError] = useState<string | null>(null);const [sentTo, setSentTo] = useState<string | null>(null);if (sentTo !== null) {return (<><h1>Validate your email</h1><p>We sent a link to <strong>{sentTo}</strong>. Open it to validate youraddress and sign in.</p><p>Open the link in <strong>this browser</strong>: the link only works inthe browser you signed up from.</p></>);}return (<><formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await signUp({ email, password });if (result.success) {setSentTo(email);return;}setError(() => {switch (result.userError.error) {case "INVALID_EMAIL":return "That email address doesn't look valid.";case "EMAIL_TAKEN":return "An account already exists with that email address.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(result.userError.retryAfterMs / 1000)} seconds.`;case "PASSWORD_TOO_SHORT":return `Password must be at least ${result.userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Password must be at most ${result.userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Password can't start or end with whitespace.";case "PASSWORD_TOO_COMMON":return "This password is one of the most commonly used passwords. Please choose a different one.";case "OTHER_ERROR":// The mutation threw unexpectedly; the original error is// available on `cause` if you want to log or inspect it.console.error("Sign-up failed:", result.userError.cause);return "Something went wrong. Please try again.";default:result.userError satisfies never;console.error("Sign-up failed:", result.userError);return "Something went wrong. Please try again.";}});}}><h1>Sign up</h1><label>Email<inputtype="email"value={email}onChange={(e) => setEmail(e.target.value)}// "username", not "email" (best practice for password manager support)autoComplete="username"requireddisabled={pending}/></label><label>Password<inputtype="password"value={password}onChange={(e) => setPassword(e.target.value)}autoComplete="new-password"requireddisabled={pending}/></label>{error ? (<p role="alert"><strong>{error}</strong></p>) : null}<button type="submit" disabled={pending}>{pending ? "Creating account…" : "Create account"}</button></form><p>Already have an account? <a href="/login">Log in</a></p></>);} - Set up the email validation page
Set up the landing page for the link in the sign-up email. It must not require authentication.
src/routes/validateEmail.tsximport { useCompleteSignUpWithEmailPassword } from "@convex-dev/auth/methods/email-password/react";import type { ReactNode } from "react";import { useSearchParams } from "react-router-dom";import { api } from "../../convex/_generated/api";/*** Landing page for the sign-up challenge link (`/validate-email?code=…`).* The hook completes the validation as soon as the page opens, which also* signs the user in; the page shows the outcome.*/export function ValidateEmail() {const [params] = useSearchParams();const emailCode = params.get("code");if (emailCode === null) {return <p>This link is incomplete. Use the link from your email.</p>;}return <ValidateEmailWithCode emailCode={emailCode} />;}function ValidateEmailWithCode({ emailCode }: { emailCode: string }) {const state = useCompleteSignUpWithEmailPassword(api.auth, { emailCode });if (state.status === "loading") {return <p>Validating your email…</p>;}if (state.status === "complete") {return (<><h1>Email validated</h1><p>Your email address is validated and you are signed in.{" "}<a href="/">Go to the dashboard</a>.</p></>);}switch (state.userError.error) {case "MISSING_SECRET":return (<><h1>Open this link in the browser you signed up from</h1><p>For your security, the validation link only works in the browserwhere the sign-up started. Open the link there, or sign up again inthis browser.</p></>);case "INVALID_CHALLENGE":return (<ValidateEmailError>This link is no longer valid. It may have expired or already beenused.</ValidateEmailError>);case "INCORRECT_CODE":return (<ValidateEmailError>This is not the latest link. Open the newest email we sent you.</ValidateEmailError>);case "EMAIL_TAKEN":return (<ValidateEmailError>This email address is already in use. Sign in instead.</ValidateEmailError>);case "OTHER_ERROR":console.error("Validation failed:", state.userError.cause);return (<ValidateEmailError>Something went wrong. Reload the page to try again.</ValidateEmailError>);default:state.userError satisfies never;console.error("Validation failed:", state.userError);return (<ValidateEmailError>Something went wrong. Reload the page to try again.</ValidateEmailError>);}}function ValidateEmailError({ children }: { children: ReactNode }) {return (<><h1>Could not validate your email</h1><p role="alert"><strong>{children}</strong></p><p><a href="/signup">Sign up again</a> to receive a new link.</p></>);} - Set up the log-in page
Set up a route for logging in:
src/routes/logIn.tsximport { useSignInWithEmailPassword } from "@convex-dev/auth/methods/email-password/react";import { useState } from "react";import { api } from "../../convex/_generated/api";export function LogIn() {const { signIn, pending } = useSignInWithEmailPassword(api.auth);const [email, setEmail] = useState("");const [password, setPassword] = useState("");const [error, setError] = useState<string | null>(null);return (<><formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await signIn({ email, password });if (result.status === "complete") {return;}setError(() => {switch (result.userError.error) {case "USER_NOT_FOUND":return "No account exists with that email address. Make sure you validated your email.";case "INVALID_CREDENTIALS":return "Incorrect email or password.";case "PASSWORD_TOO_SHORT":return `Password must be at least ${result.userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Password must be at most ${result.userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Password can't start or end with whitespace.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(result.userError.retryAfterMs / 1000)} seconds.`;case "OTHER_ERROR":console.error("Sign-in failed:", result.userError.cause);return "Something went wrong. Please try again.";default:result.userError satisfies never;console.error("Sign-in failed:", result.userError);return "Something went wrong. Please try again.";}});}}><h1>Log in</h1><label>Email<inputtype="email"value={email}onChange={(e) => setEmail(e.target.value)}// "username", not "email" (best practice for password manager support)autoComplete="username"requireddisabled={pending}/></label><label>Password<inputtype="password"value={password}onChange={(e) => setPassword(e.target.value)}autoComplete="current-password"requireddisabled={pending}/></label>{error ? (<p role="alert"><strong>{error}</strong></p>) : null}<button type="submit" disabled={pending}>{pending ? "Logging in…" : "Log in"}</button></form><p>Don't have an account? <a href="/signup">Sign up</a></p><p>Forgot your password? <a href="/forgot-password">Reset it</a></p></>);} - Set up the change-password form
Let signed-in users change their password:
src/ChangePasswordForm.tsximport { useMutation } from "convex/react";import { useState } from "react";import { api } from "../convex/_generated/api";export function ChangePasswordForm({currentEmail,}: {currentEmail: string | undefined;}) {const changePassword = useMutation(api.auth.changePassword);const [pending, setPending] = useState(false);const [currentPasswordField, setCurrentPasswordField] = useState("");const [newPasswordField, setNewPasswordField] = useState("");const [signOutOtherSessions, setSignOutOtherSessions] = useState(false);const [message, setMessage] = useState<string | null>(null);return (<formonSubmit={async (e) => {e.preventDefault();setMessage(null);setPending(true);try {const result = await changePassword({currentPassword: currentPasswordField,newPassword: newPasswordField,signOutOtherSessions,});if (result.success) {setCurrentPasswordField("");setNewPasswordField("");setMessage("Your password was changed.");return;}setMessage(() => {switch (result.userError.error) {case "NOT_LOGGED_IN":return "You are not logged in.";case "INVALID_CREDENTIALS":return "The current password is incorrect.";case "PASSWORD_TOO_SHORT":return `Password must be at least ${result.userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Password must be at most ${result.userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Password can't start or end with whitespace.";case "PASSWORD_TOO_COMMON":return "This password is one of the most commonly used passwords. Please choose a different one.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(result.userError.retryAfterMs / 1000)} seconds.`;default:result.userError satisfies never;console.error("Password change failed:", result.userError);return "Something went wrong. Please try again.";}});} catch (error) {console.error("Change password failed:", error);setMessage("Something went wrong. Please try again.");} finally {setPending(false);}}}><h2>Change password</h2>{/* Hidden email, so that password managers know which login to update */}<inputtype="email"value={currentEmail ?? ""}// "username", not "email" (best practice for password manager support)autoComplete="username"readOnlyhidden/><label>Current password<inputtype="password"value={currentPasswordField}onChange={(e) => setCurrentPasswordField(e.target.value)}autoComplete="current-password"requireddisabled={pending}/></label><label>New password<inputtype="password"value={newPasswordField}onChange={(e) => setNewPasswordField(e.target.value)}autoComplete="new-password"requireddisabled={pending}/></label><label><inputtype="checkbox"checked={signOutOtherSessions}onChange={(e) => setSignOutOtherSessions(e.target.checked)}disabled={pending}/>Sign out of all other devices</label>{message ? (<p role="alert"><strong>{message}</strong></p>) : null}<button type="submit" disabled={pending}>{pending ? "Changing…" : "Change password"}</button></form>);} - Set up the change-email form
Let signed-in users change their email address:
src/ChangeEmailForm.tsximport { useStartChangeEmail } from "@convex-dev/auth/methods/email-password/react";import { useState } from "react";import { api } from "../convex/_generated/api";export function ChangeEmailForm({currentEmail,}: {currentEmail: string | undefined;}) {const { startChangeEmail, pending } = useStartChangeEmail(api.auth);const [newEmailField, setNewEmailField] = useState("");const [currentPasswordField, setCurrentPasswordField] = useState("");const [message, setMessage] = useState<string | null>(null);return (<formonSubmit={async (e) => {e.preventDefault();setMessage(null);const result = await startChangeEmail({newEmail: newEmailField,currentPassword: currentPasswordField,});if (result.success) {setMessage(`We sent a link to ${newEmailField}. Open it in this browser to ` +"confirm the change.",);return;}setMessage(() => {switch (result.userError.error) {case "NOT_LOGGED_IN":return "You are not logged in.";case "INVALID_CREDENTIALS":return "The current password is incorrect.";case "INVALID_EMAIL":return "That email address doesn't look valid.";case "EMAIL_TAKEN":return "An account already exists with that email address.";case "PASSWORD_TOO_SHORT":return `Password must be at least ${result.userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Password must be at most ${result.userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Password can't start or end with whitespace.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(result.userError.retryAfterMs / 1000)} seconds.`;case "OTHER_ERROR":console.error("Change email failed:", result.userError.cause);return "Something went wrong. Please try again.";default:result.userError satisfies never;console.error("Email change failed:", result.userError);return "Something went wrong. Please try again.";}});}}><h2>Change email</h2>{/* Hidden email, so that password managers know which account to update */}<inputtype="hidden"name="username"value={currentEmail ?? ""}autoComplete="username"/><label htmlFor="new-email">New email</label><inputid="new-email"name="new-email"type="email"value={newEmailField}onChange={(e) => setNewEmailField(e.target.value)}autoComplete="off"requireddisabled={pending}/><label htmlFor="current-password">Current password</label><inputid="current-password"name="current-password"type="password"value={currentPasswordField}onChange={(e) => setCurrentPasswordField(e.target.value)}autoComplete="current-password"requireddisabled={pending}/>{message ? (<p role="alert"><strong>{message}</strong></p>) : null}<button type="submit" disabled={pending}>{pending ? "Sending…" : "Send confirmation link"}</button></form>);} - Set up the email change confirmation page
Set up the landing page for the link in the change-email email. It must not require authentication.
src/routes/confirmEmailChange.tsximport { useCompleteChangeEmail } from "@convex-dev/auth/methods/email-password/react";import { useSearchParams } from "react-router-dom";import { api } from "../../convex/_generated/api";/*** Landing page for the change-email confirmation link* (`/confirm-email-change?code=…`). The hook completes the change as soon as* the page opens; the page shows the outcome. No session is minted: the user* already has one.*/export function ConfirmEmailChange() {const [params] = useSearchParams();const emailCode = params.get("code");if (emailCode === null) {return <p>This link is incomplete. Use the link from your email.</p>;}return <ConfirmEmailChangeWithCode emailCode={emailCode} />;}function ConfirmEmailChangeWithCode({ emailCode }: { emailCode: string }) {const state = useCompleteChangeEmail(api.auth, {emailCode,});if (state.status === "loading") {return <p>Confirming your new email…</p>;}if (state.status === "complete") {return (<><h1>Email address changed</h1><p>The new address is now the primary address of your account.{" "}<a href="/">Back to the dashboard</a>.</p></>);}switch (state.userError.error) {case "MISSING_SECRET":return (<><h1>Open this link in the browser you started from</h1><p>For your security, the confirmation link only works in the browserwhere the email change started.</p></>);case "INVALID_CHALLENGE":return (<ConfirmEmailChangeError message="This link is no longer valid. It may have expired or already been used." />);case "INCORRECT_CODE":return (<ConfirmEmailChangeError message="This is not the latest link. Open the newest email we sent you." />);case "NOT_LOGGED_IN":return (<ConfirmEmailChangeError message="Log in first, then open the link again." />);case "EMAIL_TAKEN":return (<ConfirmEmailChangeError message="Another account validated this email address first." />);case "WRONG_USER":return (<ConfirmEmailChangeError message="This link is for another account. Log in with the account that started the change, then open the link again." />);case "OTHER_ERROR":console.error("Email change failed:", state.userError.cause);return (<ConfirmEmailChangeError message="Something went wrong. Reload the page to try again." />);default:state.userError satisfies never;console.error("Email change failed:", state.userError);return (<ConfirmEmailChangeError message="Something went wrong. Reload the page to try again." />);}}function ConfirmEmailChangeError({ message }: { message: string }) {return (<><h1>Could not change your email</h1><p role="alert"><strong>{message}</strong></p><p>Start the email change again from the <a href="/">dashboard</a>.</p></>);} - Set up the password reset request page
Let users who forgot their password request a reset link:
src/routes/requestReset.tsximport { useStartPasswordRecovery } from "@convex-dev/auth/methods/email-password/react";import { useState } from "react";import { api } from "../../convex/_generated/api";/** The `/forgot-password` page: send a password-reset link. */export function RequestReset() {const { startPasswordRecovery, pending } = useStartPasswordRecovery(api.auth);const [emailField, setEmailField] = useState("");const [error, setError] = useState<string | null>(null);const [sentTo, setSentTo] = useState<string | null>(null);if (sentTo !== null) {return (<><h1>Check your email</h1><p>We sent a password-reset link to <strong>{sentTo}</strong>. The linkstops working after 10 minutes.</p><p>Open the link in <strong>this browser</strong>: the link only works inthe browser you requested it from.</p></>);}return (<><formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await startPasswordRecovery({ email: emailField });if (result.success) {setSentTo(result.sentTo);return;}setError(() => {switch (result.userError.error) {case "INVALID_EMAIL":return "That email address doesn't look valid.";case "EMAIL_NOT_FOUND":return "No account exists with that email address.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(result.userError.retryAfterMs / 1000)} seconds.`;case "OTHER_ERROR":console.error("Reset request failed:", result.userError.cause);return "Something went wrong. Please try again.";default:result.userError satisfies never;console.error("Password reset request failed:",result.userError,);return "Something went wrong. Please try again.";}});}}><h1>Reset your password</h1><label>Email<inputtype="email"value={emailField}onChange={(e) => setEmailField(e.target.value)}autoComplete="username"requireddisabled={pending}/></label>{error ? (<p role="alert"><strong>{error}</strong></p>) : null}<button type="submit" disabled={pending}>{pending ? "Sending…" : "Send reset link"}</button></form><p>Remembered your password? <a href="/login">Log in</a></p></>);} - Set up the password reset page
Set up the landing page for the link in the password reset email. It must not require authentication.
src/routes/resetPassword.tsximport { useCompletePasswordRecovery } from "@convex-dev/auth/methods/email-password/react";import { useState, type ReactNode } from "react";import { Navigate, useSearchParams } from "react-router-dom";import { api } from "../../convex/_generated/api";/*** Landing page for the password-reset link (`/reset-password?code=…`).* Checks the link when the page opens, then asks for the new password and* signs the user in on success. A link that cannot be used shows an error* instead of the form.*/export function ResetPassword() {const [params] = useSearchParams();const emailCode = params.get("code");if (emailCode === null) {return <p>This link is incomplete. Use the link from your email.</p>;}return <ResetPasswordWithCode emailCode={emailCode} />;}function ResetPasswordWithCode({ emailCode }: { emailCode: string }) {const state = useCompletePasswordRecovery(api.auth, { emailCode });const [newPasswordField, setNewPasswordField] = useState("");const [error, setError] = useState<string | null>(null);switch (state.status) {case "loading":return <p>Checking the link…</p>;case "complete":return <Navigate to="/" replace />;case "error":switch (state.userError.error) {case "MISSING_SECRET":return (<><h1>Open this link in the browser you started from</h1><p>For your security, the reset link only works in the browserwhere the reset was requested. Open the link there, or{" "}<a href="/forgot-password">request a new link</a> in thisbrowser.</p></>);case "INVALID_CHALLENGE":return (<ResetPasswordError>This link is no longer valid. It may have expired (reset linksstop working after 10 minutes) or already been used.</ResetPasswordError>);case "INCORRECT_CODE":return (<ResetPasswordError>This is not the latest link. Open the newest email we sent you.</ResetPasswordError>);default:state.userError satisfies never;console.error("Password reset failed:", state.userError);return (<ResetPasswordError>Something went wrong. Reload the page to try again.</ResetPasswordError>);}case "ready":break;}return (<formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await state.completePasswordRecovery({newPassword: newPasswordField,});if (result.status === "complete") {return;}setError(() => {switch (result.userError.error) {case "INVALID_CHALLENGE":case "INCORRECT_CODE":// The hook moves to its error state; the page re-renders.return null;case "PASSWORD_TOO_SHORT":return `Password must be at least ${result.userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Password must be at most ${result.userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Password can't start or end with whitespace.";case "PASSWORD_TOO_COMMON":return "This password is one of the most commonly used passwords. Please choose a different one.";case "OTHER_ERROR":console.error("Password reset failed:", result.userError.cause);return "Something went wrong. Please try again.";default:result.userError satisfies never;console.error("Password reset failed:", result.userError);return "Something went wrong. Please try again.";}});}}><h1>Choose a new password</h1><label>New password<inputtype="password"value={newPasswordField}onChange={(e) => setNewPasswordField(e.target.value)}autoComplete="new-password"requireddisabled={state.pending}/></label>{error ? (<p role="alert"><strong>{error}</strong></p>) : null}<button type="submit" disabled={state.pending}>{state.pending ? "Resetting…" : "Reset password and sign in"}</button></form>);}function ResetPasswordError({ children }: { children: ReactNode }) {return (<><h1>Could not reset your password</h1><p role="alert"><strong>{children}</strong></p><p><a href="/forgot-password">Request a new link</a>.</p></>);}
Rate limits
This method rate-limits the flows that send emails or tell whether an address
has an account. When a limit is reached, the call returns a RATE_LIMITED
error with retryAfterMs, the number of milliseconds until the next attempt can
succeed. Show it to the user, as logIn.tsx does above.
Each limit is a token bucket: the user can make as many attempts as the capacity at once, then the bucket refills at a steady rate.
| Limit | Applies to | Capacity | Refill |
|---|---|---|---|
| Emails per destination address | Sign-up validation, change-email confirmation, and password reset links sent to one address | 5 | 1 every 12 min |
| Emails per client IP | The same emails, sent from one client IP to any address | 20 | 1 every 3 min |
| Account lookups per client IP | Sign-in, starting a password reset, and starting a sign-up or an email change (which tells whether the address is taken) | 60 | 1 every minute |
| Password attempts per user | Sign-in, change password, and change email (which check the current password) | 5 | 1 every minute |
The limit per destination address is shared by all of the kinds of emails. For instance, a user who requested 5 password reset links in a row must wait about 12 minutes before they can request a link to change their email to that address. Security notifications, such as the one sent after a password change, don't count toward these limits.
These values aren't configurable yet.