Introduction

In this guide, you’ll learn how to send OTP verification email notifications using the React-email package. Follow these steps:

Getting started

Integrating Novu’s code-first workflow with React.Email for your Next.js application can be done in a few steps:

  1. Create a NextJS app and wait for the installation:
npx create-novu-app@latest --api-key=<YOUR_API_KEY>

// The command will ask you if you want to include react-email into your new project.
  1. Once this installation is complete, simply cd into the directory and start your app using the npm run dev command.

Using a code-first workflow

  1. Write an email template - To write an email template, you can look over some of the examples in the React Email documentation to get inspiration. In our case, this is the template:
import {
    Body,
    Button,
    Container,
    Head,
    Heading,
    Hr,
    Html,
    Img,
    Link,
    Preview,
    Section,
    Text,
    render
} from "@react-email/components";
import * as React from "react";

interface LinearLoginCodeEmailProps {
    validationCode?: string;
    showJoinButton?: boolean;
    buttonText?: string;
    inviteLink?: string;
    logoURL?: string;
    inviteFromLocation?: string;
    inviteFromIp?: string;
    supportEmail?: string;
}

export const LinearLoginCodeEmail = ({
    validationCode,
    showJoinButton,
    buttonText,
    inviteLink,
    logoURL,
    inviteFromIp,
    inviteFromLocation,
    supportEmail
}: LinearLoginCodeEmailProps) => (
    <Html>
        <Head />
        <Preview>Your login code for Linear</Preview>
        <Body style={main}>
            <Container style={container}>
                <Img
                    src={`${logoURL}`}
                    width="42"
                    height="42"
                    alt="Linear"
                    style={logo}
                />
                <Heading style={secondary}>
                    Your login code for Linear
                </Heading>
                <Section style={buttonContainer}>
                    {showJoinButton && (
                        <Section className="text-center mt-[32px] mb-[32px]">
                            <Button
                                style={button}
                                href={inviteLink}
                            >
                                {buttonText}
                            </Button>
                        </Section>
                    )}
                </Section>
                <Text style={paragraph}>
                    This link and code will only be valid for the next 5 minutes. If the
                    link does not work, you can use the login verification code directly:
                </Text>
                <Section style={codeContainer}>
                    <Text style={code}>{validationCode}110658</Text>
                </Section>
                <Hr style={hr} />
                <Text style={paragraphSupport}>Not expecting this email?</Text>
                <Text style={paragraphSupport}>
                    This invite was
                    sent from <span style={paragraphSupportText}>{inviteFromIp}</span>{" "}
                    located in{" "}
                    <span style={paragraphSupportText}>{inviteFromLocation}</span>.
                </Text>
                <Text style={paragraphSupport}>
                    Please contact{" "}
                    <Link href="mailto:suport@linear.com" style={link}>
                        {supportEmail}
                    </Link>{" "}
                    if you did not request this code.
                </Text>
            </Container>
        </Body>
    </Html>
);

LinearLoginCodeEmail.PreviewProps = {
    validationCode: "tt226-5398x",
} as LinearLoginCodeEmailProps;

export default LinearLoginCodeEmail;

const logo = {
    borderRadius: 21,
    width: 42,
    height: 42,
};

const main = {
    backgroundColor: "#ffffff",
    fontFamily:
        '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
};

const container = {
    margin: "0 auto",
    padding: "20px 0 48px",
    maxWidth: "560px",
};

const secondary = {
    color: "#000",
    display: "inline-block",
    fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif",
    fontSize: "20px",
    fontWeight: 500,
    lineHeight: "24px",
    marginBottom: "0",
    marginTop: "2rem",
    textAlign: "center" as const,
};

const paragraphSupportText = {
    fontSize: "15px",
    fontWeight: "700",
};

const paragraph = {
    margin: "0 0 15px",
    fontSize: "15px",
    lineHeight: "1.4",
    color: "#3c4149",
};

const buttonContainer = {
    padding: "27px 0 27px",
};

