Sending a direct message to a user in Slack

In this guide we'll cover how to create a self-serve Slack integration for a multi-tenant application using Knock. It assumes that you have already created a Slack app and created a Slack channel in Knock as outlined in the Slack integration overview guide.

In this implementation, your application's users will connect their Slack workspace to Knock and be able to send messages to individual users via DM. To make this easier to implement, we'll use Knock's SlackKit components to facilitate the OAuth flow.

Here's what we'll cover in this guide:

  • Modeling a multi-tenant application in Knock using Tenants
  • Implementing a Slack OAuth flow using Knock's SlackAuthButton component
  • Using the access_token stored in Knock to request a user's Slack ID
  • Triggering a workflow with a user recipient to send a message to a DM

Key concepts

SlackKit connects multiple concepts in Knock to make it easier for your application's users to create a Slack integration. Tenants are a concept you'll see throughout the following docs that are foundational to how SlackKit works, but might not be used in every implementation of Knock.

About tenants

Tenants in Knock are meant to represent groups of users who typically share the same resources. You might call these "accounts," "organizations," "workspaces," or something similar. In a standard SlackKit implementation, you'll store a Slack workspace's access_token on a corresponding tenant in Knock.

If you already use Knock's tenant concept to power other 'account-based' features, you likely create tenants in Knock when an account or organization is created in your application. If you don't already use tenants in Knock, SlackKit can create tenants for you on the fly if they don't already exist.

💡
Note: Our best-practice recommendation is that tenants in Knock should map one-to-one to whatever abstraction you use to model accounts, organizations, or workspaces. You can think of tenants as the top-level container within your data model that you use to power multi-tenancy in your application.

Merging channel data

In this implementation, we'll actually store the required channel data for a SlackConnection across two different entities in Knock: a Tenant and an User. This is because we want to store the access_token for the Slack workspace on the Tenant and the user_id for the Slack user on the User.

When you trigger a workflow using this recipient and tenant, Knock will merge the channel data from the Tenant and the User to send the message to the correct Slack DM channel. By storing the access_token on the Tenant, your customers only need to complete the OAuth flow once to connect their Slack workspace to Knock. From there, you can create UI that allows users to link their Slack user ID to their Knock user ID or automate this process during user registration.

Implementing SlackKit

To facilitate the OAuth flow and channel selection process, we'll use Knock's SlackKit components. SlackKit is a set of React components that make it easier to build Slack integrations in Knock. You can use SlackKit to build a self-serve Slack integration that allows your users to connect their Slack workspace to Knock.

Signing a user token

The only access you'll need to manage when using SlackKit are grants for your users to interact with their Tenant in Knock. 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.

We've made it easy for you to tell Knock which resources your users should have access to by making it a part of their user token. In this section you'll learn how to generate these grants using the Node SDK and, if you're not using the SDK, how to structure them for other languages.

You'll need to generate a token for your user that includes access to the tenant storing the Slack access token as well as any recipient objects storing Slack channel data described in this reference on SlackKit resource access grants.

Using the below example, you can quickly generate a token with the Node SDK.

You'll need to pass this token along with the public API key to the KnockProvider that wraps KnockSlackProvider and the rest of your components. We recommend storing the generated user token in local storage so that your client application has easy access to it.

Adding provider components

In order to give your components the data they need, they must be wrapped in the KnockSlackProvider. We recommend putting this high in your component tree so that any Slack components that you use will be rendered within it. The Slack provider goes inside of the KnockProvider. Your hierarchy will look like this:

The KnockSlackProvider gives your components access to the status of the connection to your Slack app, so that they can all be in sync when a user is connecting, disconnecting, or experiencing a connection error.

Implementing the OAuth flow with SlackAuthButton

Your users will give your Slack app access to their own Slack workspaces via the SlackAuthButton. This button can be used on its own, or nested in the SlackAuthContainer for a bigger visual footprint.

The SlackAuthButton component with SlackAuthContainer
The SlackAuthButton component with SlackAuthContainer

Since we'll also be using this access_token to resolve a user's email address to their Slack ID, we'll need to add some additional scopes to this OAuth request. We can do that by adding the users:read and users:read.email scopes to the SlackAuthButton component with the additionalScopes parameters.

Here's an example of how to use them:

The SlackAuthButton maps a tenant in your product to a customer's Slack workspace. This means in most cases you'll just need a single instance of the SlackAuthButton.

Remember to consider which roles in your application can access the SlackAuthButton component. Knock does not control access to the component. In most cases, you'll add this connect button and container in the settings area of your product.

Resolving a user's Slack ID

To send a direct message to a user in Slack, you'll need to resolve their email address to their Slack ID. You can do this by making a request to the Slack API with the access_token stored in Knock. Here's an example of how you might create a fetchUserId function to do this using the Knock Node SDK:

We'll break this function down step-by-step:

1

Since your users have already connected their Slack workspace to Knock, you can use the knockClient.objects.getChannelData method to get the access_token for the user's tenant. Tenants in Knock are stored in a system-reserved object collection called $tenants.

2

Once you have the tenant's access_token, you can use it to make a request to the Slack API to resolve the user's email address to their Slack ID using the users.lookupByEmail endpoint.

3

Assuming the request to the Slack API is successful, you can save the user's Slack ID as channel data on the user in Knock. This will allow you to send messages to the user's Slack DM channel.

Triggering a workflow

Once you have saved the user's Slack ID as channel data, you can trigger a workflow to send a message to that user's DM channel. Here's an example of how to trigger a workflow using the Knock Node SDK: