pealdevDOCS
Back to peal.dev

AUTH STARTER · TWO-FACTOR (TOTP)

Two-factor (TOTP)

TOTP with QR code setup, backup codes, and the recovery dance. Standard authenticator apps work — Google Authenticator, 1Password, Authy.

Two-factor uses TOTP — the 6-digit codes that rotate every 30 seconds. Users scan a QR code with their authenticator app, then enter a code to confirm. Backup codes get generated at enrollment for the day they lose their phone.

Turn it on

TypeScript
// src/auth.config.ts
plugins: {
  twoFactor: { enabled: true },
},

The setup flow

  • User confirms password (sanity check, since enabling 2FA is sensitive).
  • Server returns a TOTP URI and a list of one-time backup codes.
  • UI renders the URI as a QR code; user scans it with their app.
  • User enters a 6-digit code from their app to verify the secret works.
  • Server flips user.twoFactorEnabled to true.

All of this is implemented in src/components/auth/two-factor-setup.tsx — a multi-step dialog. Drop the component into your settings page and you're done.

The sign-in flow with 2FA

TypeScript
// auth-client.ts already does this:
twoFactorClient({
  onTwoFactorRedirect() {
    window.location.href = authConfig.routes.twoFactor;
  },
}),

When a 2FA-enabled user signs in with password, Better Auth returns a "needs 2FA" response. The client plugin redirects to /two-factor where they enter the code.

Backup codes

10 codes generated at enrollment. Each one-time use. The user MUST save these before they leave the setup flow — there's no "show me again later" by design (preventing a snoop with your unlocked laptop from grabbing them).

Don't store backup codes in your own logs

Better Auth stores them hashed in the twoFactor table. If you add custom logging around the 2FA flow, make sure you never log the codes themselves. Same goes for the TOTP secret.

Time skew breaks TOTP

TOTP codes are time-based. If the server's clock drifts by more than ~30 seconds, codes that look valid to the user will be rejected. On serverless this is rarely an issue, but on a self-hosted VPS — keep ntpd happy.