Username + password
The password component lets you implement password-based login. It automatically handles subtle security details of password handling, including hashing (with Argon2id), rate limiting, and validation.
Setting up username + 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.
- Install the components
Register the password component and the username component alongside the core in
convex/convex.config.ts. The username component stores which user each username identifies:convex/convex.config.tsimport { defineApp } from "convex/server";import auth from "@convex-dev/auth/convex.config.js";import authPassword from "@convex-dev/auth/components/password/convex.config.js";import authUsername from "@convex-dev/auth/components/username/convex.config.js";const app = defineApp();app.use(auth);app.use(authPassword);app.use(authUsername);export default app; - Add a
userstable to the schemaAdd an empty
userstable to your schema inconvex/schema.ts. The username component keeps the username, so the table does not need a field for it.Convex Auth will create a row for each account. You can also add application-specific fields to this table if necessary.
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. The method calls it the first time it sees an account.You can also attach an optional
onSignInmutation, called with the resolveduserIdon every sign-in, the first one included. Anything you want to happen each time someone signs in belongs there, socreateUseronly has to handle what is true at creation time. Leave it out if there is nothing to do.convex/users.tsimport { internalMutation } from "./_generated/server";import { v } from "convex/values";/*** Create the user row for a new password account and return its id. This* example keeps no data in the row, but your app can put a profile here.*/export const createUser = internalMutation({args: {provider: v.object({name: v.literal("password"),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
usernamePasswordmethod withsetupUsernamePasswordand export the sign-up and sign-in 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 { setupConvexAuth } from "@convex-dev/auth/server";import { setupUsernamePassword } from "@convex-dev/auth/methods/username-password/server";const auth = setupConvexAuth({ component: components.auth });export const { signOut, refreshSession, isAuthenticated } = auth;export const {signUpWithUsernamePassword,signInWithUsernamePassword,changePassword,} = setupUsernamePassword(auth, {component: components.authPassword,usernameComponent: components.authUsername,}).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 creating an account. Copy the following component into your app:
src/routes/signUp.tsximport { useSignUpWithUsernamePassword } from "@convex-dev/auth/methods/username-password/react";import { DEFAULT_MIN_PASSWORD_LENGTH } from "@convex-dev/auth/methods/username-password";import { useState } from "react";import { api } from "../../convex/_generated/api";export function SignUp() {const { signUp, pending } = useSignUpWithUsernamePassword(api.auth);const [username, setUsername] = useState("");const [password, setPassword] = useState("");const [error, setError] = useState<string | null>(null);return (<><formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await signUp({ username, password });if (result.status === "complete") {return;}setError(() => {switch (result.userError.error) {case "USERNAME_TAKEN":return "That username is already taken.";case "USERNAME_TOO_SHORT":return `Username must be at least ${result.userError.minimumLength} characters.`;case "USERNAME_TOO_LONG":return `Username must be at most ${result.userError.maximumLength} characters.`;case "USERNAME_HAS_SURROUNDING_WHITESPACE":return "Username can't start or end with whitespace.";case "USERNAME_HAS_INVALID_CHARACTERS":return "Username contains characters that aren't allowed.";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 htmlFor="username">Username<input// Using a static id to help password managers behave correctlyid="username"name="username"type="text"value={username}onChange={(e) => setUsername(e.target.value)}autoComplete="username"requireddisabled={pending}/></label><label htmlFor="password">Password<input// Using a static id to help password managers behave correctlyid="password"name="password"type="password"value={password}onChange={(e) => setPassword(e.target.value)}autoComplete="new-password"minLength={DEFAULT_MIN_PASSWORD_LENGTH}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 log-in page
Then set up a route for logging in:
src/routes/logIn.tsximport { useSignInWithUsernamePassword } from "@convex-dev/auth/methods/username-password/react";import { useState } from "react";import { api } from "../../convex/_generated/api";export function LogIn() {const { signIn, pending } = useSignInWithUsernamePassword(api.auth);const [username, setUsername] = useState("");const [password, setPassword] = useState("");const [error, setError] = useState<string | null>(null);return (<><formonSubmit={async (e) => {e.preventDefault();setError(null);const result = await signIn({ username, password });if (result.status === "complete") {return;}setError(() => {switch (result.userError.error) {case "USER_NOT_FOUND":return "No account exists with that username.";case "INVALID_CREDENTIALS":return "Incorrect username 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":// The mutation threw unexpectedly; the original error is// available on `cause` if you want to log or inspect it.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 htmlFor="username">Username<input// Using a static id to help password managers behave correctlyid="username"name="username"type="text"value={username}onChange={(e) => setUsername(e.target.value)}autoComplete="username"requireddisabled={pending}/></label><label htmlFor="password">Password<input// Using a static id to help password managers behave correctlyid="password"name="password"type="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></>);}
Going further
Allowing users to change their password
It is highly recommended that you implement a feature allowing users in your app to change their password, because a user might need it if they accidentally leak their password.
- Export the
changePasswordfunctionIn
convex/auth.ts, export thechangePasswordmutation next tosignUpWithUsernamePasswordandsignInWithUsernamePassword:convex/auth.tsimport { components, internal } from "./_generated/api";import { setupConvexAuth } from "@convex-dev/auth/server";import { setupUsernamePassword } from "@convex-dev/auth/methods/username-password/server";const auth = setupConvexAuth({ component: components.auth });export const { signOut, refreshSession, isAuthenticated } = auth;export const {signUpWithUsernamePassword,signInWithUsernamePassword,changePassword,} = setupUsernamePassword(auth, {component: components.authPassword,usernameComponent: components.authUsername,}).attachUserCallbacks({ createUser: internal.users.createUser }); - Expose the username of the signed-in user
The change-password form includes a hidden username field, so that password managers know which login to update. Write a query that returns the username of the signed-in user:
convex/currentUser.tsimport { getAuthUserId } from "@convex-dev/auth/server";import { query } from "./_generated/server";import { components } from "./_generated/api";export const loggedInUser = query({args: {},handler: async (ctx) => {const userId = await getAuthUserId(ctx);if (userId === null) {return null;}const user = await ctx.db.get("users", userId);if (user === null) {return null;}const username = await ctx.runQuery(components.authUsername.public.getUsername,{ userId },);if (username === null) {// In this app, every user has a username, so this must not occur.throw new Error(`User ${userId} unexpectedly has no username`);}return { id: user._id, username };},}); - Build the change-password form
Copy the following component into your app, adapt it to your design system, and render it on your settings page.
The form asks users to re-authenticate by entering their current password for security reasons.
src/PasswordSettings.tsximport type { ChangePasswordResult } from "@convex-dev/auth/methods/username-password/server";import { DEFAULT_MIN_PASSWORD_LENGTH } from "@convex-dev/auth/methods/username-password";import { useMutation, useQuery } from "convex/react";import { useState } from "react";import { api } from "../convex/_generated/api";export function PasswordSettings() {const user = useQuery(api.currentUser.loggedInUser);const changePassword = useMutation(api.auth.changePassword);const [currentPassword, setCurrentPassword] = useState("");const [newPassword, setNewPassword] = useState("");const [signOutOtherSessions, setSignOutOtherSessions] = useState(false);const [pending, setPending] = useState(false);const [status, setStatus] = useState<string | null>(null);if (!user) {// Loading, or not signed inreturn null;}return (<formonSubmit={async (e) => {e.preventDefault();setStatus(null);setPending(true);try {const result = await changePassword({currentPassword,newPassword,signOutOtherSessions,});if (result.success) {setCurrentPassword("");setNewPassword("");setStatus("Your password has been changed.");} else {setStatus(errorMessage(result.userError));}} catch (cause) {console.error("Password change failed:", cause);setStatus("Something went wrong. Please try again.");} finally {setPending(false);}}}><h2>Change your password</h2>{/* Hidden username, so that password managers know which login to update */}<input// Using a static id to help password managers behave correctlyid="username"name="username"type="text"value={user.username}autoComplete="username"readOnlystyle={{ display: "none" }}/><label htmlFor="current-password">Current password<input// Using a static id to help password managers behave correctlyid="current-password"name="current-password"type="password"value={currentPassword}onChange={(e) => setCurrentPassword(e.target.value)}autoComplete="current-password"requireddisabled={pending}/></label><label htmlFor="new-password">New password<input// Using a static id to help password managers behave correctlyid="new-password"name="new-password"type="password"value={newPassword}onChange={(e) => setNewPassword(e.target.value)}autoComplete="new-password"minLength={DEFAULT_MIN_PASSWORD_LENGTH}requireddisabled={pending}/></label><label><inputtype="checkbox"checked={signOutOtherSessions}onChange={(e) => setSignOutOtherSessions(e.target.checked)}disabled={pending}/>Sign out of all other devices</label>{status && (<p role="alert"><strong>{status}</strong></p>)}<button type="submit" disabled={pending}>{pending ? "Changing password…" : "Change password"}</button></form>);}function errorMessage(userError: Extract<ChangePasswordResult, { success: false }>["userError"],): string {switch (userError.error) {case "INVALID_CREDENTIALS":return "Incorrect current password.";case "RATE_LIMITED":return `Too many attempts. Try again in ${Math.ceil(userError.retryAfterMs / 1000)} seconds.`;case "PASSWORD_TOO_SHORT":return `Your new password must be at least ${userError.minimumLength} characters.`;case "PASSWORD_TOO_LONG":return `Your new password must be at most ${userError.maximumLength} characters.`;case "PASSWORD_HAS_SURROUNDING_WHITESPACE":return "Your new password can't start or end with whitespace.";case "PASSWORD_TOO_COMMON":return "Your new password is one of the most commonly used passwords. Please choose a different one.";case "NOT_SIGNED_IN":return "Your session has ended. Please log in again.";default:userError satisfies never;return `Unknown error: ${JSON.stringify(userError)}`;}}
Password managers open
/.well-known/change-password
on your website when a user wants to change their password. To provide a good
user experience, we recommend redirecting that path to your settings page.