Email

Password reset, confirmation, and deletion emails through Amazon SES or a local console logger.

Onekit sends three kinds of transactional email through Better Auth: password reset links, email confirmation links, and account deletion confirmations. Delivery is optional. When no provider is configured, the server refuses those requests and the UI hides the corresponding paths, so the rest of the app keeps working.

Providers

EMAIL_PROVIDERBehaviorUse it for
unsetNo delivery. Reset and verification are unavailable; sign-up and sign-in still work.Minimal setups, tests without email
consolePrints each message, including its one-time link, to the server log. Refused when NODE_ENV=production.Local development
sesSends through Amazon SES v2 using the AWS credentials of the process.Deployed stages

bun run setup writes EMAIL_PROVIDER=console into .env, so local sign-up prints a confirmation link in the terminal running bun run dev. Open that link in the browser to verify the address.

Configure SES

  1. Verify a sending identity (a domain or an address) in the SES console for your region and request production access if the account is still sandboxed. Sandboxed accounts only deliver to verified recipients.
  2. Set EMAIL_FROM to that identity, either no-reply@example.com or Onekit <no-reply@example.com>.
  3. Set EMAIL_REGION (or rely on AWS_REGION) to the SES region.
  4. Give the process permission to call ses:SendEmail. The SST service grants this to its task role automatically when infraConfig.email.from is set; local runs use your AWS profile.

For deployment, set infraConfig.email.from in infra/config.ts. SST then sets EMAIL_PROVIDER=ses, EMAIL_FROM, EMAIL_REGION, and the task permission. Optionally set infraConfig.email.requireVerification to refuse sign-in until the address is confirmed. See Deployment.

Verification policy

AUTH_REQUIRE_EMAIL_VERIFICATION=true makes sign-up return no session and makes sign-in answer EMAIL_NOT_VERIFIED until the emailed link is used. The sign-in screen explains this and offers to resend the link. It requires an email provider; the server refuses to start auth with the flag set and no provider.

With the flag off (the default), accounts can sign in immediately. Sign-up still sends a confirmation link when a provider is configured, and the account page shows a reminder with a resend button until the address is confirmed. Verification state is stored on user.email_verified; check it in your own authorization code before trusting an address.

Flows

FlowClient callWhat happens
Forgot passwordauthClient.requestPasswordReset({ email, redirectTo: '/reset-password' })Always answers success. If the address exists, a link valid for one hour is sent. Opening it redirects to /reset-password?token=…, or ?error=INVALID_TOKEN if expired or already used.
Reset passwordauthClient.resetPassword({ newPassword, token })Sets the new password, consumes the token, and revokes every existing session.
Change passwordauthClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: true })Requires the current password and a session created within the last day. Other devices are signed out.
Confirm emailauthClient.sendVerificationEmail({ email, callbackURL: '/settings/profile' })Sends a link valid for one day. Opening it marks the address verified, signs the browser in, and redirects to the callback.
Delete accountauthClient.deleteUser({ password, callbackURL: '/' })Password accounts must supply their password. With email configured, a confirmation link valid for one day is sent and deletion happens when it is opened. Without email, deletion is immediate and social-only accounts need a session newer than one day.

The redirectTo and callbackURL values are checked against trusted origins; use app-relative paths. Emailed links point at the web server (BETTER_AUTH_URL), so native users complete these steps in a browser.

Templates

src/server/email.ts holds the transport and the three templates. Each message has a plain-text body and a small inline-styled HTML body with the same content; user-supplied values are HTML-escaped. Edit the copy there, or replace emailTemplates with a rendering library if you need richer layouts. Keep the plain-text version: it is what the console provider prints and what the integration tests read.

Test

bun run test:auth runs the full reset, verification, password change, and deletion flows against an in-memory PostgreSQL with a capturing transport; nothing is delivered. To try delivery for real, run the app with EMAIL_PROVIDER=ses, a verified EMAIL_FROM, and AWS credentials, then request a reset for your own address.

Reference: Better Auth email and password, Better Auth email verification, SES v2 SendEmail.

Onekit / A little less setup.Web · iOS · Android