Slack powers collaboration for over 700,000 businesses, so if you’re looking for ways to provide value to your app’s users, a Slack integration is something worth considering. But adding Slack support to an existing app has historically been a large engineering task: you need to map your app’s data model onto Slack’s users and channels, provide a way to authenticate and store credentials, manage notification templates in domain-specific formats like mrkdwn and Block Kit, and then create UI to handle the self-serve aspects of this process.

At Knock we’ve seen how quickly engineering teams can move when given the right primitives and abstractions. Using our KnockFeedProvider component and a few lines of code, developers can implement a fully-featured notification feed in less than an hour.

We’re excited to create a similar experience for Slack notification channels using SlackKit, a library of drop-in components powered by the KnockSlackProvider. Using components like the SlackAuthButton and SlackChannelsCombobox or our React hooks, engineering teams can add support for Slack channel notifications in Knock in hours, not days or weeks.

Knock customers already use these patterns to power Slack workflows in production, but the addition of SlackKit components simplifies the implementation for developers. In this post, we'll take a high-level look at SlackKit in action and walk through an example Next.js application that implements it.

Getting started

For this project, there are quite a few configuration variables you need before you can get started. Some of these are typical environment variables you would supply to your application, like API keys, tokens, or client ids.

The other set of values would typically be determined by your product's business logic, but you can hardcode these values for the time being.

You can access the repository for this project directly or clone it to your local machine using the following command:

git clone https://github.com/knocklabs/slack-kit-example.git

Video walkthrough

If you prefer to learn with video, watch it here.

Setting up a Slack app

You can either follow the setup steps outlined here, or use the official instruction in the SlackKit docs.

Create a new app

First, visit https://api.slack.com/apps and sign into your account. Then click Create new app and select the From scratch option. Next, select which workspace to develop it in. You'll still be able to use it in other workspaces, so this selection isn't critical.

create new app

Add bot features

Under Add features and functionality select Bots features. Then, under OAuth and Permissions, give it the channels:read scope. It doesn’t really need any scopes here since we’ll be sending the scopes we need from the component, but we need to do this so we can expose the redirect url form.

create Slack scopes

Add redirect URL

Also under OAuth and Permissions, find the redirect URL section and add this Knock URL to that field: https://api.knock.app/providers/slack/authenticate. Knock's API endpoint will handle the OAuth callback for you and store a Slack access token. Finally, Under Manage distribution, allow it to be publicly distributed.

save redirect urls

Setting up Knock

For this tutorial, you'll need a Knock account. If you don't already have one, you can sign up on this page. You can also find these steps outlined in the official SlackKit docs.

Create a Slack channel

Add a Slack channel in Knock with the Client Id and Client Secret from the Basic Info section of your Slack. Take note of this channel id for use in the next step.

create slack channel

Create a new workflow

Create a new workflow with a Slack channel step pointing to this Slack channel. Take note of this workflow key for use in the following steps. In the message template use the following liquid tag to test your ability to send messages: A new issue was submitted: {{message}}

create new workflow

Environment variables

All of these values are sourced from environment variables at runtime. The example app will check for these values as the first step. Make a copy of .env.sample using the following command: cp .env.sample .env.local

NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID

This value comes from Knock after you create a Slack channel in the dashboard.

NEXT_PUBLIC_SLACK_CLIENT_ID

This value comes from Slack when you create a new Slack app. You can find it in your app's 'Basic Info' section.

KNOCK_SIGNING_KEY

This value comes from Knock and is used to sign a JWT on behalf of a user to store channel data for Slack tokens and channel ids. You can generate a signing key under "Developers" > "API keys." Use the PEM encoded version. This is a secret value and should not be exposed publicly.

KNOCK_API_KEY

This value comes from Knock and is used to authenticate server-side API requests. You can find it listed as the secret key under "Developers" > "API keys." This is a secret value and should not be exposed publicly.

NEXT_PUBLIC_KNOCK_CLIENT_ID

This value comes from Knock and is used to authenticate public API requests from the browser. You can find it listed as the public key under "Developers" > "API keys."

NEXT_PUBLIC_KNOCK_API_URL

This value comes from Knock and is used to construct the URL for API endpoints. You can keep the default value for this.

NEXT_PUBLIC_REDIRECT_URL

This value comes from your application. It is where Knock will redirect your user after the OAuth flow with Slack. The default of http://localhost:3000 is valid when running this project locally. |

Knock resource variables

