23 min readUpdated Aug 30, 2023

Engage site visitors with realtime updates using PlanetScale

Engage site visitors with realtime updates using PlanetScale
Marie StarckMarie Starck

Not so long ago, email was considered revolutionary. Being able to send a message to someone on the other side of the world and receive a reply in a matter of minutes was a game-changer.

Today, however, email is the slow-moving dinosaur of the Internet. Now, users expect instantaneous feedback from their online interactions and to communicate with people and machines instantly. If your app requires them to constantly refresh a web page to see the latest updates, they’ll soon be looking elsewhere.

But how has technology kept pace with these increasing demands?

One approach is the publish/subscribe architectural pattern (pub/sub for short). It’s a method of asynchronous communication that enables applications to publish messages to a channel, and any clients subscribed to that channel to receive them.

This tutorial will show you how to use pub/sub to implement live commenting in a blog. As soon as someone adds a comment to a blog article, anyone reading that article will see it immediately: no page refresh required. See it in action!

The ability to comment immediately on a blog article might not be something you’ve missed in the past, but it’s a simple, self-contained example of an application that uses a live experience to engage its audience. You can build upon what you learn here to implement other audience engagement features to add to your own applications.

The tech stack

To achieve this, we’re going to be using the following technologies:

Next.js

Next.js is a React framework that makes it easy to build fast and scalable web applications. React is an incredibly popular and powerful UI library, but requires extensive configuration. Next.js eliminates much of that work, and also provides developers with options on whether to render content on the client, server, or using a mixture of both.

It would be handy if you knew a little bit about Next.js before you start this tutorial, but it’s not strictly necessary because we’ll provide a good starting point for building your application and guide you through the rest. But the Next.js website has a great mini-course that will get you up to speed quickly and their docs are awesome.

PlanetScale

PlanetScale is a database-as-a-service platform. It allows developers to quickly create highly-scalable databases and work with those databases as if they were code in a Git repository. Developers can create branches for development and push those to production when they are working as intended. You’ll be using PlanetScale to store blog comments.

Prisma

Prisma is a Node.js and TypeScript ORM. It is a server library which helps developers write queries to work with databases using a familiar object-oriented approach. You will be using it to work with your PlanetScale database.

Ably

Ably is a feature-rich pub/sub messaging platform that empowers developers to create engaging digital experiences in realtime. It guarantees message delivery to all manner of devices at huge scales while preserving the order and integrity of messages. You will use it to publish blog comments to a channel and to subscribe to that channel.

Vercel

Finally, you’ll be using Vercel to host your application and make it available to the world. The nice folks at Vercel are the same people who brought you Next.js, making deployment of your application a breeze.

Now that we know what we’re going to build and what we’re going to build it with, let’s get started!

Tutorial Steps

Step 1: Create the Next.js application

There’s a lot of boilerplate code in this application, which we’ve created for you so that you can focus on incorporating the realtime elements. Start off by forking the Github repository for this tutorial.

clicking on the Fork button on the repository’s home page. A forked copy of that Git repository will be added to your personal GitHub or GitLab repo. That’s it.

git clone https://github.com/ably-labs/ably-next-prisma-planetscale.git

This repo contain two branches:

  • main - which contains the solution code for this tutorial
  • starter-project - which is the starting point for this tutorial

Check out the starter-project branch: you’ll be working with this branch for the whole of this tutorial. Use the main branch to compare your code with the solution code if you get stuck.

git checkout starter-project

The starter project gives you all the base components necessary to start implementing realtime communication and data persistence. It was built using MUI, previously known as Material-UI.

Take a look at the components folder and get a feel for which component does what.

Execute the following npm commands at your project’s root directory to install the core dependencies and launch the project:

npm install
npm run dev

Visit http://localhost:3000 in your browser and you’ll see your sample blog app, with some dummy content and a section for commenting:

Starter project: a simple article with comments

Next.js will continue to hot-reload the project as you develop, so as a general rule you can see it taking shape as you work through the remaining steps without having to restart the server. When you update certain configuration files you will have to restart the server, but we’ll let you know when that’s the case.

Step 2: Install the Ably React Hooks package

The realtime commenting that you will implement in this app is provided by Ably.

Next.js is powered by React and React can be a bit fussy about where and when you use code from libraries such as Ably’s. To make life simpler for React developers, Ably has developed the Ably React Hooks NPM module. Install it into your project now:

npm install ably @ably-labs/react-hooks

Step 3: Register for Ably

To use Ably’s service, you need to sign up for a free account and obtain an API key.

