Published on May 31, 2024 (22 days ago)

How to build video activity notifications in Next.js

Jeff EverhartDave Kiss
By Jeff and Dave9 min readEngineering

Video applications tend to generate a lot of notifications. When someone watches your Loom video, you get an email. When your stream goes live, all of your followers get push notifications. When you publish a new video or short, all of your subscribers get a message in their in-app feed.

For video-based use cases, these notifications all serve the critical purpose of driving eyeballs to your application’s content. But let’s be honest: most people can’t stand notifications. Either they don’t consider a user’s preferences, get sent too frequently for the activities that generate them, or they go to the wrong channels. People mute, unsubscribe, or just tune out, and your main method of driving engagement for your video app tanks.

Doing this right and at scale is a hard engineering problem. In the same way Mux exists to solve your video infrastructure woes, Knock exists to provide you with a fully-featured notification infrastructure out of the box. Let’s explore how you can use Mux and Knock to build out scaleable, thoughtful, and battle-tested notifications for your video apps.

LinkWhat is Knock?

Knock provides notification infrastructure that helps you implement notifications your users will love, without the effort of building and maintaining your own in-house notifications system. Knock helps product teams craft cross-channel notification experiences that employ batching, delays, and throttling using a standardized templating system and a drag-and-drop workflow builder. Engineering teams benefit from code-level abstractions and custom components, combined with the reliability of a custom-built platform that supports automatic retries and deep observability.

LinkHow to send video view notifications

To get started, we’ll look at a pattern implemented by a lot of video-based applications, particularly those based around collaboration or sales. When you create and share a video with a tool like Loom or Vidyard, you can get notifications when an internal or external user watches your video.

Using the MuxPlayer component, we can implement a quick video-view notification workflow. This example uses a Next.js client component where we attach a custom onPlay event handler to the MuxPlayer.

'use client'; import MuxPlayer from '@mux/mux-player-react'; export default function Player() { return ( <MuxPlayer playbackId="O6LdRc0112FEJXH00bGsN9Q31yu5EIVHTgjTKRkKtEq1k" onPlay={callOnPlay} /> ); } // We can send data about our video and the person watching it // to a Next.js API endpoint async function callOnPlay() { await fetch('/api/knock/on-play', { method: 'post', body: JSON.stringify({ videoId: 'test video', user: 'test user' }), }); }

This callOnPlay function sends a POST request to an API endpoint where we pass both videoId and user values as a part of the payload. When the Next.js API route process that request, we’ll use Knock’s Node SDK to trigger a workflow using that payload data:

trigger workflow
import { Knock } from '@knocklabs/node'; // The Knock SDK allows you to interact with Knock's backend services const knockClient = new Knock(process.env.KNOCK_SECRET_KEY); export async function POST(request: Request) { const body = await request.text(); // In a real app, you might make a database call to lookup the // video owner const videoOwner = { id: 'a7jadfhk23', email: '' } // The workflows.trigger method runs a workflow for each recipient knockClient.workflows.trigger('new-view', { recipients: [videoOwner], data: JSON.parse(body), }); return Response.json({ message: 'ok' }); }

LinkHow to build advanced video notification workflows

Workflows in Knock are how we orchestrate notifications around a given topic. They allow us to combine multiple notification channels, like an in-app feed, email, SMS, or push, with different logical function steps that control the execution of the workflow, allowing us to delay, batch, branch, or skip certain steps based on conditions you define.

The knockClient.workflows.trigger method takes the key of your workflow as well as some information about the recipients who will receive notifications via the workflow and any custom data you want to pass into this workflow run.

In Knock, these workflows are created either using a drag and drop workflow builder, or by defining your workflow steps using a JSON schema. In most cases, product teams adopt the workflow builder as a way to lift notification workflow design out of the codebase. This allows product teams to make changes to workflows and message templates without needing an engineer to ship code changes.

We can see that this particular example sends a notification to an in-app feed, which is a real-time service managed by Knock.

LinkImplementing a real-time video notification feed

To render this in-app feed inside of a Next.js application, you can use Knock’s drop-in React components:

'use client'; import { useState, useRef, useEffect } from 'react'; import { KnockProvider, KnockFeedProvider, NotificationIconButton, NotificationFeedPopover, BellIcon, } from '@knocklabs/react'; // Required CSS import, unless you're overriding the styling import '@knocklabs/react/dist/index.css'; const FeedContainer = () => { // These state variables control the display of our feed popover const [isVisible, setIsVisible] = useState(false); const notifButtonRef = useRef(null); return ( {/* The KnockProvider handles authenticating Knock feed requests, it can optionally take a server-signed token for extra security */} <KnockProvider apiKey={process.env.NEXT_PUBLIC_KNOCK_API_KEY as string} userId={process.env.NEXT_PUBLIC_KNOCK_USER_ID as string} > {/* The KnockFeedProvider makes a connection to a feed channel. In some cases, you may want more than one */} <KnockFeedProvider feedId={process.env.NEXT_PUBLIC_KNOCK_FEED_CHANNEL_ID as string} colorMode="dark" > <> <NotificationIconButton ref={notifButtonRef} onClick={(e) => setIsVisible(!isVisible)} /> <NotificationFeedPopover buttonRef={notifButtonRef} isVisible={isVisible} onClose={() => setIsVisible(false)} /> </> </KnockFeedProvider> </KnockProvider> ); }; export default FeedContainer;

The NotificationFeedPopover component works in conjunction with the KnockProvider and KnockFeedProvider to render a real-time feed experience. The feed components manage the WebSocket connection to Knock’s feed API and provide you with ways to manage message statuses like seen, read, and archived. It supports dark mode 😎but also provides lower-level React hooks or a vanilla JS client so you can attach this real-time feed to any UI elements or framework code you want.

What if this video went viral and accumulated five thousand views in an hour? What would our user’s inbox look like?

Using the features of the workflow builder, we can move away from naive notifications where we send an in-app message and an email for every view.

If users spend a lot of time in your app during the day, are all the emails they get just immediately deleted because they’ve already seen the in-app feed?

Here are just a few options we could build using the workflow builder to level up the notification experience for you users:

  1. Add a batch step before our in-app feed step: this would catch all of the new-view notifications and batch them by videoId so that your users would get one message with data from multiple activities
  2. Add a delay step and then step condition to the SendGrid email step: this would only send an email notification if the previous in-app notification hadn’t been seen within 3 hours
  3. Allow users to manage their preferences for the new-view workflow by channel type: this would allow a user to indicate that they don’t want to receive emails from the new-view workflow, which means Knock would automatically skip that step for that user

Let’s look at another popular video use case built around Mux webhooks for video publishing and streaming.

LinkHandling video subscriber notifications with Webhooks

In addition to the custom components and APIs we’ve already looked at, Knock also provides lots of data abstractions that let you map parts of your application’s data model into Knock. In this example, we’ll look at how to model video or live stream subscription using Knock.

Knock has a concept called Objects, which is basically a lightweight NoSQL data store for whatever entities are important to your application. Then, using a concept called a Subscription, your application can create a relationship between a user and an object to simplify the notification process.

Let’s take a look at an example using Knock and Mux to build YouTube-inspired channels:

import { Knock } from '@knocklabs/node'; const knockClient = new Knock(process.env.KNOCK_SECRET_KEY); // Knock Objects require an id, live in a collection, and have any number of properties const channels = [ { id: 'knock', properties: { name: 'Knock', icon: '🔔', YouTube: '', Twitter: '', }, }, { id: 'mux', properties: { name: 'Mux', icon: '📹', YouTube: '', Twitter: '', }, }, ]; // Here we create a new 'channel' object for each array item channels.forEach(async (channel) => { await knockClient.objects.set('channels',,; });

This code sample creates an object for both entities inside of a channels collection, where each channel can hold an arbitrary number of properties. Ideally the id of these objects would map back to a channelId or similar property in your own application to make referencing them easy.

LinkWorking with Knock objects and subscriptions

