Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
MongoDB
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
MongoDBchevron-right

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

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

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:
1const subscription = new EventSource("https://example.io/subscribe")

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.
1import {MongoClient} from 'mongodb';
2
3const client = new MongoClient("<mongodb-url>");
4const ghostDb = client.db('ghost');
5const ghostCollection = ghostDb.collection('webhooks');
6const ghostChangeStrem = ghostCollection.watch();
7
8ghostChangeStream.on('change', document => {
9 /* document is the MongoDB collection entry, e.g. our webhook */
10});
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:
1# ssh
2$ git clone git@github.com:tq-bit/mongodb-article-mongo-changestreams.git
3
4# HTTP(s)
5$ git clone https://github.com/tq-bit/mongodb-article-mongo-changestreams.git
6
7# Change to the starting branch
8$ git checkout 1-get-started
9
10# Install NPM dependencies
11$ npm install
12
13# Make a copy of .env.example
14$ cp .env.example .env
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
1npm run dev
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:
1// Replace
2// TODO: Add function to write the head
3// with
4console.log('Step 1: Write the response head and keep the connection open');
5res.writeHead(200, {
6 'Content-Type': 'text/event-stream',
7 'Cache-Control': 'no-cache',
8 Connection: 'keep-alive'
9});

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:
1// Replace
2// TODO: Add functionality to write the opening message
3// with
4console.log('Step 2: Write the opening event message');
5res.write('event: open\n');
6res.write('data: Connection opened!\n'); // Data can be any string
7res.write(`id: ${crypto.randomUUID()}\n\n`);

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:
1setInterval(() => {
2 console.log('Step 3: Send a message every five seconds');
3 res.write(`event: message\n`);
4 res.write(`data: ${JSON.stringify({ message: 'Five seconds have passed' })}\n`);
5 res.write(`id: ${crypto.randomUUID()}\n\n`);
6}, 5000);
7
8
9// Step 4: Handle request events such as client disconnect
10// Clean up the Change Stream connection and close the connection stream to the client
11req.on('close', () => {
12 console.log('Step 4: Handle request events such as client disconnect');
13 res.end();
14});
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.
1// Replace
2// TODO: Add insert one functionality for DB
3// with
4return notificationCollection.insertOne(notification);
And on to src/components/notification/notification.controller.ts. To process incoming webhooks, we'll add a handler function into handleArticleCreationNotification:
1// Replace
2// TODO: ADD handleArticleCreationNotification
3// with
4const incomingWebhook: GhostWebhook = req.body;
5await NotificationModel.createNotificiation({
6 id: crypto.randomUUID(),
7 ghostId: incomingWebhook.post?.current?.id,
8 ghostOriginalUrl: incomingWebhook.post?.current?.url,
9 ghostTitle: incomingWebhook.post?.current?.title,
10 ghostVisibility: incomingWebhook.post?.current?.visibility,
11 type: NotificationEventType.PostPublished,
12});
13
14res.status(200).send('OK');
This handler will pick data from the incoming webhook and insert a new notification.
1curl -X POST -d '{
2 "post": {
3 "current": {
4 "id": "sj7dj-lnhd1-kabah9-107gh-6hypo",
5 "url": "http://localhost:2368/how-to-create-realtime-notifications",
6 "title": "How to create realtime notifications",
7 "visibility": "public"
8 }
9 }
10}' http://localhost:3000/api/notification/article/create
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/notification.rest 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:
1const notificationStream = notificationCollection.watch();
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':
1notificationStream.on('change', (next) => {
2 console.log('Step 3.1: Change in Database detected!');
3});
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:
1notificationStream.on('change', (next) => {
2 // ... previous code
3 const {
4 // @ts-ignore, fullDocument is not part of the next type (yet)
5 fullDocument /* The newly inserted fullDocument */,
6 operationType /* The MongoDB operation Type, e.g. insert */,
7 } = next;
8});
Let's write the notification to the client. We can do this by repeating the method we used for the opening message.
1notificationStream.on('change', (next) => {
2 // ... previous code
3 console.log('Step 3.2: Writing out response to connected clients');
4 res.write(`event: ${operationType}\n`);
5 res.write(`data: ${JSON.stringify(fullDocument)}\n`);
6 res.write(`id: ${crypto.randomUUID()}\n\n`);
7});
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/notification.rest 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:
1// ... other app.use hooks
2app.use(express.static('public'));
3// ... app.listen()
Finally, navigate to Code Injection and add the following two entries in the 'Site Header':
1<script src="http://localhost:3000/js/plugin.js"></script>
2<link href="http://localhost:3000/css/plugin.css" rel="stylesheet">
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
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article

Can You Keep a Secret?


May 12, 2022 | 18 min read
Tutorial

How to Migrate Your Flask App From SQL To MongoDB


Jun 28, 2024 | 9 min read
Article

Window Functions & Time Series Collections


Aug 13, 2024 | 7 min read
Tutorial

Beyond Vectors: Augment LLM Capabilities With MongoDB Aggregation Framework


Jun 20, 2024 | 16 min read
Table of Contents