Your new Ably account comes with a pre-created project called Default, and your API key for that project will be displayed on your screen.

If, for any reason, you don’t see it, click the Default project in the dashboard and then select the API Keys tab at the top. Click the Show button to display the root API key (not to be confused with the Subscribe only API key, which will only let you read messages from a channel, and not publish to it). So make sure you get the right one.

Ably UI displaying the root API key

Step 4: Configure Ably React Hooks

In this step you will instantiate the Ably client and authenticate with the Ably platform.

First, create a file called .env in your project’s root directory and create a configuration key called ABLY_API_KEY which stores the API key you retrieved in the preceding step. Note that, even though this API key is an alphanumeric string, you don’t need to surround it with quotes.

ABLY_API_KEY=<paste your api key here>

One of the many great things about Next.js is that it supports API routes. These enable you to build API endpoints as Node.js serverless functions. This is really useful for us, because we can create an API endpoint that authenticates with the Ably service without having to expose our API key to the browser.

Implement an API endpoint called /createTokenRequest which will generate a token for you to authenticate with Ably on the client-side. Next.js makes it really easy to include API routes in your application. All you need to do is create the route handler in the pages/api directory, in a file of the same name.

In your Next.js application, create a createTokenRequest.js file in your pages/api folder. This route is then available as the /createTokenRequest endpoint.

Inside that file, create a new Ably client and use it to generate a new token:

import Ably from "ably/promises";

export default async function handler(req, res) {
  const client = new Ably.Realtime(process.env.ABLY_API_KEY);
  const tokenRequestData = await client.auth.createTokenRequest({
    clientId: "ably-blog-app",
  });
  res.status(200).json(tokenRequestData);
}

You can then use this endpoint to configure Ably on the client-side. But, if you were to do so using a relative, rather than absolute URL, this would generate an error due to a bug in one of its dependencies. It wouldn’t stop the Ably service from working, but it would soon clutter up your console. So let’s fix that now by creating an environment variable for the URL. This URL will change when we deploy to Vercel in a later step.

It also needs to be available to the browser, which it isn’t by default. Luckily, Next.js can help with that. Any environment variables prefixed by NEXT_PUBLIC_ are accessible on the client. So don’t ever put anything sensitive in there (such as your API key), but it’s a good place to configure your application’s host URL. Enter the following in your .env file:

NEXT_PUBLIC_HOSTNAME=http://localhost:3000

Now, head to your _app.js file and use configureAbly from @ably-labs/react-hooks to call your new authentication endpoint:

import { configureAbly } from "@ably-labs/react-hooks";
import "../styles/globals.css";

configureAbly({
  authUrl: `${process.env.NEXT_PUBLIC_HOSTNAME}/api/createTokenRequest`,
});

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

Step 5: Implement Pub/Sub

Now that you have the Ably library installed, you can work on the realtime elements.

Ably pub/sub is based on the concept of channels. Publishers send messages to a named channel and subscribers consume messages sent to that channel.

Ably pub/sub for comment updates

The Ably React Hooks NPM module provides a hook called useChannel. This hook subscribes to a specified channel when the component mounts and unsubscribes from it when the component unmounts.

The useChannel hook takes a callback function. This function is called every time a new message arrives on the channel so you can process it and make any necessary updates:

import { useChannel } from "@ably-labs/react-hooks";

const [channel] = useChannel("your-channel-name", (message) => {
    //Process the incoming message
});

Let’s talk about how you’ll use this in your application.

When a user submits a new comment on a blog post, your application will publish a message containing this comment to a channel called comment-channel.

Anyone else reading that blog post will see the new comment immediately, because they are subscribed to the channel and have therefore received and processed this new comment.

To do this, head to your Comments component and note that there is a state variable called comments. This will store your list of comments. This variable gets passed to the CommentsList component.

Create a submitComment() function and use channel.publish() to publish a new message to the channel. In Ably, a message has two parameters: name and data. For the data, you only need the commenter’s username and the text of the comment itself. Pass this submitComment() function to your AddCommentSection component.

Once this message is pushed, everyone subscribed to this channel, including the person creating the comment, will receive it, and it will be added to the list of comments in your state variable, thanks to the callback in useChannel.

Here is the code for Comments.js:

import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";

import Typography from "@mui/material/Typography";

import CommentsList from "./CommentsList";
import AddCommentSection from "./AddCommentSection";

