BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 17, 2026ReactBetter Auth UI

Verify Email Addresses with Codes in BTST Better Auth UI

Connect six-digit verification codes, server policy, delivery, and the BTST verification page without confusing verification with passwordless sign-in.

Verify Email Addresses with Codes in BTST Better Auth UI

BTST's email-verification page accepts a six-digit code. To use it after password registration, connect Better Auth's Email OTP plugin, its browser client, and the matching UI option. A verification-link email does not supply the code this form expects.

This guide extends a working integration with @btst/better-auth-ui@2.0.0 and Better Auth 1.6.16. The delivery example uses a Next.js App Router handler. Start with the auth integration guide if your server, browser client, and provider are not connected.

Choose code verification deliberately#

Email ownership verification and passwordless sign-in are separate jobs. The companion exposes emailVerification: { otp: true } for verification and emailOTP: true for the passwordless sign-in form. Enabling one does not mean you intended the other.

The released verification form reads the email from the page's query string, accepts six digits, and calls authClient.emailOtp.verifyEmail. Its resend action requests an OTP with type email-verification. Keep the server's code length at six when using this built-in form.

If you prefer verification links, retain your link delivery callback and omit the OTP UI option. Do not point a verification-link email at the six-digit form or rename a link token as an OTP.

Configure the server policy and delivery#

Create these additions beside your existing auth configuration:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
import type { BetterAuthOptions } from "better-auth";
import { emailOTP } from "better-auth/plugins/email-otp";
import { after } from "next/server";
import { sendEmail } from "@/lib/email";

export const verificationPolicy = {
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  emailVerification: {
    sendOnSignUp: true,
    sendOnSignIn: true,
    autoSignInAfterVerification: false,
  },
} satisfies Partial<BetterAuthOptions>;

export const verificationPlugin = emailOTP({
  otpLength: 6,
  expiresIn: 300,
  allowedAttempts: 3,
  storeOTP: "hashed",
  disableSignUp: true,
  overrideDefaultEmailVerification: true,
  sendVerificationOTP: async ({ email, otp, type }) => {
    after(async () => {
      await sendEmail({
        to: email,
        subject: "Your account verification code",
        text: `Your code for ${type} is ${otp}. It expires in five minutes.`,
      });
    });
  },
});

Merge the two policy objects into their corresponding existing options and append verificationPlugin to your existing plugins array. Preserve password constraints, registration policy, recovery callbacks, database configuration, secret, and other plugins. Do not replace the whole auth configuration with this fragment.

In the pinned Email OTP implementation, overrideDefaultEmailVerification connects the core verification-mail callback to OTP delivery. This example uses core sendOnSignUp and sendOnSignIn triggers; it does not also enable the plugin's separate signup hook. Unverified password sign-in is rejected and can trigger another verification message.

disableSignUp here prevents automatic account creation through OTP sign-in. It does not disable password registration, remove the plugin's passwordless endpoints, or replace authorization. Hiding the passwordless UI is likewise not a server policy. Review all enabled auth methods against your product's account rules.

sendEmail must be a server-only integration that rejects provider errors. The Next.js after callback schedules delivery after the response within the request's supported lifetime. It is not a durable retry queue. For another framework, use its supported background work or your existing delivery queue. Do not log codes, email bodies, or private recipient data.

Connect the same browser client and provider#

Append emailOTPClient() to the auth client already passed to BTST:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
import { createAuthClient } from "better-auth/react";
import { emailOTPClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [emailOTPClient()],
});

Preserve your other client plugins, base URL, and API mount. Then merge this option into the existing overrides.auth object:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
import type { AuthPluginOverrides } from "@btst/better-auth-ui/client";

export const verificationUI = {
  emailVerification: { otp: true },
} satisfies Partial<AuthPluginOverrides>;

With the default view paths and a /p site mount, the form is /p/auth/email-verification?email=.... The released signup form navigates there when registration returns no session token and OTP verification is enabled. The sign-in form also navigates there on EMAIL_NOT_VERIFIED. Preserve the provider's navigation and session-refresh wiring.

With autoSignInAfterVerification: false, successful code verification returns the user to sign-in. Enabling automatic sign-in changes that transition and needs a corresponding session test. The page query string identifies the address to verify; the server still checks the code. Keep email parameters out of analytics and shared error reports.

Verify rejection as well as delivery#

CheckExpected result with this configuration
Register a new test accountAccount is unverified; a six-digit code is delivered
Sign in before verificationServer refuses password sign-in
Submit an incorrect or expired codeServer refuses verification
Submit a valid code for a different emailServer refuses verification for that address
Submit the correct codeEmail becomes verified; user returns to sign-in
Reuse a consumed codeServer refuses another verification

The form's 30-second resend countdown is a convenience, not abuse protection. Keep server rate limiting and attempt limits enabled, and test them through the API. Also check delivery failure handling and a real inbox before launch. The examples were type-checked and exercised with an in-memory auth handler; that does not verify your email provider or deployed browser flow.

Use the Email OTP documentation for the API and BTST auth documentation for the provider. For account recovery, continue with the separate password-reset guide.

In This Post

Choose code verification deliberatelyConfigure the server policy and deliveryConnect the same browser client and providerVerify rejection as well as delivery