BlogAnnounced at MongoDB.local NYC 2024: A recap of all announcements and updatesLearn more >>
MongoDB Developer
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right

Adding Real-Time Notifications to Ghost CMS Using MongoDB and Server-Sent Events

Tobias Quante7 min read • Published Aug 14, 2023 • Updated Aug 14, 2023
DockerChange StreamsJavaScriptMongoDB
Facebook Icontwitter iconlinkedin icon
Rate this tutorial

About Ghost

Ghost is an open-source blogging platform. Unlike other content management systems like WordPress, its focus lies on professional publishing.
This ensures the core of the system remains lean. To integrate third-party applications, you don't even need to install plugins. Instead, Ghost offers a feature called Webhooks which runs while you work on your publication.
These webhooks send particular items, such as a post or a page, to an HTTP endpoint defined by you and thereby provide an excellent base for our real-time service.

Server-sent events

You are likely familiar with the concept of an HTTP session. A client sends a request, the server responds and then closes the connection. When using server-sent events (SSEs), said connection remains open. This allows the server to continue writing messages into the response.
A default HTTP lifecycle consists of a client request and a server response
Like Websockets (WS), apps and websites use SSEs for real-time communication. Where WSs use a dedicated protocol and work in both directions, SSEs are unidirectional. They use plain HTTP endpoints to write a message whenever an event occurs on the server side.
Server Sent events are written into an already existing connection response
Clients can subscribe to these endpoints using the EventSource browser API:

MongoDB Change Streams

Now that we’ve looked at the periphery of our application, it's time to present its core. We'll use MongoDB to store a subset of the received Ghost Webhook data. On top of that, we'll use MongoDB Change Streams to watch our webhook collection.
In a nutshell, Change Streams register data flowing into our database. We can subscribe to this data stream and react to it. Reacting means sending out SSE messages to connected clients whenever a new webhook is received and stored.
The following Javascript code showcases a simple Change Stream subscription.
Its event-based nature matches perfectly with webhooks and SSEs. We can react to newly received webhooks where the data is created, ensuring data integrity over our whole application.

Build a real-time endpoint

We need an extra application layer to propagate these changes to connected clients. I've decided to use Typescript and Express.js, but you can use any other server-side framework. You will also need a dedicated MongoDB instance*. For a quick start, you can sign up for MongoDB Atlas. Then, create a free cluster and connect to it.
Let's get started by cloning the 1-get-started branch from this Github repository:
Make sure to fill out the MONGO_HOST environment variable with your connection string!
Express and the database client are already implemented. So in the following, we'll focus on adding MongoDB change streams and server-sent events.
Once everything is set up, you can start the server on http://localhost:3000 by typing
The application uses two important endpoints which we will extend in the next sections:
  • /api/notification/subscribe <- Used by EventSource to receive event messages
  • /api/notification/article/create <- Used as a webhook target by Ghost
* If you are not using MongoDB Atlas, make sure to have Replication Sets enabled.

Add server-sent events

Open the cloned project in your favorite code editor. We'll add our SSE logic under src/components/notification/notification.listener.ts.
In a nutshell, implementing SSE requires three steps:
  • Write out an HTTP status 200 header.
  • Write out an opening message.
  • Add event-based response message handlers.
We’ll start sending a static message and revisit this module after adding ChangeStreams.
You can also git checkout 2-add-sse to see the final result.

Write the HTTP header

Writing the HTTP header informs clients of a successful connection. It also propagates the response's content type and makes sure events are not cached.
Add the following code to the function subscribeToArticleNotification inside:

Write an opening message

The first message sent should have an event type of 'open'. It is not mandatory but helps to determine whether the subscription was successful.
Append the following code to the function subscribeToArticleNotification:

Add response message handlers

We can customize the content and timing of all further messages sent. Let's add a placeholder function that sends messages out every five seconds for now. And while we’re at it, let’s also add a handler to close the client connection:
Append the following code to the function subscribeToArticleNotification:
To check if everything works, visit http://localhost:3000/api/notification/subscribe.
server sent event data in a browser screen

Add a POST endpoint for Ghost

