Build a notification system for a blog site with React, NodeJS and Novu 🚀

In this tutorial, you will learn how to create a blog on your own that notifies newsletter subscribers when a new article arrives. Throughout the tutorial, there are several visual demonstrations and sample codes to help you navigate through the project. The sample blog uses Novu as a notification system, Zoho Mail as an email provider, Payload CMS for the back end, and Next.js for the front end.

Hung Vu
Hung VuFebruary 23, 2023

Novu – the first open-source notification infrastructure

Just a quick background about us. Novu provides a unified API that makes it simple to send notifications through multiple channels, including In-App, Push, Email, SMS, and Chat. With Novu, you can create custom workflows and define conditions for each channel, ensuring that your notifications are delivered in the most effective way possible.

I would be super happy if you could give us a star! And let me also know in the comments ❤️


In recent years, learning in public has become a way to improve yourself and demonstrate your skillsets to potential employers. One of the popular ways to do so is by creating a blog, where you can publicly present your achievement and discuss technical topics with the audience. That all sounds good on paper, but how to start actually?

How to start

Novu provides a simple way to manage multiple complex notification workflows.

Creating a blog from the ground requires a full-stack experience. Let’s think about some questions such as below.

  • Do you know how to set up an email server?
  • Do you know how to send emails across different channels via code?
  • Do you know how to secure a database?

So on and so forth. To create a full-fledged application, there are many concerns that need to be addressed. With that said, this tutorial can help you bootstrap a simple blog that for the most part, works out of the box, in which, many functions are handled by well-known and battle-tested tools such as Novu for all the email-related functionalities.

What are the key technologies to create this blog?

  • Novu, an open-source notification infrastructure for developers
    • When publishing a new article, you want the readers to know that your content is now available. There are several channels to do so, such as SMSs, emails, in-app messages, and so on. Arguably, implementing a notification feature in your application might not be that complicated, but when talking about management and maintenance at scale, it is another can of worms. This is where a notification infrastructure comes into play.
    • Novu abstracts away all the hard-core implementation and is production-ready out-of-the-box. With Novu, you can centrally manage notifications across multiple channels in a user-friendly dashboard. This includes but is not limited to integration with third-party providers like email, SMS, push, chat, and in-app as well as analytics of notifications and subscribers.
    • As an open-source solution, you can completely self-host and customize Novu however you see fit. Also, if you want to delegate service management to other people, Novu also offers a fully-managed cloud-hosted solution that can get you running in a matter of minutes.
    • In this article, you will learn about the use and implementation of email notifications using a free cloud-hosted version of Novu.
  • Zoho Mail, a workspace email platform
    • Zoho Mail is a fully-managed cloud-hosted email service provider. Technically, it is more of a workspace email service (communication between human users), rather than a transactional and marketing email service (automatic emails). However, it is possible to use Zoho Mail as an email-as-a-service platform too.
    • To ensure consistency, all examples in this article use the author’s email. When following the article, you should change the examples to your own accounts and domains.
    • Zoho Mail’s free plan is more than sufficient to demonstrate Novu’s capabilities. The integration will be done via Simple Mail Transfer Protocol (SMTP).
  • Payload CMS, the code first, open-source React and TypeScript headless content management service (CMS)
  • Next.js, the open-source React “meta” framework
    • React is a popular UI library for creating a single-page application. With Next.js built on top of React, all toolings, configurations, and optimization are expanded and ready out-of-the-box.
    • Next.js has a vast array of features, making it capable of becoming a full-stack framework. However, this article mainly focuses on using Next.js for the front end. Meanwhile, Payload CMS handles the back end of the blog.
Architectural Diagram: Relationship between Next.js, PayloadCMS, Novu, and Zoho Mail.
1Architectural Diagram: Relationship between Next.js, PayloadCMS, Novu, and Zoho Mail.
3## How to set up Zoho Mail?
5As a workspace email service, Zoho Mail can have different degrees of configuration depending on your personal/organizational policies. This article only shows the fundamental steps required to get Zoho Mail up and running, and the steps include:
  1. Sign up for a Zoho Mail account
  2. Add and verify domain ownership
  3. Configure email delivery, SPF, and DKIM
  4. Retrieve SMTP information and application-specific credentials