To make the connection between Slack channels and Knock objects, you'll also need to provide details for several types of resources in Knock. To do this, you can replace the values in the getAppDetails function inside of the /app/lib/app-details.ts file. These values would typically be determined by your application's business logic, but we can hardcode them for this example:

export function getAppDetails() {
  return {
    tenant: "knock-projects",
    collection: "repositories",
    objectId: "repo-2",
    userId: "123",
    workflowKey: "new-issue",
  };
}

You should already have a value for workflowKey from a previous step, and you can choose a user identifier to use from the dashboard.

Create a tenant

In SlackKit, tenants are used to store the access token for an organization's Slack workspace. You can create a new tenant from the dashboard and include it's ID as the value for the tenant property in the getAppDetails function. You can also use this cURL command to create a tenant by replacing the values for tenant-id, KNOCK_API_KEY, and tenant-name:

curl --location --request PUT 'https://api.knock.app/v1/tenants/<tenant-id>' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <KNOCK_API_KEY>' \
--data '{
  "name": "<tenant-name>",
}'
💡

You do NOT have to create your tenant before using SlackKit. If you pass a tenant ID into the KnockSlackProvider that doesn't exist, SlackKit will create it for you on the completion of the OAuth process. However, tenants are an important concept in Knock so we'd recommend reading about these key concepts in the SlackKit overview.

Create an object

In SlackKit, objects are used to store channels and act as the recipient of your workflows. You can't create a new object from the dashboard, so you can use this cURL command to create an object by replacing the values for object-collection, object-id, KNOCK_API_KEY, and object-name:

curl --location --request PUT 'https://api.knock.app/v1/objects/<object-collection>/<object-id>' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <KNOCK_API_KEY>' \
--data '{
    "name": "<object-name>"
}'

Once you've done that, update the values in the getAppDetails function to point to your new object.

💡

You do NOT have to create your object before using SlackKit. If you pass an object ID into the SlackChannelsCombobox that doesn't exist, SlackKit will create it for you on the completion when the user registers a channel. However, objects are an important concept in Knock so we'd recommend reading about these key concepts in the SlackKit overview.

Running the app locally

Now that you have all of the configuration out of the way, you can install your dependencies using one of the following commands:

npm install
#or
yarn install

After your dependencies have installed, you can run the app in dev mode:

npm run dev
#or
yarn dev

If you open the app in a browser at http://localhost:3000 you should see a screen that looks like this:

confirm env vars

This step gives you an opportunity to confirm that you have all of the necessary environment variables needed to use the application.

If you click Next you'll navigate to a screen where you can confirm the Knock resources you'll use to connect your Slack app:

confirm knock resources

If all of the values in this step look good, you can click Next to authenticate with your Slack app and store the access_token in Knock.

Sharing Knock and Slack state

Before we dig into the actual SlackKit UI components and their functionality, let's look at how we share certain values across those components in our application.

In the @knocklabs/react package, Knock already exposes a KnockProvider component that is meant to provide top-level authentication and authorization to components farther down in the component tree.

With the SlackKit release, the @knocklabs/react package now also contains a Slack-specific provider, called KnockSlackProvider. Since these both rely on React context, they are implemented as client-side components in /app/components/providers.tsx in the following manner:

"use client";

import { KnockProvider, KnockSlackProvider } from "@knocklabs/react";

export default function Providers({
  userToken,
  knockUserId,
  tenant,
  children,
}: {
  userToken: string;
  knockUserId: string;
  tenant: string;
  children: JSX.Element;
}) {
  return (
    <>
      <KnockProvider
        apiKey={process.env.NEXT_PUBLIC_KNOCK_CLIENT_ID!}
        userId={knockUserId}
        host={process.env.NEXT_PUBLIC_KNOCK_API_URL}
        userToken={userToken}
      >
        {/*
        The KnockProvider handles authentication with Knock, while the KnockSlackProvider
        provides shared context to all Slack-related components.
        Both are required for Slack-related apps.
        */}
        <KnockSlackProvider
          knockSlackChannelId={process.env.NEXT_PUBLIC_KNOCK_SLACK_CHANNEL_ID!}
          tenant={tenant}
        >
          {children}
        </KnockSlackProvider>
      </KnockProvider>
    </>
  );
}

To create some of the necessary authentication data, like userToken and userId, we implement the Providers component inside of the root layout.tsx file. In this code, which runs on the server, we also add some additional grants to the userToken JWT to allow the user to interact with the Slack resources stored in Knock. You can read more about these resource access grants in the docs.

