# OTP Plugin

> Email-based one-time password authentication for passwordless sign-in and verification flows.

*Canonical: https://makerkit.dev/docs/tanstack-prisma/better-auth/otp-plugin*

---

This guide is part of the [Prisma](/prisma) stack docs.

Send 6-digit verification codes via email for passwordless sign-in, email verification, and password reset. The OTP plugin provides a code-based alternative to magic links.

This page is part of the [Authentication documentation](./overview).

## Overview

The OTP (One-Time Password) plugin sends 6-digit codes to users' email addresses. Unlike magic links that require clicking a URL, OTP codes can be entered directly in the app. This works better for:

- Mobile users who may not want to switch apps
- Environments where email links are blocked
- Users who prefer typing a code over clicking links

## Features

- **Passwordless sign-in** - Sign in with just an email and code
- **Email verification** - Verify email addresses with codes instead of links
- **Password reset** - Reset passwords with email code verification

## Configuration

The OTP plugin is enabled by default and requires no additional environment variables. It uses your existing mailer configuration.

### Email Setup

Ensure your mailer is configured:

```bash {% title="apps/web/.env.local" %}
MAILER_PROVIDER=nodemailer
EMAIL_SENDER=noreply@yourapp.com
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_USER=your_user
EMAIL_PASSWORD=your_password
```

See the [Email Configuration](../emails/configuration) documentation for full mailer setup.

## Implementation

The OTP plugin is configured in `packages/better-auth/src/plugins/otp-auth.ts`:

```typescript
import { emailOTP } from 'better-auth/plugins/email-otp';
import { getProductName } from '../utils/product-name';

export const otpPlugin = emailOTP({
  async sendVerificationOTP({ email, otp, type }) {
    // Log in development mode
    if (process.env.NODE_ENV === 'development') {
      console.log(`[DEV] OTP for ${email} (${type}): ${otp}`);
    }

    const productName = getProductName();

    if (type === 'sign-in') {
      const { sendOtpSignInEmail } =
        await import('../emails/send-otp-sign-in-email');
      await sendOtpSignInEmail({ email, otp, productName });
    } else if (type === 'email-verification') {
      const { sendOtpEmailVerificationEmail } =
        await import('../emails/send-otp-email-verification-email');
      await sendOtpEmailVerificationEmail({ email, otp, productName });
    } else {
      const { sendOtpPasswordResetEmail } =
        await import('../emails/send-otp-password-reset-email');
      await sendOtpPasswordResetEmail({ email, otp, productName });
    }
  },
});
```

## OTP Types

| Type | Purpose | Email Template |
|------|---------|----------------|
| `sign-in` | Passwordless authentication | `otp-sign-in.email.tsx` |
| `email-verification` | Verify email address | `otp-email-verification.email.tsx` |
| `forget-password` | Password reset | `otp-password-reset.email.tsx` |

## OTP Lifecycle

1. User requests an OTP (sign-in, verification, or password reset)
2. Server generates a 6-digit code
3. Code is stored with expiration (typically 10 minutes)
4. Email is sent to user with the code
5. User enters code in the application
6. Server validates code and completes action
7. Code is invalidated after successful use

## Development Mode

In development (`NODE_ENV=development`), OTP codes are logged to the console:

```
[DEV] OTP for user@example.com (sign-in): 847293
```

This allows testing without configuring email or checking inboxes.

## Client Usage

### Request OTP for Sign-In

```typescript
import { authClient } from '@kit/better-auth/client';

// Request OTP code
await authClient.emailOtp.sendVerificationOtp({
  email: 'user@example.com',
  type: 'sign-in',
});

// User receives email with 6-digit code
```

### Verify OTP and Sign In

```typescript
import { authClient } from '@kit/better-auth/client';

const result = await authClient.signIn.emailOtp({
  email: 'user@example.com',
  otp: '847293',
});

if (result.error) {
  console.error('Invalid or expired code');
}
```

### Email Verification with OTP

```typescript
import { authClient } from '@kit/better-auth/client';

// Request verification code
await authClient.emailOtp.sendVerificationOtp({
  email: 'user@example.com',
  type: 'email-verification',
});

// Verify the code
await authClient.emailOtp.verifyOtp({
  email: 'user@example.com',
  otp: '123456',
});
```

### Password Reset with OTP

```typescript
import { authClient } from '@kit/better-auth/client';

// Request password reset code
await authClient.emailOtp.sendVerificationOtp({
  email: 'user@example.com',
  type: 'forget-password',
});

// Verify code and set new password
await authClient.emailOtp.resetPassword({
  email: 'user@example.com',
  otp: '123456',
  newPassword: 'newSecurePassword123',
});
```

## Email Templates

OTP emails use templates in `packages/email-templates/src/emails/`:

- `otp-sign-in.email.tsx` - Sign-in OTP
- `otp-email-verification.email.tsx` - Email verification OTP
- `otp-password-reset.email.tsx` - Password reset OTP

Each template receives:
- `email` - Recipient email address
- `otp` - The 6-digit code
- `productName` - Your app name

## OTP vs Magic Links

| Feature | OTP | Magic Link |
|---------|-----|------------|
| User action | Type 6-digit code | Click link |
| Mobile UX | Better (stays in app) | Requires app switch |
| Email blocking | Works if email delivers | Links may be blocked |
| Expiration | Typically shorter | Typically longer |
| Security | Both are secure | Both are secure |

Choose based on your users' preferences and environment.

## Common Pitfalls

- **OTP expiration**: Codes expire quickly (default 10 minutes). Users must enter codes promptly.
- **Email delays**: Slow email delivery can cause codes to expire before arrival. Monitor delivery times.
- **Rate limiting**: Multiple OTP requests may be rate limited. Implement appropriate UI feedback.
- **Code reuse**: Each code can only be used once. Failed attempts don't consume the code.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How long are OTP codes valid?", "answer": "By default, OTP codes expire in 10 minutes. This can be configured in the plugin options."},
     {"question": "Can I use OTP and magic links together?", "answer": "Yes. Both plugins can be enabled simultaneously. Users can choose their preferred method."},
     {"question": "What happens if a user requests multiple OTPs?", "answer": "Each new OTP request invalidates the previous code. Only the most recent code is valid."},
     {"question": "Are OTP codes case-sensitive?", "answer": "No. OTP codes are numeric only (6 digits), so case sensitivity doesn't apply."},
     {"question": "How do I customize OTP email templates?", "answer": "Edit the email templates in packages/email-templates/src/emails/. Templates are React components using react-email."}
   ]
/%}

---

**Next:** [Rate Limiting →](./rate-limiting)
