DEV Community

Cover image for Building background email notifications with Next.js, Resend and Trigger.dev
Marc Seitz
Marc Seitz

Posted on

Building background email notifications with Next.js, Resend and Trigger.dev

What you will find in this article?

Email notifications are the most common way to keep your users informed about actions taking on your application. Typical notifications include: someone follow you, someone likes your post, someone viewed your content. In this post, we are going to explore how we can create a simple asynchronous email notification system using Next.js, Resend and Trigger.dev.

We will use Next.js as the framework to build our application. We will use Resend to send emails and Trigger.dev to offload and send the emails asynchronously.

Image description

Papermark - the open-source DocSend alternative.

Before we kick it off, let me share Papermark with you. It's an open-source alternative to DocSend that helps you securely share documents and get real-time page-by-page analytics from viewers. It's all open-source!

I would be super happy if you could give us a star! Don't forget to share your thoughts in the comments section ❀️
https://github.com/mfts/papermark

Papermark App

Setup the project

Let's go ahead and set up our project environment for our email background notification system. We'll be creating a Next.js app, and set up to Resend, and most importantly, Trigger to handle the asynchronous email notifications.

Setting up Next.js with TypeScript and Tailwindcss

We'll use create-next-app to generate a new Next.js project. We'll also be using TypeScript and Tailwind CSS, so make sure to select those options when prompted.



npx create-next-app

# ---
# you'll be asked the following prompts
What is your project named?  my-app
Would you like to add TypeScript with this project?  Y/N
# select `Y` for typescript
Would you like to use ESLint with this project?  Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias


Enter fullscreen mode Exit fullscreen mode

Install Resend and React-Email

Resend is a developer-first transactional email service. We'll use it to send emails to our users. react-email is a React component library that makes it easy to create beautiful emails.



npm install resend react-email


Enter fullscreen mode Exit fullscreen mode

Install Trigger

Trigger is a background job framework for TypeScript. It allows you to offload long-running tasks from your main application and run them asynchronously. We'll use it to send emails asynchronously.

The Trigger CLI is the easiest way to set up Trigger in your new or existing Next.js project. For more info, check out their docs.



npx @trigger.dev/cli@latest init


Enter fullscreen mode Exit fullscreen mode

Building the application

Now that we have our setup in place, we are ready to start building our application. The main features we'll cover are:

  • Setup Resend email
  • Write a API route to send email
  • Add a Trigger job to make the email sending asynchronous

#1 Setup Resend email

First, we'll need to set up Resend to send emails. We'll create a new file resend-notification.ts in our project and add the following code.



// lib/emails/resend-notification.ts
import { Resend } from "resend";
import { NotificationEmail } from "@/components/emails/notification";

const resend = new Resend(process.env.RESEND_API_KEY!);

export async function sendNotificationEmail({
  name,
  email,
}: {
  name: string | null | undefined;
  email: string | null | undefined;
}) {
  const emailTemplate = NotificationEmail({ name });
  try {
    // Send the email using the Resend API
    await resend.emails.send({
      from: "Marc from Papermark <marc@papermark.io>",
      to: email as string,
      subject: "You have a new view on your document!",
      react: emailTemplate,
    });
  } catch (error) {
    // Log any errors and re-throw the error
    console.log({ error });
    throw error;
  }
}


Enter fullscreen mode Exit fullscreen mode

And the notification email template using react-email will look like this:



// components/emails/notification.tsx
import React from "react";
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
  Tailwind,
} from "@react-email/components";

export default function ViewedDocument({
  name,
}: {
  name: string | null | undefined;
}) {
  return (
    <Html>
      <Head />
      <Preview>See who visited your document</Preview>
      <Tailwind>
        <Body className="bg-white my-auto mx-auto font-sans">
          <Container className="my-10 mx-auto p-5 w-[465px]">
            <Heading className="text-2xl font-normal text-center p-0 mt-4 mb-8 mx-0">
              <span className="font-bold tracking-tighter">Papermark</span>
            </Heading>
            <Heading className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
              New Document Visitor
            </Heading>
            <Text className="text-sm leading-6 text-black">
              Your document was just viewed by someone.
            </Text>
            <Text className="text-sm leading-6 text-black">
              You can get the detailed engagement insights like time-spent per
              page and total duration for this document on Papermark.
            </Text>
            <Section className="my-8 text-center">
              <Button
                className="bg-black rounded text-white text-xs font-semibold no-underline text-center"
                href={`${process.env.NEXT_PUBLIC_BASE_URL}/documents`}
                style={{ padding: "12px 20px" }}>
                See my document insights
              </Button>
            </Section>
            <Text className="text-sm">
              Cheers,
              <br />
              The Papermark Team
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}


Enter fullscreen mode Exit fullscreen mode

#2 Write a API route to send email

Now, we have our email template ready. We can use it to send emails to our users. We'll create a serverless function that takes the name and email of the user and sends them an email using the sendNotificationEmail function we created earlier.



// pages/api/send-notification.ts
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/lib/prisma";
import { sendViewedDocumentEmail } from "@/lib/emails/resend-notification";

export const config = {
  maxDuration: 60,
};

export default async function handle(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // We only allow POST requests
  if (req.method !== "POST") {
    res.status(405).json({ message: "Method Not Allowed" });
    return;
  }

  // POST /api/send-notification
  try {
    const { viewId } = req.body as {
      viewId: string;
    };

    // Fetch the link to verify the settings
    const view = await prisma.view.findUnique({
      where: {
        id: viewId,
      },
      select: {
        document: {
          select: {
            owner: {
              select: {
                email: true,
                name: true,
              },
            },
          },
        },
      },
    });

    if (!view) {
      res.status(404).json({ message: "View / Document not found." });
      return;
    }

    // send email to document owner that document
    await sendViewedDocumentEmail({
      email: view.document.owner.email as string,
      name: view.document.owner.name as string,
    });

    res.status(200).json({ message: "Successfully sent notification", viewId });
    return;
  } catch (error) {
    console.log("Error:", error);
    return res.status(500).json({ message: (error as Error).message });
  }
}


Enter fullscreen mode Exit fullscreen mode

#3 Add a Trigger job to make the email sending asynchronous

Our email sending function is ready, but we don't want to send emails synchronously and therefore wait until the email is send before the application responds to the user. We want to offload the email sending to a background job. We'll use Trigger to do that.

In the setup, Trigger CLI created a jobs directory in our project. We'll create a new file notification-job.ts in that directory and add the following code.



// jobs/notification-job.ts
import { client } from "@/trigger";
import { eventTrigger, retry } from "@trigger.dev/sdk";
import { z } from "zod";

client.defineJob({
  id: "send-notification",
  name: "Send Notification",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "link.viewed",
    schema: z.object({
      viewId: z.string(),
    }),
  }),
  run: async (payload, io, ctx) => {
    const { viewId } = payload;

    // get file url from document version
    const notification = await io.runTask(
      "send-notification",
      async () => {
        const response = await fetch(
          `${process.env.NEXT_PUBLIC_BASE_URL}/api/send-notification`,
          {
            method: "POST",
            body: JSON.stringify({ viewId }),
            headers: {
              "Content-Type": "application/json",
            },
          }
        );

        if (!response.ok) {
          await io.logger.error("Failed to send notification", { payload });
          return;
        }

        const { message } = (await response.json()) as {
          message: string;
        };

        await io.logger.info("Notification sent", { message, payload });
        return { message };
      },
      { retry: retry.standardBackoff }
    );

    return {
      success: true,
      message: "Successfully sent notification",
    };
  },
});


