Add 2FA to your Next.js app in 30 minutes with Better Auth
Step-by-step tutorial to add two-factor authentication (TOTP) to a Next.js app using the Better Auth plugin.
Israel Palma
3 min read
Adding 2FA (two-factor authentication) to a Next.js app used to be painful. In 2026, with Better
Auth and its TOTP plugin, it's 30 well-organized minutes.
This guide assumes you already have Better Auth working. If not, start with their quickstart and
come back here.
## What you'll have at the end
- QR generation for apps like Google Authenticator, 1Password, Authy
- TOTP verification on login
- Backup codes for recovery
- Endpoint for the user to enable/disable 2FA
## Step 1: install the plugin
```bash
bun add better-auth
```
You already have it installed, we just activate the plugin.
## Step 2: configure Better Auth
`src/lib/auth/server.ts`:
```ts
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { db } from '@/lib/db/client';
export const auth = betterAuth({
database: prismaAdapter(db, { provider: 'postgresql' }),
emailAndPassword: { enabled: true },
plugins: [
twoFactor({
issuer: 'CREA.MBA',
}),
],
});
```
## Step 3: Prisma schema
Better Auth needs additional tables for 2FA. In your `schema.prisma`:
```prisma
model TwoFactor {
id String @id
secret String
backupCodes String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
```
Then:
```bash
bunx prisma migrate dev --name add-2fa
```
## Step 4: endpoint to start setup
When the user enables 2FA, you generate a TOTP secret and return a QR for them to scan.
`src/app/api/auth/2fa/enable/route.ts`:
```ts
import { auth } from '@/lib/auth/server';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const result = await auth.api.enableTwoFactor({
body: { password: '' }, // ask for password in production
headers: await headers(),
});
return NextResponse.json({
qrCode: result.totpURI,
backupCodes: result.backupCodes,
});
}
```
## Step 5: UI component
```tsx
'use client';
import { useState } from 'react';
import QRCode from 'react-qr-code';
export function Enable2FA() {
const [qrCode, setQrCode] = useState(null);
const [backupCodes, setBackupCodes] = useState([]);
const [verificationCode, setVerificationCode] = useState('');
async function startSetup() {
const res = await fetch('/api/auth/2fa/enable', { method: 'POST' });
const data = await res.json();
setQrCode(data.qrCode);
setBackupCodes(data.backupCodes);
}
async function verifyAndActivate() {
const res = await fetch('/api/auth/2fa/verify', {
method: 'POST',
body: JSON.stringify({ code: verificationCode }),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) alert('2FA enabled');
}
if (!qrCode) {
return ;
}
return (
);
}
```
## Step 6: integrate in login
On your login screen, after validating email + password, check if the user has 2FA enabled. If they
do, ask for the TOTP code before creating the session.
```tsx
async function handleLogin(email: string, password: string) {
const res = await auth.signIn.email({ email, password });
if (res.requiresTwoFactor) {
setShow2FAInput(true);
return;
}
// full login
}
async function handle2FACode(code: string) {
const res = await auth.twoFactor.verifyTotp({ code });
if (res.success) {
// full login
}
}
```
## Step 7: backup codes
If the user loses their phone, backup codes save lives. Better Auth generates them automatically.
When one is used, it's invalidated (not reusable).
Your UI should let the user regenerate them whenever they want. It's good practice to regenerate
them every 6-12 months.
## Common errors
**1. The code is invalid**: the server's clock and the phone's clock must be in sync. If your server
drifts more than 30 seconds, TOTP codes fail.
**2. QR doesn't scan**: many apps prefer `otpauth://totp/...` rather than a raw URL. Better Auth
already provides it in the right format.
**3. Backup codes in plain text**: never store backup codes in plain text. Better Auth hashes them
by default.
## Bottom line
30 minutes for properly done 2FA. The longest part is usually the setup UI; the Better Auth plugin
logic is practically plug-and-play.
There's no excuse not to have it today. And your B2B users will notice.
Enjoyed this article?
Subscribe for more tutorials and tips on building products with AI