const button = {
    backgroundColor: "#5e6ad2",
    borderRadius: "3px",
    fontWeight: "600",
    color: "#fff",
    fontSize: "15px",
    textDecoration: "none",
    textAlign: "center" as const,
    display: "block",
    padding: "11px 23px",
};

const hr = {
    borderColor: "#dfe1e4",
    margin: "42px 0 26px",
};

const code = {
    color: "#000",
    display: "inline-block",
    fontFamily: "HelveticaNeue-Bold",
    fontSize: "16px",
    fontWeight: 700,
    letterSpacing: "6px",
    lineHeight: "40px",
    paddingBottom: "8px",
    paddingTop: "8px",
    margin: "0 auto",
    width: "100%",
    textAlign: "center" as const,
};

const paragraphSupport = {
    color: "#444",
    fontSize: "15px",
    fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif",
    letterSpacing: "0",
    lineHeight: "23px",
    padding: "0 40px",
    margin: "0",
    textAlign: "center" as const,
};

const link = {
    color: "#444",
    textDecoration: "underline",
};

const codeContainer = {
    background: "rgba(0,0,0,.05)",
    borderRadius: "4px",
    margin: "16px auto 14px",
    verticalAlign: "middle",
    width: "280px",
};

export function renderOTPEmail(payload: any) {
    return render(<LinearLoginCodeEmail {...payload} />);
}
  1. Launch Dev Studio - Novu’s code-first approach lets you see how the template would look when rendered, right when defining it, using the Dev Studio. To launch the dev studio locally you can run npx novu-labs@latest echo. The Dev Studio will be started by default on port 2022, and accessible via: http://localhost:2022

Dev Studio lets you see the workflow you wrote without sending it off!

  1. Define a workflow that uses that template to send notifications - In this step, we need to define a workflow that uses the template we wrote above to render the email notification:
import { Client, workflow } from "@novu/framework";
import { renderOTPEmail } from "./otp";

export const client = new Client({
  apiKey: process.env.NOVU_API_KEY,
  /**
   * Disable this flag only during local development
   */
  strictAuthentication: process.env.NODE_ENV !== "development",
});

export const otpFlow = workflow('otp-flow', async ({ step, payload }) => {
  // Send a welcome email
  await step.email('send-email', async (inputs) => {
    return {
      subject: `Here's your verification code, Sumit!`,
      body: renderOTPEmail(inputs, payload),
    };
  }, {
    inputSchema: {
      type: "object",
      properties: {
        showJoinButton: { type: "boolean", default: true },
        buttonText: { type: "string", default: "Login to Linear" },
        logoURL: {
          type: "string",
          default: "https://react-email-demo-7qy8spwep-resend.vercel.app/static/linear-logo.png",
          format: "uri",
        },
        supportEmail: { type: "string", default: "support@linear.co" },
        validationCode: { type: "string", default: "tt226-5398x" },
        inviteLink: {
          type: "string",
          default: "https://linear.app",
          format: "uri",
        },
        inviteFromIp: { type: "string", default: "204.13.186.218" },
        inviteFromLocation: {
          type: "string",
          default: "São Paulo, Brazil",
        },
      },
    },
  });
  // JSON Schema for validation and type-safety. Zod, and others coming soon.
}, { payloadSchema: { properties: { text: { type: 'string' } } } });
  1. Triggering the workflow - Lastly, we need to trigger the workflow we created above. Here’s how to trigger it:
import { Novu } from '@novu/node';

const novu = new Novu('<NOVU_API_KEY>');

export async function POST(request: Request) {
  const res = await request.json();

  await novu.trigger('otp-flow', {
    to: {
      subscriberId: 'new-user',
    },
    payload: {
      email: res.email,
      username: res.username,
    },
  });

  console.log('triggered')
  return Response.json({ success: true });
}

When we trigger this workflow, here’s the email received on the client-side:

The workflow as delivered on a client-side device

That’s it!

That’s how you create and use an OTP workflow. You can check out our docs for a hands on guide with more in-depth instructions.

Once you’ve built the workflow, you might want read one of our other guides on how to empower product teams to manage notification workflows.

Don’t forget to share your workflows with us and as always, hit us up on Discord with any questions you might have!