Let's visit src/components/notification/notification.model.ts next. We'll add a simple insert command for our database into the function createNotificiation:
You can also git checkout 3-webhook-handler to see the final result.
And on to src/components/notification/notification.controller.ts. To process incoming webhooks, we'll add a handler function into handleArticleCreationNotification:
This handler will pick data from the incoming webhook and insert a new notification.
You can also test the insert functionality by using Postman or VSCode REST client and then check your MongoDB collection. There is an example request under /test/ in the project's directory, for your convenience.

Trigger MongoDB Change Streams

So far, we can send SSEs and insert Ghost notifications. Let's put these two features together now.
Earlier, we added a static server message sent every five seconds. Let's revisit src/components/notification/notification.listener.ts and make it more dynamic.
First, let's get rid of the whole setInterval and its callback. Instead, we'll use our notificationCollection and its built-in method watch. This method returns a ChangeStream.
You can create a change stream by adding the following code above the export default code segment:
The stream fires an event whenever its related collection changes. This includes the insert event from the previous section.
We can register callback functions for each of these. The event that fires when a document inside the collection changes is 'change':
The variable passed into the callback function is a change stream document. It includes two important information for us:
  • The document that's inserted, updated, or deleted.
  • The type of operation on the collection.
Let's assign them to one variable each inside the callback:
Let's write the notification to the client. We can do this by repeating the method we used for the opening message.
And that's it! You can test if everything is functional by:
  1. Opening your browser under http://localhost:3000/api/notification/subscribe.
  2. Using the file under test/ with VSCode's HTTP client.
  3. Checking if your browser includes an opening and a Ghost Notification.
server sent event data in a browser screen after fully implementing change streams
For an HTTP webhook implementation, you will need a running Ghost instance. I have added a dockerfile to this repo for your convenience. You could also install Ghost yourself locally.
To start Ghost with the dockerfile, make sure you have Docker Engine or Docker Desktop with support for docker compose installed.
For a local installation and the first-time setup, you should follow the official Ghost installation guide.
After your Ghost instance is up and running, open your browser at http://localhost:2368/ghost. You can set up your site however you like, give it a name, enter details, and so on.
Configure the Ghost deployment
In order to create a webhook, you must first create a custom integration. To do so, navigate into your site’s settings and click on the “Integrations” menu point. Click on “Add Webhook,” enter a name, and click on “Create.”
Ghost third-party custom integrations
Inside the newly created integration, you can now configure a webhook to point at your application under http://<host>:<port>/api/notification/article/create*.
* This URL might vary based on your local Ghost setup. For example, if you run Ghost in a container, you can find your machine's local IP using the terminal and ifconfig on Linux or ipconfig on Windows.
Local ghost-realtime application
And that’s it. Now, whenever a post is published, its contents will be sent to our real-time endpoint. After being inserted into MongoDB, an event message will be sent to all connected clients.

Subscribe to Change Streams from your Ghost theme

There are a few ways to add real-time notifications to your Ghost theme. Going into detail is beyond the scope of this article. I have prepared two files, a plugin.js and a plugin.css file you can inject into the default Casper theme.
Try this out by starting a local Ghost instance using the provided dockerfile.
You must then instruct your application to serve the JS and CSS assets. Add the following to your index.ts file:
Finally, navigate to Code Injection and add the following two entries in the 'Site Header':
The core piece of the plugin is the EventSource browser API. You will want to use it when integrating this application with other themes.
When going back into your Ghost publication, you should now see a small bell icon on the upper right side.
subscription notifications in Ghost

Moving ahead

If you’ve followed along, congratulations! You now have a working real-time notification service for your Ghost blog. And if you haven’t, what are you waiting for? Sign up for a free account on MongoDB Atlas and start building. You can use the final branch of this repository to get started and explore the full power of MongoDB’s toolkit.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial

Window Functions & Time Series Collections

May 13, 2022 | 7 min read

Scaling the Gaming Industry with Gaspard Petit of Square Enix

Mar 22, 2023 | 29 min

Getting Started with MongoDB and Java - CRUD Operations Tutorial

Mar 01, 2024 | 24 min read

At the Intersection of AI/ML and HCI with Douglas Eck of Google (MongoDB Podcast)

Feb 27, 2023 | 30 min
Table of Contents