export default function Comments({ initialComments = [] }) {
  const [comments, setComments] = useState(initialComments);

  const [channel] = useChannel("comment-channel", (message) => {
    // Add new incoming comment to the list of comments
    setComments((comments) => {
      return [...comments, message.data];
    });
  });

  const submitComment = async (username, comment) => {
    try {
      const body = { username, comment };
      await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });

      channel.publish({
        name: "comment",
        data: {
          username,
          comment,
        },
      });
    } catch (error) {
      console.error("An error occurred when creating a comment: ", error);
    }
  };

  return (
    <React.Fragment>
      <Typography variant="h6" gutterBottom>
        Comments ({comments.length})
      </Typography>
      <CommentsList comments={comments} />
      <AddCommentSection submitComment={submitComment} />
    </React.Fragment>
  );
}

Note: In this example, we're using Ably's React Hooks on the client to both publish and subscribe to a channel. In a production application, you would typically want publishing to be handled by the server. This would enable you to perform any required validation before a message is distributed to other users and persisted. For example, you might want to run the message through a profanity filter. You can use our REST SDK for server-side publishing.

Step 6: Connect the comment form

In your AddCommentSection component, first receive the Comment components submitComment function as props.

Create two new state variables: username and comment. Then, replace the calls to console.log()s in the component’s JSX markup with calls to the functions that set the username and state variables: setUsername() and setComment(), respectively. Also, set the corresponding TextField value attributes to {username} and {comment}

Finally, create an addComment() function that will use your new submitComment() function to publish the message and reset your form:

Here is the code for AddCommentSection.js:

import React, { useState } from "react";

import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
import Button from "@mui/material/Button";

export default function AddCommentSection({ submitComment }) {
  const [username, setUsername] = useState("");
  const [comment, setComment] = useState("");

  const addComment = () => {
    //Publish message
    submitComment(username, comment);
    //Reset form
    setUsername("");
    setComment("");
  };

  return (
    <FormControl>
      <Grid container spacing={2}>
        <Grid item xs={12}>
          <TextField
            required
            id="username"
            name="username"
            label="Username"
            variant="outlined"
            value={username}
            onChange={(event) => setUsername(event.target.value)}
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            required
            id="comment"
            name="comment"
            label="Comment"
            variant="outlined"
            multiline
            rows={4}
            value={comment}
            onChange={(event) => setComment(event.target.value)}
          />
        </Grid>
        <Grid item xs={12}>
          <Button variant="contained" onClick={addComment}>
            Submit
          </Button>
        </Grid>
      </Grid>
    </FormControl>
  );
}

Step 7: Try it out!

If your server isn’t already running, start it with npm run dev.

Open http://localhost:3000 in two separate browser tabs and try submitting comments from each. If everything is working correctly, then you should see new comments appearing instantly in both tabs.

The only issue is that if you refresh the page, you will lose all your comments. This is because your app is missing the last piece of the puzzle: data persistence. That’s what you’ll implement next.

Step 8: Create your PlanetScale database

In this tutorial, you will be using PlanetScale for a hosted database in which to store blog comments. To start with, head to https://planetscale.com/ and create a free account. Once you’re logged in, click the New Database button to create a database.

In the dialog that appears, name the database ably-realtime-db and choose the region closest to you to minimise latency.

Click the Create Database button and your database is ready to use:

UI of PlanetScale database creation

Once you’re done, go back to your terminal and install the PlanetScale CLI. This will enable you to run many helpful commands, such as opening a shell to query your database.

Step 9: Install and configure the Prisma ORM

In your application’s root directory, run the following command:

npx prisma init

This command does three things:

  • Adds a DATABASE_URL environment variable to your .env file with a dummy URL
  • Creates a prisma folder within your application
  • Add a schema.prisma file inside the prisma folder

The schema.prisma file configures your database URL and provider. You will also define your comment data model in here in a future step.

Right now, however, the DATABASE_URL in .env is a dummy one. So head back to the PlanetScale webpage. On the right side of your page, you should see a Connect button. Click on it and then on Generate Password. Select the Prisma format and you should see something like this:

datasource db { provider = "mysql" url = "mysql://******:************@******.region.psdb.cloud/database-name?sslaccept=strict" }

Copy this URL and add it to your .env file, but remove the double quotes, as this can cause problems with Vercel when you deploy your app in a later step:

DATABASE_URL=mysql://****:****@***.region.psdb.cloud/ably-realtime-db?sslaccept=strict

In your schema.prisma file, change the provider to mysql and enforce referential integrity as shown below. This step is necessary due to incompatibilities between Prisma and PlanetScale, because the latter doesn’t support foreign key constraints. To overcome this, we have to set the referentialIntegrity property:

generator client {
  provider = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
  referentialIntegrity = "prisma"
}

