kavachOS
Authentication

Email OTP

Six-digit one-time codes sent via email for passwordless sign-in.

Email OTP sends a short numeric code to the user's inbox. It works well for mobile flows where clicking a link is awkward and for verification steps inside an existing session.

Setup

Install

pnpm add kavachos

Add the plugin

lib/kavach.ts
import { createKavach } from 'kavachos';
import { emailOtp } from 'kavachos/plugins/email-otp'; 

const kavach = await createKavach({
  database: { provider: 'postgres', url: process.env.DATABASE_URL! },
  secret: process.env.KAVACH_SECRET!,
  baseUrl: 'https://auth.example.com',
  plugins: [
    emailOtp({ 
      onSendOtp: async (email, code) => { 
        await resend.emails.send({
          from: 'auth@example.com',
          to: email,
          subject: `Your code: ${code}`,
          html: `<p>Your sign-in code is <strong>${code}</strong>. It expires in 10 minutes.</p>`,
        });
      }, 
    }), 
  ],
});

How it works

  1. User submits their email to POST /auth/email-otp/send.
  2. KavachOS generates a cryptographically random code and calls your onSendOtp function with the email and code.
  3. User enters the code in your UI and submits to POST /auth/email-otp/verify.
  4. On success, a session cookie is set.

If the email belongs to an existing account, the same user ID is returned. If it is new, an account is created automatically.

Send a code

POST /auth/email-otp/send

await fetch('/auth/email-otp/send', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'user@example.com' }),
});

The response is always 200 to prevent email enumeration. Codes are rate-limited to one per minute per email address — requests within the window return 429. Build a countdown timer into your UI.

Verify a code

POST /auth/email-otp/verify

const res = await fetch('/auth/email-otp/verify', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com',
    code: '482910', 
  }),
});

const { userId, sessionId } = await res.json(); 

After maxAttempts failed verifications, the code is invalidated and a new one must be requested.

Codes are single-use. A successful verification invalidates the code immediately. Do not retry the same code after a success response.

Options

OptionTypeDefaultDescription
onSendOtp(email: string, code: string) => Promise<void>requiredCalled with the recipient email and the numeric code
codeLengthnumber6Number of digits in the OTP
codeTtlnumber600Code lifetime in seconds (default: 10 minutes)
maxAttemptsnumber5Failed attempts before the code is invalidated
createUserIfNotFoundbooleantrueAuto-create accounts for new emails

On this page