Once we’ve created some channels we add user subscriptions to them using the Node SDK:

import { Knock } from '@knocklabs/node'; const knockClient = new Knock(process.env.KNOCK_SECRET_KEY); // Our subscriptions contain a required id that references a user // and optionally properties that store metadata about the subscription const subscriptions = [ { id: '9d17eb2d-1980-4614-bcfe-5696ca9be631', properties: { role: 'regular', }, }, { id: 'a12896b9-658b-43ff-8d5b-79c34215778a', properties: { role: 'allAccess', }, }, ]; // This looks the same as the example channels above, excluded for brevity const channels = ["channel objects here"] // Here we loop through each channel and subscribe our users channels.forEach(async (channel) => { subscriptions.forEach(async (subscription) => { await knockClient.objects.addSubscriptions('channels',, { recipients: [], properties:, }); }); });

Each subscription consists of a user id and custom properties we want to store on that subscription, like a regular versus an allAccess user.

By allowing Knock to store this relationship between the channel and subscriber it simplifies the process of notifying them about events related to the channel. For example, let’s say the Knock channel has created a new stream event, and Mux sends our application a webhook alerting us of the activity:

import { revalidatePath } from 'next/cache'; import { headers } from 'next/headers'; import Mux from '@mux/mux-node'; import { Knock } from '@knocklabs/node'; const knockClient = new Knock(process.env.KNOCK_SECRET_KEY); const mux = new Mux({ webhookSecret: process.env.MUX_WEBHOOK_SECRET, }); export async function POST(request: Request) { // We first validate and unwrap the Mux webhook payload const headersList = headers(); const body = await request.text(); const event = mux.webhooks.unwrap(body, headersList); // Then we trigger different workflows based on event.type switch (event.type) { case '': console.log('📺 live stream started.....'); // We fetch object data about the channel that just started a stream const channel = await knockClient.objects.get('channels', 'knock'); // Then we trigger a workflow using that object as a recipient, and pass in the rest of the channel data as a payload const { workflow_run_id } = await knockClient.workflows.trigger( 'new-stream', { recipients: [{ collection: 'channels', id: 'knock' }], data: { channel, }, } ); break; case 'video.asset.ready': console.log('🎞️ new asset ready.....'); console.log(`📨 new workflow run started: ${workflow_run_id}`); break; default: break; } return Response.json({ message: 'ok' }); }

We unpack the webhook request from Mux, then look at the event.type to trigger the correct workflow. Now, instead of making a database call to query for all of the subscribers on that particular channel, we trigger our workflow using the channel object as a recipient, and optionally pass in data about the channel itself.

const { workflow_run_id } = await knockClient.workflows.trigger( 'new-stream', { recipients: [{ collection: 'channels', id: 'knock' }], data: { channel, }, } );

LinkCustomizing notifications by subscription properties

When Knock processes this workflow, it fans out to all of the subscribers to this particular object and initiates a workflow run for each of them. Inside the workflow, we can use the data stored on the subscription to customize a template, create branches, or customize step conditions: recipient.subscription.role

For example, when our application receives a new webhook from Mux, our new-stream workflow could do the following things:

  1. Add an extra section to the email notification if the recipient is an allAccess member
  2. Create a branch based on recipient.subscription.role where allAccess members get an immediate notification, but regular members have a fifteen minute delay

LinkWrapping up

If you’re interested in adding notification support to your own video project, you can check out this open source repository for the full code samples that implement the example features outlined in this post.

Knock also provides backend SDKs in Python, PHP, Go, .NET, Elixir, Ruby and Java so you can trigger notifications from any application stack. With the drop-in React package and vanilla JavaScript client, you can integrate with Knock’s real-time feed infrastructure in any frontend framework.

To get started with Knock, you can sign up for a free tier that supports up to 10,000 monthly notifications and join a Slack community dedicated to helping you level-up your notifications.

Written By

Dave Kiss

Was: solo-developreneur. Now: developer community person. Happy to ride a bike, hike a hike, high-five a hand, and listen to spa music.

Leave your wallet where it is

No credit card required to get started.