methods/email-password/server
For AI agents: see llms.txt for the complete documentation index. Markdown versions are available by adding .md to a page URL or requesting Accept: text/markdown.
The emailPassword method: accounts have an email address and a password, and
the email must be validated before the first sign-in.
Type Aliases
ChangePasswordResult
type ChangePasswordResult = Infer<typeof changePasswordResult>;
The result of changePassword.
INVALID_CREDENTIALS and RATE_LIMITED are about the current password. The
other errors are about the new password.
CheckPasswordRecoveryResult
type CheckPasswordRecoveryResult = Infer<typeof checkPasswordRecoveryResult>;
The result of checkPasswordRecovery.
CompleteChangeEmailResult
type CompleteChangeEmailResult = Infer<typeof completeChangeEmailResult>;
The result of completeChangeEmail.
CompletePasswordRecoveryResult
type CompletePasswordRecoveryResult = Infer<
typeof completePasswordRecoveryResult
>;
The result of completePasswordRecovery: the minted session tokens, or an
error.
CompleteSignUpResult
type CompleteSignUpResult = Infer<typeof completeSignUpResult>;
The result of completeSignUpWithEmailPassword: the minted session tokens, or
an error.
EmailPasswordOptions
type EmailPasswordOptions = object;
Options for setupEmailPassword.
Properties
| Property | Type | Description |
|---|---|---|
component | ComponentApi | The mounted email component (components.authEmail). The method uses it to track verified emails and to run the challenges. |
emailSender | EmailSenderOptions | How the method sends emails. |
passwordComponent | PasswordComponentApi | The mounted password component (components.authPassword). The method drives its _internal_dangerous_setPassword / verifyPassword mutations. |
urls | EmailPasswordUrls | The landing pages the emailed links point at. |
EmailPasswordProfile
type EmailPasswordProfile = Record<string, never>;
EmailPasswordUrls
type EmailPasswordUrls = object;
The landing pages the emailed links point at. The link's code query parameter
will be appended to these URLs.
Properties
EmailSenderOptions
type EmailSenderOptions = object;
How the method sends emails: challenge links (through the email component) and security notifications (directly).
Only Resend is supported for now, through the @convex-dev/resend component.
Mount that component in the app and pass its sendEmail reference and your
Resend API key here.
TODO: support other email providers. TODO: offer a first-party zero-configuration email service. TODO: let applications customize the email templates.
Properties
SignInResult
type SignInResult = Infer<typeof signInResult>;
The result of signInWithEmailPassword: the minted session tokens, or an error.
SignUpResult
type SignUpResult = Infer<typeof signUpResult>;
The result of signUpWithEmailPassword. On success, the secret the browser must
keep for completeSignUpWithEmailPassword; the session arrives only after the
email is validated.
StartChangeEmailResult
type StartChangeEmailResult = Infer<typeof startChangeEmailResult>;
The result of startChangeEmail.
StartPasswordRecoveryResult
type StartPasswordRecoveryResult = Infer<typeof startPasswordRecoveryResult>;
The result of startPasswordRecovery.
Functions
setupEmailPassword()
function setupEmailPassword<UsersTable>(core, options): object;
A password sign-in method where accounts have a validated email address and a password.
- Sign-up creates the user immediately but without a session; the user signs in
only after they open the challenge link. Wire it up in
convex/auth.ts:
import { 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.
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,
startPasswordRecovery,
checkPasswordRecovery,
completePasswordRecovery,
startChangeEmail,
completeChangeEmail,
} = setupEmailPassword(auth, {
component: components.authEmail,
passwordComponent: components.authPassword,
emailSender: {
kind: "resend",
sendEmail: components.resend.lib.sendEmail,
apiKey: env.RESEND_API_KEY,
from: "My App <auth@example.com>",
},
urls: {
signUp: `${SITE_URL}/validate-email`,
changeEmail: `${SITE_URL}/confirm-email-change`,
recovery: `${SITE_URL}/reset-password`,
},
}).attachUserCallbacks({
createUser: internal.users.createUserEmailPassword,
});
- Sign-in accepts any verified email of the account.
- Change-password and change-email require the session and the current password (OWASP ASVS v5 6.2.3), and send a security notification to the affected address (ASVS 6.3.7).
- Recovery proves ownership of a verified email through a 10-minute link, then sets the new password and signs the user in.
Account resolution (email → app user id) is owned by the email component; the
password component stores only { userId, passwordHash }.
Type Parameters
| Type Parameter |
|---|
UsersTable extends string |
Parameters
| Parameter | Type |
|---|---|
core | AuthCore<UsersTable> |
options | EmailPasswordOptions |
Returns
attachUserCallbacks()
attachUserCallbacks(__namedParameters): object;
Supply the app's user callbacks (see UserCallbacks for how their args must be declared) and get the functions of this method to export.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | UserCallbacks<"emailPassword", EmailPasswordProfile, UsersTable> |
Returns
changePassword
changePassword: RegisteredMutation<
"public",
{
currentPassword: string;
newPassword: string;
signOutOtherSessions?: boolean;
},
Promise<
| {
success: true;
}
| {
success: false;
userError:
| {
error: "PASSWORD_TOO_SHORT";
minimumLength: number;
}
| {
error: "PASSWORD_TOO_LONG";
maximumLength: number;
}
| {
error: "PASSWORD_HAS_SURROUNDING_WHITESPACE";
}
| {
error: "PASSWORD_TOO_COMMON";
}
| {
error: "NOT_LOGGED_IN";
}
| {
error: "INVALID_CREDENTIALS";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
};
}
>
>;
Change the signed-in user's password. Requires the session and the current
password (OWASP ASVS v5 6.2.3), and notifies the primary email address (ASVS
6.3.7). Signs out the user's other sessions if signOutOtherSessions is true
(ASVS v5 7.4.3).
checkPasswordRecovery
checkPasswordRecovery: RegisteredQuery<
"public",
{
browserSecret: string;
emailCode: string;
},
Promise<
| {
success: true;
}
| {
success: false;
userError:
| {
error: "INVALID_CHALLENGE";
}
| {
error: "INCORRECT_CODE";
};
}
>
>;
Check a password recovery link before the user types a new password: tell
whether completePasswordRecovery would accept the link code + browser secret.
It does not consume the link.
completeChangeEmail
completeChangeEmail: RegisteredMutation<
"public",
{
browserSecret: string;
emailCode: string;
},
Promise<
| {
success: true;
}
| {
success: false;
userError:
| {
error: "EMAIL_TAKEN";
}
| {
error: "INVALID_CHALLENGE";
}
| {
error: "INCORRECT_CODE";
}
| {
error: "NOT_LOGGED_IN";
}
| {
error: "WRONG_USER";
};
}
>
>;
Complete an email change. The old primary address is removed and replaced with the new address. This also notifies old address (OWASP ASVS 6.3.7).
No session is minted — the user already has one.
completePasswordRecovery
completePasswordRecovery: RegisteredMutation<
"public",
{
browserSecret: string;
emailCode: string;
newPassword: string;
},
Promise<
| {
status: "complete";
tokens: {
accessToken: string;
refreshToken: string;
refreshTokenExpiresAt: number;
userId: string;
};
}
| {
status: "error";
userError:
| {
error: "PASSWORD_TOO_SHORT";
minimumLength: number;
}
| {
error: "PASSWORD_TOO_LONG";
maximumLength: number;
}
| {
error: "PASSWORD_HAS_SURROUNDING_WHITESPACE";
}
| {
error: "PASSWORD_TOO_COMMON";
}
| {
error: "INVALID_CHALLENGE";
}
| {
error: "INCORRECT_CODE";
};
}
>
>;
Complete a password recovery: the link code + browser secret prove ownership of the email, so set the new password, sign out every existing session of the user, and sign the user in, in one transaction. Notifies the primary email (ASVS 6.3.7).
completeSignUpWithEmailPassword
completeSignUpWithEmailPassword: RegisteredMutation<
"public",
{
browserSecret: string;
emailCode: string;
},
Promise<
| {
status: "complete";
tokens: {
accessToken: string;
refreshToken: string;
refreshTokenExpiresAt: number;
userId: string;
};
}
| {
status: "error";
userError:
| {
error: "EMAIL_TAKEN";
}
| {
error: "INVALID_CHALLENGE";
}
| {
error: "INCORRECT_CODE";
};
}
>
>;
Complete a sign-up: validate the email with the code from the link and the secret from the starting browser, then sign the user in. Validation and sign-in happen in one transaction.
signInWithEmailPassword
signInWithEmailPassword: RegisteredMutation<
"public",
{
email: string;
password: string;
},
Promise<
| {
status: "complete";
tokens: {
accessToken: string;
refreshToken: string;
refreshTokenExpiresAt: number;
userId: string;
};
}
| {
status: "error";
userError:
| {
error: "PASSWORD_TOO_SHORT";
minimumLength: number;
}
| {
error: "PASSWORD_TOO_LONG";
maximumLength: number;
}
| {
error: "PASSWORD_HAS_SURROUNDING_WHITESPACE";
}
| {
error: "INVALID_CREDENTIALS";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
}
| {
error: "USER_NOT_FOUND";
};
}
>
>;
Verify an existing account's password and, on success, mint a session. Any
verified email of the account works. Returns USER_NOT_FOUND when no account
has verified the email and INVALID_CREDENTIALS when the password is wrong.
(Address existence is already observable via sign-up's EMAIL_TAKEN, so
distinguishing them here leaks nothing new.) The lookup of the address has a
per-IP limit, and returns RATE_LIMITED when the client has done too many
lookups.
signUpWithEmailPassword
signUpWithEmailPassword: RegisteredMutation<
"public",
{
email: string;
password: string;
},
Promise<
| {
browserSecret: string;
success: true;
}
| {
success: false;
userError:
| {
error: "PASSWORD_TOO_SHORT";
minimumLength: number;
}
| {
error: "PASSWORD_TOO_LONG";
maximumLength: number;
}
| {
error: "PASSWORD_HAS_SURROUNDING_WHITESPACE";
}
| {
error: "PASSWORD_TOO_COMMON";
}
| {
error: "INVALID_EMAIL";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
}
| {
error: "EMAIL_TAKEN";
};
}
>
>;
Create the app user and password for a new account, without a session. The user
signs in for the first time through completeSignUpWithEmailPassword, after
opening the challenge link.
Repeated sign-ups with one unvalidated address create separate users; the first completed validation wins, and the others can never sign in.
startChangeEmail
startChangeEmail: RegisteredMutation<
"public",
{
currentPassword: string;
newEmail: string;
},
Promise<
| {
browserSecret: string;
success: true;
}
| {
success: false;
userError:
| {
error: "PASSWORD_TOO_SHORT";
minimumLength: number;
}
| {
error: "PASSWORD_TOO_LONG";
maximumLength: number;
}
| {
error: "PASSWORD_HAS_SURROUNDING_WHITESPACE";
}
| {
error: "INVALID_CREDENTIALS";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
}
| {
error: "INVALID_EMAIL";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
}
| {
error: "EMAIL_TAKEN";
}
| {
error: "NOT_LOGGED_IN";
};
}
>
>;
Start changing the signed-in user's primary email address. The new address
replaces the old one: after the change, the account no longer has the old
address. Requires the session and the current password (OWASP ASVS v5 6.2.3).
Sends a challenge link to the new address; the change happens in
completeChangeEmail.
startPasswordRecovery
startPasswordRecovery: RegisteredMutation<
"public",
{
email: string;
},
Promise<
| {
browserSecret: string;
sentTo: string;
success: true;
}
| {
success: false;
userError:
| {
error: "INVALID_EMAIL";
}
| {
error: "RATE_LIMITED";
retryAfterMs: number;
}
| {
error: "EMAIL_NOT_FOUND";
};
}
>
>;
Start a password recovery: send a reset link (valid 10 minutes) to a verified email address. Any verified address of the account works, not only the primary one.
EMAIL_NOT_FOUND is surfaced to the caller. This reveals whether an address has
an account, which sign-up's EMAIL_TAKEN reveals anyway; the recipe accepts
that trade-off for a clearer flow. The lookup has a per-IP limit, so a client
cannot probe many addresses.