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 18, 2026ReactBetter Auth UI

Add Two-Factor Authentication to BTST Better Auth UI

Connect authenticator enrollment, sign-in challenges, and recovery codes while preserving server session checks.

Add Two-Factor Authentication to BTST Better Auth UI

Two-factor authentication in BTST needs a server challenge, a matching browser client, and a page that completes the challenge. Enabling a settings card supplies only the interface. This guide connects authenticator-app codes and recovery codes to an existing email-and-password integration.

The examples target @btst/stack@3.1.2, @btst/better-auth-ui@2.0.1, and better-auth@1.6.16. Start with the BTST auth integration if your handler, client, providers, and catch-all pages are not mounted yet. Preserve that setup when adding the fragments below.

Configure the server before showing enrollment#

Add this plugin to the existing betterAuth({ plugins: [...] }) configuration:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
import { twoFactor } from "better-auth/plugins/two-factor";

export const twoFactorPlugin = twoFactor({
  issuer: "Example App",
});

Keep your existing database, secret, origin checks, rate limits, and email/password configuration. Generate and review the plugin schema using tooling compatible with your installed Better Auth release, then apply it through your normal migration process. Enrollment needs the two-factor storage and user field in that schema.

The 1.6.16 implementation returns a TOTP URI and backup codes from enrollment. With the default policy, the user must verify a generated code before twoFactorEnabled becomes true. Keep this check enabled: successfully displaying setup information does not prove the authenticator works.

This article deliberately uses the released API rather than copying newer options from the moving upstream two-factor documentation. Check your installed version before extending the configuration to other enrollment methods.

Connect the client and BTST controls#

Add the matching client plugin 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 { twoFactorClient } from "better-auth/client/plugins";

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

Merge this UI fragment into StackProvider's existing overrides.auth:

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

export const twoFactorUI = {
  twoFactor: ["totp"],
} satisfies Partial<AuthPluginOverrides>;

These fragments omit your other client plugins and provider options for brevity; retain them. With default route paths and a /p mount, account security is /p/account/security, the challenge page is /p/auth/two-factor, and backup-code recovery is /p/auth/recover-account. Mount both the auth and account plugins so these pages exist.

The released security page shows its two-factor card only when the option is enabled and the user has a linked credential account. A social-only account therefore will not see this card. Supporting passwordless enrollment needs an independently designed flow; turning on the server's passwordless option does not change this UI condition.

Complete enrollment, then test a new sign-in#

From account security, enter the current password and save the recovery codes somewhere private. The companion sends the TOTP URI to its challenge page during setup. Treat that URI as a credential: it includes the authenticator secret. Exclude the setup URL and its query parameters from analytics, logs, screenshots, and support recordings. Never use a public QR-code service to render it.

After scanning the secret into an authenticator, submit a current code. Then sign out and sign in with the password again. The companion sign-in form checks twoFactorRedirect and navigates to the challenge page before its normal success transition. A custom sign-in form must preserve this distinction.

In the pinned server release, an outstanding challenge removes the provisional session. A protected server request should remain unauthenticated until verification succeeds. Hooks reading newSession must tolerate it being absent during this transition.

CheckExpected result
Start enrollment with a wrong passwordRequest rejected; no new authenticator enrolled
Start valid enrollment but stop before verificationTwo-factor protection is not yet enabled
Submit an invalid authenticator codeVerification rejected
Sign in after completing enrollmentChallenge precedes an authenticated session
Use one recovery codeSign-in completes; that code cannot be used again
Disable protectionPassword and server policy still apply

The release's automatic sign-in challenge applies to email/password, username, and phone-number sign-in endpoints. Do not assume it also wraps OAuth, magic links, email OTP, or passkeys. Test every enabled sign-in method against your application's policy before describing an account as requiring a second factor everywhere. The separate passkey guide covers that integration.

Also test trusted-device behavior on a fresh browser profile. A trusted browser can intentionally skip a later challenge; it is unsuitable for testing the ordinary untrusted-device path.

The snippets were type-checked and the pinned handler was exercised with an in-memory adapter for enrollment, challenge, recovery, and rejection cases. Physical authenticator enrollment, real migrations, and a complete browser integration were not exercised for this guide. Continue with the BTST auth documentation and session-management guide for server access checks after sign-in.

In This Post

Configure the server before showing enrollmentConnect the client and BTST controlsComplete enrollment, then test a new sign-in