Sign up for a Zoho Mail Account

This is a rather trivial step. A Zoho Mail account can be created by filling in personal information or using third-party single sign-on (SSO). One thing to note, an account created here is a super administrator account for your whole domain. This admin account does not require it to be in the same domain as others. For example, you sign up with, and that can be an administrator for

Add and verify domain ownership

After the registration, you can access the Admin Console. This is where a super administrator controls the whole domain from. Navigate to the Domains tab, this is where you can add your email domain.

The Domains tab of the Zoho Mail Admin Console.

The Domains tab of the Zoho Mail Admin Console.
After that, there are several ways to verify domain ownership, and one of the traditional ones is using a DNS record. Zoho Mail provides a unique and public TXT record that is used during the verification process. Add that to your DNS configuration, and your domain is verified.


Zoho Mail verification is done via TXT records on Cloudflare (DNS provider).

Configure email delivery, SPF, and DKIM

Technically, after the domain verification process. Zoho Mail is capable of sending emails out, but for incoming emails, more steps are needed. Also, with no Sender Policy Framework (SPF) and Domain Keys Identified Mail (DKIM), your domain is susceptible to certain types of domain spoofing attacks. Certain email recipients do outright reject emails from domains without a proper SPF and DKIM configuration, hence affecting your deliverability. With that said, click on the domain you just added mine is, and go to Email Configuration. In there, Zoho Mail provides several more records to add to your DNS configuration, and you are good to go after the addition is complete.

Image description

The DNS records for are properly set up in Cloudflare, as verified by Zoho Mail.

Retrieve SMTP information and application-specific credentials

To connect Novu with Zoho Mail, you need to have SMTP configuration and account credentials. SMTP configuration resides in Mail Settings > Tools & Configurations > Configurations. It is recommended to use TLS configuration for SMTP.

Image description

Retrieve SMTP information via the Mail Settings tab in Zoho Mail.

For account credentials, you need App Passwords for the account. In a sense, this acts like an API key so Novu can bypass Zoho Mail’s multi-factor authentication, and it is not recommended to create an App Password on the super administrator account due to security implications. For some reason, you cannot generate App Passwords via an Admin Console, so it is necessary to sign in with a regular user account.

Within your regular user Zoho Mail dashboard, navigate to My Account > Security and scroll down to App Passwords (Application-Specific Passwords). There, you can generate or revoke the application password (Novu in this case) but keep in mind that the password is only displayed once. If you lose the password, then it needs to be generated again. Although the password is named Novu, it is more of a label, so the password can be used by other applications too. Certainly, it is not a good practice to share a password like that.

Image description

Creating an Application-specific Passwords in User Profile/Security section.

Now, your Zoho Mail account should be all set. It is time to see how we can send a Zoho email via Novu unified APIs.

How to set up Novu?

As a reminder, Novu offers both self-hosted and cloud solutions. To simplify the process, the cloud version is a great choice here. Therefore, the first step is to register an account at Novu. When first signing in, Novu shows very pleasing and user-friendly instructions to get you started. Let’s see what they are.

Novu’s guidance for new users.

Connect your delivery provider

Novu supports several providers across different channels including email, SMS, Chat, and Push notification. There is no integration for Zoho out of the box. However, Novu supports a custom SMTP integration, meaning you can connect with practically every email service as SMTP is a widely adopted protocol. Certainly, Zoho also supports SMTP (as shown in the previous section).

Image description

Custom SMTP is possible via Novu’s Integration Store

Choosing the Custom SMTP option, you need to fill out the following information:

  1. User – This is your Zoho Mail email address. In this case, it is
  2. Password – This is your App Password created in the previous section. Your master password can also be used, but it fails if multi-factor authentication is enabled on the Zoho side.
  3. Host – This is an Outgoing Server Name retrieved in the previous section. In this case, it is
  4. Port – There are 2 options, SSL and TLS. As TLS is preferred in email transmission, port 587 is the choice.
  5. Secure – This field is only applicable to SSL connections. Set to true for a secure connection. If it is set on a TLS connection, an error happens, as shown below. Leave it empty for the TLS connection.