Enter fullscreen mode Exit fullscreen mode

Add an export to the jobs index file, otherwise Trigger won't know about the job. Small detail but even I have forgotten about this and searched for the error for a good hour.



// jobs/index.ts
export * from "./notification-job";


Enter fullscreen mode Exit fullscreen mode

Bonus: Prevent rogue access to the API route

We have our API route ready, but we don't want to allow anyone to access it. We want to make sure that only our application can access it. We'll use a simple header authentication key to do that.

In the Trigger job, we'll add the header to the request:



// jobs/notification-job.ts
..
...
const response = await fetch(
  `${process.env.NEXT_PUBLIC_BASE_URL}/api/jobs/send-notification`,
  {
    method: "POST",
    body: JSON.stringify({ viewId }),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`, // <- add the authenication header with a local env variable
    },
  },
);
...
..


Enter fullscreen mode Exit fullscreen mode

In the API route, we'll check if the API key matches just before the try {} catch {} block:



// pages/api/send-notification.ts
..
...
// Extract the API Key from the Authorization header
const authHeader = req.headers.authorization;
const token = authHeader?.split(" ")[1]; // Assuming the format is "Bearer [token]"

// Check if the API Key matches
if (token !== process.env.INTERNAL_API_KEY) {
  res.status(401).json({ message: "Unauthorized" });
  return;
}
...
..


Enter fullscreen mode Exit fullscreen mode

Make sure you add the INTERNAL_API_KEY to your .env file.



# .env
INTERNAL_API_KEY="YOUR_API_KEY"

Enter fullscreen mode Exit fullscreen mode




Conclusion

Voila! We have our asynchronous email notification system ready. We can now send emails to our users asynchronously without impacting user wait time. We can also use Trigger to offload many other tasks from our main application that we don't want the user to wait for.

Thank you for reading. I am Marc, an open-source advocate. I am building papermark.io - the open-source alternative to DocSend.

Keep on coding!

Help me out!

If you found this article helpful and got to understand Trigger and background tasks a bit better, I would be very happy if you could give us a star! And don't forget to share your thoughts in the comments ❀️

https://github.com/mfts/papermark

Image description

Top comments (12)

Collapse
 
matijasos profile image
Matija Sosic

Nice one! Is this how Papermark works internally?

Collapse
 
mfts profile image
Marc Seitz

Yes one small part of it. Specifically, it’s the email that senders receive when someone visits the document

Collapse
 
clauscastillo profile image
Claus Castillo

It is a very intuitive explanation and it was very helpful! Just that there are errors in the mail template is not using the prop "name" and also the function to send the notification differs in name between the code snippets, caused me some confusion.

Collapse
 
mfts profile image
Marc Seitz

Thanks for the feedback!

Collapse
 
srbhr profile image
Saurabh Rai

I have seen papermark, and it's an excellent tool you've built. OSS for the win.
On another side note, what are your opinions on "react-email" is it stable enough?

Collapse
 
mfts profile image
Marc Seitz

react-email is amazing! can 100% recommend using it. i never created such beautiful email templates as with react-email

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Nicely detailed article using some great open source tools like Trigger.dev and Papermark.

This is a great one to bookmark.

Collapse
 
mfts profile image
Marc Seitz

Thanks Nathan πŸ’ͺ

Collapse
 
fernandezbaptiste profile image
Bap

Very cool stuff πŸ’…

Collapse
 
mfts profile image
Marc Seitz

πŸ™ŒπŸ™Œ

Collapse
 
nevodavid profile image
Nevo David

Trigger.dev and Papermark are really cool!

Collapse
 
mfts profile image
Marc Seitz

Trigger is perfect for this kind of task :)