I've been looking at PubNub as a potential alternative to Firebase for push notifications. At a high level, PubNub is pubsub-as-a-service: once you plug in PubNub's JavaScript library, you can start publishing messages and subscribing to channels without having to deploy Kafka, ZeroMQ, etc.

Like Firebase, PubNub handles client-side socket connections for you, so your client apps don't have to worry about retrying failed HTTP requests and maintaining connection state. Unlike Firebase's realtime database, PubNub does not maintain state for you. If you subscribe to a channel after a message is sent, you will not get that message (modulo PubNub's replay), whereas in Firebase you can always get the last state.

In this article, I'll walk you through a small realtime chat app I built with PubNub and Next.js. You can find the chat app on GitHub, below is a demo video:

Setting Up PubNub

Creating a PubNub instance is simple. There are 2 keys: a publishKey and a subscribeKey . The publishKey gives you permission to publish to channels, and the subscribeKey gives you permission to subscribe to channels. PubNub has more sophisticated security settings, but these two keys are sufficient for this example.

const PubNub = require ( 'pubnub' ); const { publishKey, subscribeKey } = require ( './config' ); const pubnub = new PubNub({ publishKey, subscribeKey });

The above code works well on both the client and the server. Note that I used require() above. If you're using ES6 imports, you need to do import pubnub from 'pubnub'; , import * as PubNub from 'pubnub'; will not work.

The pubnub object above has 2 methods you'll be concerned with in this article, publish() and subscribe() . The publish() method lets you send a message to a channel:

const message = { client: 'Brian K.' , content: 'Hello, World!' }; pubnub.publish({ channel: 'messages' , message });

The subscribe() method lets you register a callback that will be called every time a message is received on a given channel:

pubnub.subscribe({ channels: [ 'messages' ] }); pubnub.addListener({ message: ({ message }) => { console .log( ` ${message.client} : ${message.content} ` ); } });

The above code is fully isomorphic, so your server can publish() a message and any client that called subscribe() on that channel will receive it. Communicating between client and server becomes very easy when you don't have to worry about failed HTTP requests and potentially disconnected sockets!

Integrating With Next.js

Here's how I set up the server-side code that handles serving up the client-side Next.js app:

const body = require ( 'body-parser' ); const express = require ( 'express' ); const next = require ( 'next' ); const app = next({ dev: true }); const handle = app.getRequestHandler(); const PubNub = require ( 'pubnub' ); const { port, publishKey, subscribeKey } = require ( './config' ); const pubnub = new PubNub({ publishKey, subscribeKey }); async function start ( ) { await app.prepare(); const server = express(); const messages = []; server.use(body.json()); server.use((req, res, next) => { req.state = { messages }; next(); }); server.get( '/message' , ); server.post( '/message/:client' , ); server.get( '*' , (req, res) => { return handle(req, res); }); server.listen(port); console .log( `Listening on ${port} ` ); } start().catch(error => console .error(error.stack));

There's separate REST-ish endpoints for getting a list of messages and publishing a new message. In this example, the client does not have permission to push to the 'messages' channel. The API sits as a layer in front of PubNub to make sure every message published is properly formatted. In this case, the validation is simple and not terribly robust:

server.post( '/message/:client' , (req, res) => { const message = { client: req.params.client, content: req.body.message }; messages.push(message); pubnub.publish({ channel: 'messages' , message }); res.json({ message }); });

You can imagine a more sophisticated validation layer, but I'm skipping that in the interest of keeping this example lean. The Next.js app lives in a single component, pages/index.js :

const PubNub = require ( 'pubnub' ); const React = require ( 'react' ); const config = require ( '../config.client.json' ); const superagent = require ( 'superagent' ); const clientColors = [ 'FF0B69' , '1DACCC' , '1195B2' , 'FFEB25' , 'ccbc1d' ]; export default class extends React . Component { static async getInitialProps({ req }) { } constructor () { } componentDidMount() { this .pubnub = new PubNub({ subscribeKey: config.subscribeKey }); this .pubnub.subscribe({ channels: [ 'messages' ] }); this .pubnub.addListener({ message: ({ message }) => { this .setState( Object .assign({}, this .state, { messages: ( this .state.messages || this .props.messages).concat([message]) })); } }); } componentWillUnmount() { this .pubnub.unsubscribe(); } submitMessage() { return ev => { superagent.post( `/message/ ${this.client} ` , { message: this .state.input }). then(() => { this .setState( Object .assign({}, this .state, { input: '' })); }) } } }

Conclusion

PubNub looks like a great tool for pushing state changes from the server to the client. Unlike socket.io, it's a hosted solution, so you don't have to worry about the nuances of scaling websocket connections. Unlike Firebase, PubNub is message based, so you don't have to worry about managing persistent state. Give it a shot next time you're looking to build push notifications into an app and let me know what you think!

Got JavaScript fatigue? There's better ways to fight it than reaching for yet another crappy opinionated app starter. I've been experimenting with Bulletproof's new Neuromaster supplement (non-affiliate link) during my work-from-home days. It's helped me stay motivated and focused even as distractions and interruptions pile up.