In this same file, define a data model for blog comments. It will be a very straightforward one with an autogenerated ID, a user name, and a comment:

model Comment { 
  id Int @default(autoincrement()) @id 
  username String
  comment String
}

Once your model is in place, sync your schema to your PlanetScale database. To start, connect to your database by opening a new tab in the terminal and running the following command:

npx prisma db push

If all goes well, you should see the following message:

?  Your database is now in sync with your schema. Done in 996ms
    ...
    ✔ Generated Prisma Client (3.9.1 | library) to ./node_modules/@prisma/client in 122ms

Check that your schema is working correctly by connecting to your database using the PlanetScale CLI. Specify the database name and the branch you are working on, as follows:

pscale shell ably-realtime-db starter-project

This will open a shell into your database where you can execute describe Comment; and see the following table definition:

ably-realtime-db/| starter-project |> describe Comment;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int          | NO   | PRI | NULL    | auto_increment |
| username | varchar(191) | NO   |     | NULL    |                |
| comment  | varchar(191) | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+

Type exit; to quit the shell.

Great! Your Prisma schema is now in sync with PlanetScale. The last step is to promote your current PlanetScale branch to be the production branch. To do this, execute the following:

pscale branch promote ably-realtime-db starter-project

Step 10: Install the Prisma client package

You have defined the connection details and data model in prisma.schema. The next step is to get your app to use this information to persist comment data.

First, stop your server if it’s still running, and install the Prisma client package:

npm install @prisma/client

Then, instantiate the Prisma client. Create a folder called lib and a file inside the folder called prisma.js for this:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
export default prisma;

Step 11: Create API routes for reading and writing comments to the database

In the same way that you built the /api/createTokenRequest route, create another route called /api/comment that your application can make GET and POST requests to. GET reads the existing comments from the database and POST writes a new comment to the database.

At the moment, the only methods you need to pay attention to are GET and POST. Anything else should return a 405 HTTP status.

Your GET endpoint should return a list of all your comments using a simple Prisma query. For the POST endpoint, your username and comment are contained within the body of the request, which you will use to create a new comment in your table.

In /pages/api/comment.js, enter the following code:

import prisma from "../../lib/prisma";

export default function handler(req, res) {
  switch (req.method) {
    case "GET":
      return handleGET(req, res);
    case "POST":
      return handlePOST(req, res);
    case "OPTIONS":
      return res.status(200).send("ok");
    default:
      return res.status(405).end();
  }
}

// GET /api/comment - retrieves all the comments
async function handleGET(req, res) {
  try {
    const comments = await prisma.comment.findMany();
    res.json(comments);
    res.status(200).end();
  } catch (err) {
    res.status(err).json({});
  }
}

// POST /api/comment - creates a new comment
async function handlePOST(req, res) {
  try {
    const comment = await prisma.comment.create({
      data: {
        ...req.body,
      },
    });
    res.json(comment);
    res.status(200).end();
  } catch (err) {
    res.status(err).json({});
  }
}

Step 12: Read existing comments from the database

You can now make a GET request to your /api/comment endpoint to fetch all the comments from your database. You can do that with getServerSideProps. This function is a really handy feature of Next.js that enables you to make an API call when the page is requested by the user. When the user first loads the page, the client will make an API call, fetch the saved comments, and pass them to props.

In pages/index.js, retrieve the comments from props and pass them to the Comments component to populate your list:

import Container from "@mui/material/Container";
import Grid from "@mui/material/Grid";
import CssBaseline from "@mui/material/CssBaseline";
import Divider from "@mui/material/Divider";

import Header from "../components/Header";
import Footer from "../components/Footer";
import Content from "../components/Content";
import Comments from "../components/Comments";

const Home = ({ comments = [] }) => {
  return (
    <div>
      <CssBaseline />
      <Container maxWidth="lg">
        <Header />
        <Grid container spacing={5} sx={{ mt: 3 }}>
          <Grid
            item
            xs={12}
            sx={{
              py: 3,
            }}
          >
            <Content />
            <Divider
              variant="middle"
              sx={{
                my: 3,
              }}
            />
            <Comments initialComments={comments} />
          </Grid>
        </Grid>
        <Footer />
      </Container>
    </div>
  );
};

