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.
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
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
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
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
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;
}
}
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>
);
}
#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 });
}
}
#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",
};
},
});
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";
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
},
},
);
...
..
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;
}
...
..
Make sure you add the INTERNAL_API_KEY
to your .env
file.
# .env
INTERNAL_API_KEY="YOUR_API_KEY"
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 ❤️
Top comments (12)
Nice one! Is this how Papermark works internally?
Yes one small part of it. Specifically, it’s the email that senders receive when someone visits the document
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.
Thanks for the feedback!
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?
react-email is amazing! can 100% recommend using it. i never created such beautiful email templates as with react-email
Nicely detailed article using some great open source tools like Trigger.dev and Papermark.
This is a great one to bookmark.
Thanks Nathan 💪
Very cool stuff 💅
🙌🙌
Trigger.dev and Papermark are really cool!
Trigger is perfect for this kind of task :)