1139985482967856:error:1408F10B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:331:
  1. DKIM: Domain name / DKIM: Private key / DKIM: Key selector – Leave empty. It appears that these fields do not need to be set for Zoho Mail assuming your DKIM was properly configured in the DNS records. Later in the article, there is proof to show that DKIM works.
  2. From email address – Enter your user’s email address. In most cases, it is the same as the User field, which is here.
  3. Sender name – Enter your preferred name. In this case, it is Hung Vu.
  4. Active (button) – Enable it so SMPT is included in Novu Workflow.
  5. Verify provider credentials (button) – Enable it to get a confirmation that the SMTP configuration is working. If this button is disabled, you can only see a success message. However, when any of Novu’s functions evoke an SMTP connection, an error might show up there.

And you have just successfully set up Custom SMTP in Novu!

Image description

Custom SMTP is successfully activated.

Create your first notification template

Now is the time to create a new Novu Notification Template. This essentially is a way for you to define the “shape”, or format of your email, alongside an associated workflow. The workflow defines how this specific template behaves, and it consists of two main parts: Triggers, and Events. Let’s establish the goal to achieve:

After publishing your blog article

Novu notifies users of your new article via Zoho Mail

With that said, let’s navigate to Notifications and click on the New button on the top right to create a new Notification template.

Image description

Novu’s Notification Template section.

There are three sections of the template: Notification Settings, Workflow Editor, and User Preference Editor. Let’s fill them out as below.

  1. Notification Settings
    • Notification Name: Email - New Article
    • Notification Description: This is a channel for notifying about a new blog article via an email
    • Notification Group: General
  2. User Preference Editor
    • Leave everything as is.
  3. Workflow Editor
    • Drag and drop an Email step to the workflow.
    • Hover your mouse over the Email step, and choose Edit Template.
Image description

Novu’s Workflow Editor section.

  • Set fields as follows:
    • Email Subject: New article is available!
    • Preheader: Leave empty
    • Email Layout: Leave empty
1{{author}} just released a new article at {{article_url}}.
2Let's check it out. ❤️
  • Email Content: This accepts custom variables from the JSON request, and the variable is wrapped in double curly braces {{variables}}. With that said, the content is as below.
  • You should see the step variables author and article_url appear on the right panel.
Image description

A sign that variables to be used in the email template are successfully configured.

  • Press Update to save the workflow.

Navigate back to either Edit Template or Workflow Editor dashboard, and here, Test Workflow is available on the top right of your screen. Click on it, and you can edit the payload to trigger this Email - New Article workflow.

Image description

Information to test workflow. The information can be manually edited in the fields.

Image description

This a sign that Novu successfully sends a request to Zoho Mail.

Note: A success here only means Novu is able to craft and send an email via Zoho Mail. The status delivery status can only be checked via Zoho Mail’s Sent box. As in the picture below, one of the email is undeliverable because I typed in the wrong receipient name.

Image description

The real status of the test can only be checked in Zoho Mail, not via Novu.

For a delivered email, it should reside in the recipient’s mailbox as seen below. Certainly, SPF and DKIM are successfully validated for this email. At this point, you have successfully set up a unified Novu API.

Image description

The emails from reach their destination and pass SPF and DKIM tests, as verified by Gmail.

How to build a simple blog using Payload CMS and Next.js?

Unlike other popular CMS like Strapi, and Contentful, the Payload CMS is code-first. This infrastructure as a code approach allows users to consistently build the product and migrate between servers without worrying about human errors in configuring via GUI elements. Meanwhile, Next.js supports a wide variety of rendering strategies and is built on top of a mature React ecosystem. It has been a top choice for developers even at an enterprise scale (according to the State of JS survey), certainly, it is more than suitable for a simple blog website.

The admin dashboard of Payload CMS uses Next.js. That means you can use the same technology on both the front-end (blog website), and front-of-back-end (admin dashboard), hence reducing overhead and development time. Luckily, the Payload team has a boilerplate for integration with the CMS, and it should be a good starting point. At the moment, the boilerplate is using Next.js 12, and with the boilerplate being under active development, it might become Next.js 13 in the near future and introduce many breaking changes compared to this article.

That said, as Payload CMS uses MongoDB, the database server must be configured first. It can be done easily by downloading the MongoDB Community edition. The MongoDB team provides a full installation package for many operating systems (OS), so the installation and server initiation is rather trivial.