export const getServerSideProps = async () => {
  const res = await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`);
  const comments = await res.json();
  return {
    props: { comments },
  };
};

export default Home;

Step 13: Write new comments to the database

In your Comments component, you can now save a new comment from your submitComment function. All you have to do is make a POST request with the username and comment in the request body.

Then, when you publish the message, all the subscribers can be notified and updated accordingly:

import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";

import Typography from "@mui/material/Typography";

import CommentsList from "./CommentsList";
import AddCommentSection from "./AddCommentSection";

export default function Comments({ initialComments = [] }) {
  const [comments, setComments] = useState(initialComments);

  const [channel] = useChannel("comment-channel", (message) => {
    // Add new incoming comment to the list of comments
    setComments((comments) => {
      return [...comments, message.data];
    });
  });

  const submitComment = async (username, comment) => {
    try {
      const body = { username, comment };
      await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });

      channel.publish({
        name: "comment",
        data: {
          username,
          comment,
        },
      });
    } catch (error) {
      console.error("An error occurred when creating a comment: ", error);
    }
  };

  return (
    <React.Fragment>
      <Typography variant="h6" gutterBottom>
        Comments ({comments.length})
      </Typography>
      <CommentsList comments={comments} />
      <AddCommentSection submitComment={submitComment} />
    </React.Fragment>
  );
}

Run your server again using npm run dev and try adding some comments.

Then, open another terminal window and execute the following command:

npx prisma studio

This will open http://localhost:5555/ in a new browser tab where you can see and manipulate the contents of your Comment table:

Prisma Studio showing some data in the comments table

That’s it! You are now ready to share the results of your work with the world. You’ll do that in the final step, by deploying your application to Vercel.

Step 14: Deploy your app to Vercel

Before you deploy your app, you’ll have to do a bit of tidy up to deal with the inevitable CORS issues that arise when you host any web application.

In next.config.js, add the following HTTP headers. These specify which origins, methods, and other features your requests will support:

/** @type {import('next').NextConfig} */

const nextConfig = {
  reactStrictMode: true,
  async headers() {
    return [
      {
        // match all API routes
        source: "/api/:path*",
        headers: [
          { key: "Access-Control-Allow-Credentials", value: "true" },
          { key: "Access-Control-Allow-Origin", value: "*" },
          {
            key: "Access-Control-Allow-Methods",
            value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
          },
          {
            key: "Access-Control-Allow-Headers",
            value:
              "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

You also need to support the OPTIONS method, which is used as a preflight check in API requests. In your pages/api/comment.js file, add handling for OPTIONS by returning HTTP status 200:

export default function handler(req, res) {
  switch (req.method) {
    case "GET":
      return handleGET(req, res);
    case "POST":
      return handlePOST(req, res);
    case "OPTIONS":
      return res.status(200).send("ok");
    default:
      return res.status(405).end();
  }
}

Once you’re happy that your application is working correctly, commit and push your starter-project branch.

git checkout starter-project
git add .
git commit -m "Final code"
git push

If you don’t already have one, create an account with Vercel.

Once you’re logged in, click on New Project, and import the Git repository where your code lives. Copy the URL that Vercel has generated for you, which should be something like https://[project-name].vercel.app/.

Edit your .env file NEXT_PUBLIC_HOSTNAME setting to point to the Vercel URL instead of http://localhost:3000. Commit and push the change:

git add .
git commit -m "Configure deployment URL"
git push

You’re currently still working on the starter-project branch, so for now set that as the Production branch in Vercel.

To do this, click your Github project in the Overview page, select the Settings tab and then the Git page from the left-hand navigation menu. Change the Production Branch to starter-project, then click Save.

Then, visit the Deployments tab, click the three little dots to the right of your deployment and select Redeploy.

Once the deployment is complete, your app should be up and running!

Displaying realtime communication in a live environment

Next steps

Now that you've got your Next.js application up and running, speaking to your database, and working nicely with Ably's realtime messaging platform, you should consider performance.

Next.js renders pages using a method of your choice, and selecting the correct rendering method will help you optimise the performance of statically-generated websites.

One good contender might be on-demand Incremental Static Regeneration (ISR). This would enable you to recreate the comment page as required, without triggering a rebuild of your whole site.

Conclusion

In this tutorial, you learned how to implement pub/sub messaging in realtime using the Ably platform within a Next.js application. You also learned how to create a database with PlanetScale and how to fetch and write data to it with the Prisma ORM. Finally, you used Vercel to deploy your application in a live environment.

Realtime communication might still be new for some developers, but the public is increasingly demanding instant feedback on all interactions they have online. You’re now in a great position to give them what they want, whatever the nature of your application or service.

We’d like to acknowledge the significant contribution to this article by Mark Lewin, who is a Senior Developer Educator here at Ably.

Join the Ably newsletter today

1000s of industry pioneers trust Ably for monthly insights on the realtime data economy.
Enter your email