New in Neon: Vector and BM25 full-text search extensions built for lakebase architecture, plus major CLI updates. See what shipped this week.

Phone Number

Sign in existing users with phone OTP codes delivered via your SMS provider

Beta

The Neon Auth with Better Auth is in Beta. Share your feedback on Discord or via the Neon Console.

Neon Auth is built on Better Auth and supports the Phone Number plugin through the Neon SDK.

Phone Number lets an existing user sign in with a one-time password (OTP) delivered to their phone. The flow works like this:

  1. The user enters their phone number in E.164 format (for example, +15551234567).
  2. Neon Auth generates a 6-digit OTP and fires the send.otp webhook with delivery_preference: "sms". Your webhook handler delivers the code via your SMS provider.
  3. The user enters the code. Neon Auth verifies it, creates a session, and signs them in.

important

The Phone Number plugin is sign-in only. Users must already exist in your project with a phone number linked to their account. There is no phone-first sign-up path.

Neon Auth does not deliver SMS for you. The plugin requires a send.otp webhook that forwards codes to your SMS provider (Twilio, MessageBird, Vonage, etc.).

Prerequisites

  • A Neon project with Auth enabled
  • An existing user with a phone number linked to their account
  • A webhook subscribed to the send.otp event that forwards the code to your SMS provider (see the Webhooks guide)

Enable Phone Number

  1. Open the Neon Console.
  2. Select your project and go to Auth > Plugins.
  3. Toggle Phone Authentication on.
  4. Configure the options:
    • OTP Expiry (60-600 seconds, default: 300) controls how long a generated OTP stays valid.

Neon Console Auth Plugins tab with Phone Number settings

Deliver OTPs via your SMS provider

Neon Auth fires the send.otp webhook with delivery_preference: "sms" when an OTP needs delivery. Your handler must deliver the code through your SMS provider (Twilio, MessageBird, Vonage, etc.). Without a send.otp webhook, authClient.phoneNumber.sendOtp() fails.

Configure a webhook subscribed to send.otp (see the Webhooks guide for full setup and signature verification), then branch on delivery_preference in your handler:

app/api/webhooks/neon-auth/route.ts
import { NextResponse } from 'next/server';
import twilio from 'twilio';

const twilioClient = twilio(
  process.env.TWILIO_ACCOUNT_SID!,
  process.env.TWILIO_AUTH_TOKEN!
);

export async function POST(request: Request) {
  const payload = await request.json(); // verify the signature first in production

  if (payload.event_type === 'send.otp' && payload.event_data.delivery_preference === 'sms') {
    const toNumber = payload.user?.phone_number;
    if (!toNumber) {
      return NextResponse.json({ error: 'missing phone number' }, { status: 400 });
    }

    await twilioClient.messages.create({
      from: process.env.TWILIO_FROM_NUMBER!,
      to: toNumber,
      body: `Your code is ${payload.event_data.otp_code}.`,
    });
  }

  return NextResponse.json({ ok: true });
}

User context for first OTPs

When an unauthenticated user requests their first phone OTP (no prior account linked to that phone number), the webhook user object contains the phone_number only. Do not rely on user.name, user.email, or other profile fields when templating the SMS for first-time sends.

For a runnable Next.js handler, signature verification, and a local tunneling setup, see the nextjs-phone-login example.

Link a phone to an existing user from an authenticated session by calling authClient.phoneNumber.verify() with updatePhoneNumber: true:

src/link-phone-number.ts
import { authClient } from '@/lib/auth/client';

export async function sendLinkingOtp(phoneNumber: string) {
  const { error } = await authClient.phoneNumber.sendOtp({ phoneNumber });
  if (error) throw error;
}

export async function linkPhoneNumber(phoneNumber: string, code: string) {
  const { data, error } = await authClient.phoneNumber.verify({
    phoneNumber,
    code,
    updatePhoneNumber: true,
  });

  if (error) throw error;
  return data;
}

For a complete "Add phone number" form with session-aware state (no number, unverified, verified), see the AddPhoneForm component in the nextjs-phone-login example.

Sign in an existing user with phone OTP

Build a two-step phone sign-in form: send the OTP, then verify it. Do not pass updatePhoneNumber. Omitting it tells Better Auth to sign in the existing user rather than link a phone number:

src/phone-otp.ts
import { authClient } from '@/lib/auth/client';

export async function sendPhoneOtp(phoneNumber: string) {
  const { error } = await authClient.phoneNumber.sendOtp({ phoneNumber });
  if (error) throw error;
}

export async function signInWithPhoneOtp(phoneNumber: string, code: string) {
  const { data, error } = await authClient.phoneNumber.verify({
    phoneNumber,
    code,
  });

  if (error) throw error;
  return data;
}

For a complete working form with resend, error handling, and attempt-budget awareness, see the nextjs-phone-login example.

Use Phone Number alongside UI components

Unlike Email OTP, NeonAuthUIProvider does not expose a phoneNumber prop, and the pre-built AuthView does not render a phone sign-in UI. If you're using Neon Auth UI components, render your own phone sign-in form next to AuthView on the sign-in route:

app/auth/[path]/page.tsx
import { AuthView } from '@neondatabase/auth-ui';
import { authViewPaths } from '@neondatabase/auth-ui/server';
import { PhoneSignInSection } from './phone-sign-in-section'; // your own component

export default async function AuthPage({ params }: { params: Promise<{ path: string }> }) {
  const { path } = await params;
  const isSignIn = path === authViewPaths.SIGN_IN;

  return (
    <>
      <AuthView path={path} />
      {isSignIn && <PhoneSignInSection />}
    </>
  );
}

PhoneSignInSection is a component you write that wraps your phone sign-in form. See the nextjs-phone-login example for a complete implementation.

info

If you haven't set up Neon Auth UI components yet, see the UI components reference for setup, or the Next.js or React quick start for building custom forms instead.

Webhook events

The Phone Number plugin uses two webhook events:

See the Webhooks guide for payload structure, signature verification, and retry behavior.

Limitations

  • Sign-in only. Users must already exist with a linked, verified phone number. No phone-first sign-up.
  • Bring your own SMS provider. No built-in SMS delivery. Requires a send.otp webhook forwarding to your SMS provider.
  • E.164 format required. Phone numbers must match ^\+[1-9]\d{1,14}$ (for example, +15551234567). Numbers with spaces, dashes, or parentheses are rejected.
  • OTP length is fixed at 6 digits. Not configurable.
  • Rate limit. Calls to /phone-number/* endpoints are limited to 10 requests per 60 seconds per IP, in addition to Neon Auth's global rate limits.
  • Attempt lockout. After 3 incorrect codes, the OTP is invalidated. Request a new one to try again.

Need help?

Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.

Was this page helpful?
Edit on GitHub