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.
EMAIL_PROVIDER | Behavior | Use it for |
|---|---|---|
| unset | No delivery. Reset and verification are unavailable; sign-up and sign-in still work. | Minimal setups, tests without email |
console | Prints each message, including its one-time link, to the server log. Refused when NODE_ENV=production. | Local development |
ses | Sends 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.
EMAIL_FROM to that identity, either no-reply@example.com or Onekit <no-reply@example.com>.EMAIL_REGION (or rely on AWS_REGION) to the SES region.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.
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.
| Flow | Client call | What happens |
|---|---|---|
| Forgot password | authClient.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 password | authClient.resetPassword({ newPassword, token }) | Sets the new password, consumes the token, and revokes every existing session. |
| Change password | authClient.changePassword({ currentPassword, newPassword, revokeOtherSessions: true }) | Requires the current password and a session created within the last day. Other devices are signed out. |
| Confirm email | authClient.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 account | authClient.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.
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.
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.