With MongoDB out of the way, first, create a new repository using a template at payloadcms/nextjs-custom-server, and clone that to your local machine. Unlike forking, creating a new repository from a template resets the commit history and disassociate it from an original template repository.

1git clone

Now, navigate to the cloned repository and install the necessary packages.

1# Payload CMS is using yarn for its project.
2# But you can always use a package manager of your choice.
3cd blog-website-with-novu
4yarn install 

In the boilerplate repository (root folder), there is a .env.example, create a copy of that and name the file .env. As Payload CMS uses dotenv, which looks for a .env file by default, any other similar names like .env.local can raise an exception.

1# Create a new .env file
2cp .env.example .env

Assuming the MongoDB server is up and running, now you can fire up the project using.

1yarn dev

However, in case you see an exception as below (which may happen on certain OS versions).

1ERROR (payload): Error: cannot connect to MongoDB. Details: connect ECONNREFUSED ::1:27017

You need to modify a MONGO_URL in the .env file as below.

1# Before
4# After

Lastly, let’s grab a Novu API key and put it in your environment file.

Image description

Novu’s API key resides in the Settings tab. You can ignore Application Identifier in this tutorial.

1# Add this key to .env
2NOVU_API_KEY=<Your API key>

How to create a new article in the Payload CMS admin dashboard?

