OTPs

Make sure you're done with setup before configuring authentication methods

This authentication method has two steps:

  1. The user provides an email address or a phone number, to which a one-time-password (OTP) code is sent
  2. The user then fills out the code within your app

Your database keeps track of the issued code that was sent which allows for secure verification and expiration.

You will configure your message template, so you can send an OTP and a magic link together.

Email providers

Convex Auth implements configuration via Auth.js (opens in a new tab) "provider" configs. These JS objects define how the library sends emails.

Open the Auth.js Magic Links providers doc (opens in a new tab) to see a list of available email providers.

Choose an email provider (for example Resend) and follow the guide below.

Ignore the "database provider" configuration in Auth.js docs. Your Convex backend is your database!

Setup

Environment variables

Configure the relevant environment variables for a given email provider, as per Auth.js docs, on your Convex backend.

See setting environment variables Convex docs (opens in a new tab).

For example for Resend, after you sign up and obtain your API key, you can run (with your own value):

npx convex env set AUTH_RESEND_KEY yourresendkey

Provider configuration

Create a custom provider config for sending an OTP.

This example uses additional dependencies
  1. The Resend SDK (resend on NPM)
  2. oslo, a handy library which is part of the Lucia project

You can install these with npm i resend@3.2.0 oslo.

Note: resend v3.3 and v3.4 is broken in Convex runtime, fix is coming in v3.5

convex/ResendOTP.ts
import { Email } from "@convex-dev/auth/providers/Email";
import { Resend as ResendAPI } from "resend";
import { alphabet, generateRandomString } from "oslo/crypto";
 
export const ResendOTP = Email({
  id: "resend-otp",
  apiKey: process.env.AUTH_RESEND_KEY,
  maxAge: 60 * 15, // 15 minutes
  async generateVerificationToken() {
    return generateRandomString(8, alphabet("0-9"));
  },
  async sendVerificationRequest({ identifier: email, provider, token }) {
    const resend = new ResendAPI(provider.apiKey);
    const { error } = await resend.emails.send({
      from: "My App <onboarding@resend.dev>",
      to: [email],
      subject: `Sign in to My App`,
      text: "Your code is " + token,
    });
 
    if (error) {
      throw new Error("Could not send");
    }
  },
});

Then use it in convex/auth.ts:

convex/auth.ts
import { convexAuth } from "@convex-dev/auth/server";
import { ResendOTP } from "./ResendOTP";
 
export const { auth, signIn, signOut, store } = convexAuth({
  providers: [ResendOTP],
});

Check out the example repo (opens in a new tab) for a more polished email template, including using React for the email templating (add "jsx": "react-jsx" to your convex/tsconfig.json).

You can find more examples of email customization in Auth.js docs (opens in a new tab).

Add sign-in form

Now you can trigger the email sending from a form submission via the signIn function.

After the email is sent, you should show a second form for submitting the code. When the second form is submitted, call the signIn function again.

For example for the custom ResendOTP provider:

src/SignIn.tsx
import { useAuthActions } from "@convex-dev/auth/react";
 
export function SignIn() {
  const { signIn } = useAuthActions();
  const [step, setStep] = useState<"signIn" | { email: string }>("signIn");
  return step === "signIn" ? (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        void signIn("resend-otp", formData).then(() =>
          setStep({ email: formData.get("email") as string }),
        );
      }}
    >
      <input name="email" placeholder="Email" type="text" />
      <button type="submit">Send code</button>
    </form>
  ) : (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        void signIn("resend-otp", formData);
      }}
    >
      <input name="code" placeholder="Code" type="text" />
      <input name="email" value={step.email} type="hidden" />
      <button type="submit">Continue</button>
      <button type="button" onClick={() => setStep("signIn")}>
        Cancel
      </button>
    </form>
  );
}

The second call to signIn will include the user-provided code. Because this is a short code, we must also provide the email that matches the account used for the sign-in. The library automatically rate-limits failed attempts.

Check out the example repo (opens in a new tab) for a more polished UI.

When you're done configuring your chosen authentication methods, learn how to use authentication in your frontend and backend in Authorization.

Phone providers

There are no built-in providers, but one is easy to build. The example repo has an example of using Twilio and their Node.js SDK (opens in a new tab).

The sign-in form works the same as above, but uses a phone field instead of an email field: example (opens in a new tab).

If you use a third-party service that takes care of generating and validating OTPs, you can use a custom ConvexCredentials config to implement the authentication method. The example repo shows how to do this with the Twilio Verify service (opens in a new tab).

When you're done configuring your chosen authentication methods, learn how to use authentication in your frontend and backend in Authorization.