🚨

Note: this is necessary because the user in this context is an end user in your application who does not have access to Knock as a member of the account. Therefore, these grants provide them elevated privileges to operate on specific resources using the API.

import { Knock } from "@knocklabs/node";
import { Grants } from "@knocklabs/node/dist/src/common/userTokens";
import "@knocklabs/react/dist/index.css";

import Providers from "./components/providers";
import "./global.css";
import { getAppDetails } from "./lib/app-details";

const { userId, tenant, collection, objectId } = getAppDetails();

const signingKey = process.env.KNOCK_SIGNING_KEY!;

async function MyApp({ children }: { children: React.ReactElement }) {

	const userToken = signingKey
		? await Knock.signUserToken(userId, {
				grants: [
					Knock.buildUserTokenGrant({ type: "tenant", id: tenant }, [
						Grants.SlackChannelsRead,
					]),
					Knock.buildUserTokenGrant(
						{ type: "object", id: objectId, collection: collection },
						[Grants.ChannelDataRead, Grants.ChannelDataWrite],
					),
				],
			})
		: "secretOrPrivateKey";

	return (
		<>
			<html>
				<body className="px-12 py-6">
					<h1 className="text-2xl font-bold mb-6">SlackKit Demo App</h1>
					<Providers userToken={userToken} knockUserId={userId} tenant={tenant}>
						{children}
					</Providers>
				</body>
			</html>
		</>
	);
}

export default MyApp;

Authenticating with Slack

On this screen, you can initiate the OAuth flow with Slack. This page uses two SlackKit components to help facilitate this interaction: SlackAuthButton and SlackAuthContainer.

initiate oauth flow

Both of these components are client-side components. The SlackAuthButton component takes your redirectUrl and slackClientId as props:

<SlackAuthContainer
    actionButton={
        <SlackAuthButton
        slackClientId={process.env.NEXT_PUBLIC_SLACK_CLIENT_ID!}
        redirectUrl={process.env.NEXT_PUBLIC_REDIRECT_URL}
        />
    }
/>

When this button is clicked, it will initiate an OAuth flow in a pop-up window where the user can authenticate with Slack and choose a workspace to install the app. Once the flow is complete, the pop-up window should close and the state of the component should update to show it is Connected to Slack. Behind the scenes, Knock handles the OAuth callback from Slack and stores an access_token on the tenant you provided to the KnockSlackProvider as channel data.

🚨

Note: if the pop-up window does not close, double check that the redirectUrl matches your current environment.

From here, you can click Next to choose one or more Slack channels to associate with your object recipient.

Choosing Slack channels

On this route, you can associate your Knock object recipient with one or more Slack channels using the SlackChannelCombobox component. It accepts a slackChannelsRecipientObject as a prop that specifies a collection and objectId for the recipient object:

"use client";

import { SlackChannelCombobox } from "@knocklabs/react";

export default function SlackChannelWrapper({
  collection,
  objectId,
  className,
}: {
  collection: string,
  objectId: string,
  className: string,
}) {
  const slackChannelsRecipientObject = {
    objectId: objectId,
    collection: collection,
  };
  return (
    <div className={className}>
      <SlackChannelCombobox
        slackChannelsRecipientObject={slackChannelsRecipientObject}
      />
    </div>
  );
}

As you select channels using the combobox input, the component will update the channel data for that object to store the Slack channel ids.

choosing slack channels
🚨

Note: if you want to use a private Slack channel, you need to make sure that the Slack Bot has been invited to that channel.

At this point, Knock has all of the information it needs to send Slack messages to your selected channels. Click Next to examine the channel data for your object and tenant.

Examine channel data

In this step, you're not required to take any action, but this page uses the Node SDK to pull channel data for your object recipient and tenant. This will give you an idea of how Knock is storing the data and these two abstractions interact.

This data is pulled dynamically, so if you make different channel selections or disconnect and reconnect your Slack auth, the value for access_token and connections.channelIds will change as well.

If you click Next you can test your workflow end-to-end.

Trigger your workflow

In this step, you can see a code sample using the Node SDK of how you'd trigger your workflow. This is actually the code that runs in a server action when you submit the form on this page. Go ahead and submit a form to send a Slack message into your designated channels:

a form to trigger our workflow

Wrapping up

If you need help implementing any of the code in this example app, consider joining our Slack community. We've got over 500 other developers in there learning how to improve their notification experiences with Knock.

Thanks for reading and Knock on 🤘