When the server successfully starts, go to [localhost:3000/admin](http://localhost:3000/admin) to register an account and access the admin dashboard. By default, there are three collections: Pages, Media, and Users. To create a new article, let’s navigate to Pages.

admin dashboard

An admin dashboard of Payload CMS.

Click on Create new Page, and fill out the following information.

  1. Page Title: This is your first article
  2. Featured Image: Leave empty
  3. Page Layout: Choose the Content option, and let the body of your article be
1This is the body of your article. I suppose this is where your writing journey begins!
  1. Page meta – Title: Search engine optimization (SEO) title, and tab name
  2. Page meta – Description: SEO description
  3. Page meta – Keywords: Software engineering and technical writer blog
  4. Page Slug: Leave empty. This by default, is created based on Page Title upon saving. In this case, it is this-is-your-first-article, but you can always change the slug as you want

Now, save the page and navigate to localhost:3000/this-is-your-first-article.


A new blog article successfully shows up on the front end.

Still, Novu has not come into play yet, so now is the time to change it!

How to create newsletter subscriber collections in the Payload CMS?

Essentially, there are 2 goals to achieve:

  1. Create a collection for newsletter subscribers in Payload CMS. Upon user registration, deletion, and similar operations, Payload CMS makes a query to Novu and performs associated tasks.
  2. Create a UI element on the front end, so readers can register for your newsletter.

With that said, first install Novu’s SDK using.

1yarn add @novu/node

Then, navigate to collections folder, and create NewsletterSubscriber.ts. This file creates a collection in Payload CMS and generates a respective database schema.

1// Author: Hung Vu
2// This collection represents a list of subscriber information.
3// The list is intended to be used for newsletter emails.
4import { CollectionConfig } from "payload/types";
5import { Novu } from "@novu/node";
7const novu = new Novu(process.env.NOVU_API_KEY);
9export const NewsletterSubscriber: CollectionConfig = {
10  slug: "newsletter-subscribers",
11  admin: {
12    useAsTitle: "email",
13  },
14  access: {
15    // Public user can subscribe.
16    // By default, all other operations like "read", "update", etc. are restricted
17    // to only authorized users.
18    create: () => true, 
19  },
20  fields: [
21    {
22      // Payload CMS also allows field validation,
23      // This should be done in production code to avoid spam.
24      name: "email",
25      label: "Subscriber Email",
26      type: "text",
27      required: true,
28      unique: true,
29    },
30  ],
31  hooks: {
32    // It is the best to move these to "utilities",
33    // and have an appropriate error handler in production code.
34    afterChange: [
35      (args) => {
36        const operation = args.operation;
37        const email =;
38        const internal_id =;
39        // Create and update subscriber, Novu recommends the use of internal id.
40        // Source:
41        operation === "create"
42          ? novu.subscribers
43              .identify(internal_id, { email })
44              .catch((err) => console.error(err))
45          : operation === "update"
46          ? novu.subscribers
47              .update(internal_id, { email })
48              .catch((err) => console.error(err))
49          : null;
50      },
51    ],
52    afterDelete: [
53      (args) => {
54        // Delete subscriber
55        const internal_id =;
56        novu.subscribers.delete(internal_id).catch((err) => console.error(err));
57      },
58    ],
59  },
62export default NewsletterSubscriber;

In payload.config.ts, modify it as follows.

1// Author: Hung Vu
2import { buildConfig } from "payload/config";
3import dotenv from "dotenv";
4import Page from "./collections/Page";
5import Media from "./collections/Media";
6import NewsletterSubscriber from "./collections/NewsletterSubscriber";
10export default buildConfig({
11  serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
12  collections: [
13    Page,
14    Media,
15    // Make Payload CMS aware of the new collection.
16    NewsletterSubscriber,
17  ],

On the front end, create a newsletter registration component in components/NewsletterRegistration/index.tsx.

1// Author: Hung Vu
2// This component allows readers to subscribe to the newsletter.
3import React from "react";
5const NewsletterRegistration: React.FC = () => {
6  const [readerEmail, setReaderEmail] = React.useState<string>();
7  const submit = async () => {
8    try {
9      // Remember to handle exceptions and status code in the production code.
10      await fetch("http://localhost:3000/api/newsletter-subscribers/", {
11        method: "POST",
12        headers: {
13          "Content-Type": "application/json",
14        },
15        body: JSON.stringify({
16          email: readerEmail,
17        }),
18      });
19    } catch (err) {
20      console.error(err);
21    }
22  };
23  return (
24    <form
25      style={{
26        display: "flex",
27        flexDirection: "column",
28        marginBottom: "32px",
29      }}
30      onSubmit={(event) => {
31        // There is "TypeError: NetworkError when attempting to fetch resource." without the preventDefault() statement.
32        event.preventDefault();
33        submit();
34      }}
35    >
36      <label htmlFor="newsletter">Subscribe to newsletter</label>
37      <input
38        id="newsletter"
39        type="text"
40        placeholder="Enter your email"
41        onChange={(event) => setReaderEmail(}
42      />
43      <input type="submit" value="Subscribe" />
44    </form>
45  );
48export default NewsletterRegistration;

And enable this component in the footer section at pages/[...slug].tsx

1// Import, and add this to line 52.
2<NewsletterRegistration />
Image description

A newsletter registration component is added to the article.

How to send a newsletter to your subscribers?

At this stage, you have an article and a list of subscribers in hand already. Therefore, how can you let the subscribers know about your new publishment? Well, the approach is the same as linking a NewsletterSubscriber collection and Novu, meaning, this time you need to link the Page collection and Novu using a collection hook in Payload CMS.

Navigate to collections/Page.ts and modify it as follow.

1// Author: Hung Vu
2// This blog article collections notifies newsletter
3// subscribers whenever a new article is released.
5import { CollectionConfig } from "payload/types";
6import { MediaType } from "./Media";
7import formatSlug from "../utilities/formatSlug";
8import { Image, Type as ImageType } from "../blocks/Image";
9import { CallToAction, Type as CallToActionType } from "../blocks/CallToAction";
10import { Content, Type as ContentType } from "../blocks/Content";
11import payload from "payload";
12import { Novu } from "@novu/node";
14const novu = new Novu(process.env.NOVU_API_KEY);
16export type Layout = CallToActionType | ContentType | ImageType;
18export type Type = {
19  title: string;
20  slug: string;
21  image?: MediaType;
22  layout: Layout[];
23  meta: {
24    title?: string;
25    description?: string;
26    keywords?: string;
27  };
30export const Page: CollectionConfig = {
31  slug: "pages",
32  admin: {
33    useAsTitle: "title",
34  },
35  access: {
36    read: (): boolean => true, // Everyone can read Pages
37  },
38  fields: [
39    {
40      name: "author",
41      label: "Author name",
42      type: "text",
43      required: true,
44    },
45    {
46      name: "title",
47      label: "Page Title",
48      type: "text",
49      required: true,
50    },
51    {
52      name: "image",
53      label: "Featured Image",
54      type: "upload",
55      relationTo: "media",
56    },
57    {
58      name: "layout",
59      label: "Page Layout",
60      type: "blocks",
61      minRows: 1,
62      blocks: [CallToAction, Content, Image],
63    },
64    {
65      name: "meta",
66      label: "Page Meta",
67      type: "group",
68      fields: [
69        {
70          name: "title",
71          label: "Title",
72          type: "text",
73        },
74        {
75          name: "description",
76          label: "Description",
77          type: "textarea",
78        },
79        {
80          name: "keywords",
81          label: "Keywords",
82          type: "text",
83        },
84      ],
85    },
86    {
87      name: "slug",
88      label: "Page Slug",
89      type: "text",
90      admin: {
91        position: "sidebar",
92      },
93      hooks: {
94        beforeValidate: [formatSlug("title")],
95      },
96    },
97  ],
98  hooks: {
99    afterChange: [
100      async (args) => {
101        const author =;
102        const urlSlug = args.doc.slug;
103        const operation = args.operation;
104        try {
105          // Using local API bypasses the access control rules.
106          // This is a way to retrieve records from other collections internally.
107          // Also, async/await can be used in hooks
108          // Source:
109          const newsletterSubscriberList = (
110            await payload.find({
111              collection: "newsletter-subscribers",
112            })
113          ).docs;
114          // This triggers only on "create"
115          if (operation === "create") {
116            newsletterSubscriberList.forEach((subcriber) => {
117              const email =;
118              const internalId =;
119              novu.trigger("email-new-article", {
120                to: {
121                  subscriberId: internalId,
122                  email: email,
123                },
124                payload: {
125                  author: author,
126                  // The url is hard-coded only for demonstration purpose
127                  article_url: `http://localhost:3000/${urlSlug}`,
128                },
129              });
130            });
131          }
132        } catch (err) {
133          console.error(err);
134        }
135      },
136    ],
137  },
140export default Page;

You may wonder why novu is re-initialized here. If it is created in server/index.ts and shared to other locations, an exception happens, as shown below.

1BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
2This is no longer the case. Verify if you need this module and configure a polyfill for it.

Novu perhaps is using a singleton pattern when initializing the instance, so it should be fine, and indeed, here are the results.

Image description

Subscriber’s email shows up in Novu’s Subscribers dashboard.

Image description

The newsletter successfully reach to the subscriber.

Wrap up

In this article, you have learned and achieved several goals:

  1. Set up Zoho Mail.
  2. Know Novu’s capabilities and integration with Zoho Mail via SMTP.
  3. Bootstrap a simple Next.js/Payload CMS project.
  4. Integrate Novu to Payload to create a standard workflow for notifying your blog subscribers about new articles.

Certainly, Novu’s capabilities are not limited to just that. There is much more that you can achieve with a powerful notification workflow in Novu. Here are some resources to explore:

  1. Novu’s Templates
  2. Novu’s Digest Engine
  3. Novu’s Notification Center

Also, don’t forget to check out the code repository of this tutorial on GitHub, and feel free to reach out to Novu’s team via Discord whenever you have any questions about this excellent open-source notification infrastructure for developers!

Help me out!

If you feel like this article helped you, I would be super happy if you could give us a star! And let me also know in the comments ❤️

Hung Vu
Hung VuFebruary 23, 2023

Related Posts


Components for Developers: Why I Joined Novu

Today, I pen this post with excitement, and a forward-looking spirit. For the past four years, my team was building components that are worth a thousand APIs! We aimed to offer a Google-level authentication experience with amazing frontend DX in a few lines of code.

Sokratis Vidros
Sokratis VidrosApril 17, 2024

Welcome to the new Novu Community

Why the Novu community is important, and what we're doing to support you.

Justin Nemmers
Justin NemmersFebruary 27, 2024

Making GraphQL Codegen Work For You: GraphQL Integration with React and TypeScript

In this guide, we’ll be showing you how to use GraphQL alongside React and GraphQL Codegen to create a simple page that can pull data from an API and send emails. We’ll be using Novu as an open source notification system for developers that can send our emails after being passed through a form created within React.

Carl-W Igelstrom
Carl-W IgelstromMarch 1, 2023