Prerequisites for This Post:
Series on how to build and monetize app for Slack: Introduction
To follow along, you should already have a basic Slack bot built using Node.js and Bolt.js, deployed to AWS Lambda, and interacting with DynamoDB.
If you haven’t set that up yet, check out these official tutorials to get started:
- Create a basic Slack bot with Bolt.js library: https://slack.dev/bolt-js/getting-started/
- Deploy your bot to AWS Lambda: https://slack.dev/bolt-js/deployments/aws-lambda/
In this tutorial, I'll show you how to enable any user to install your Slack bot to their organization.
You can achieve this by sharing a link like the following: https://api.reviewnudgebot.com/slack/install/
Alternatively, you can add an "Install" button to your website that directs users to this link.
You can find the complete code example for this tutorial in my GitHub repository:
GitHub Repository - Step 8: Public Installation
This repository includes the code for each step of building a Slack bot, from the initial setup to developing paid features and integrating with the Paddle payment provider to accept subscriptions.
We’ll be implementing endpoints to complete the OAuth authentication process, allowing our Slack bot to obtain an authentication token to interact with new workspaces.
Fortunately, Bolt.js for Slack handles much of the heavy lifting for us, including:
- Endpoint logic to initiate the OAuth installation process
- Token management
- Authentication of requests from different workspaces
This means we don’t need to manage tokens for posting messages or retrieving user information across various workspaces.
Our tasks are to:
- Configure a new receiver with ExpressReceiver:
We’ll pass our secret tokens, Slack scopes (which users can review before installation), and other installation options. - Implement a token management repository interface as specified by Bolt.js:
We’ll create an interface to store tokens in DynamoDB. - Expose two endpoints:
One to accept installation requestsAnother to handle the authentication result event (success event with tokens)The
ExpressReceiver
will take care of the complex logic, and we’ll only need to define these endpoints and their handlers.
Get New Tokens
Since our bot will be installed in multiple workspaces, a single bot token is insufficient, as it’s only valid for one workspace.
To handle this, add the following two environment variables SLACK_CLIENT_ID
and SLACK_CLIENT_SECRET
to your .env
files. You can find these values on the App Settings page under Basic Information and then App Credentials.
Next, import the new type of receiver for OAuth:
const { App, ExpressReceiver } = require("@slack/bolt");
Configure the ExpressReceiver and create your app instance.
You can find a complete example in the repository: https://github.com/maksouth/code-review-reminder-steps/blob/step-8-public-installation/app.js
const expressReceiver = new ExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
clientId: process.env.SLACK_CLIENT_ID,
clientSecret: process.env.SLACK_CLIENT_SECRET,
stateSecret: 'my-state-secret',
scopes: [
'chat:write',
'channels:history',
'channels:read',
'links:read',
'reactions:read',
'users:read'
],
processBeforeResponse: true,
installationStore: createDynamoDBInstallationStore(),
installerOptions: {
directInstall: true,
legacyStateVerification: true,
}
});
const createOAuthApp = () => {
const app = new App({
receiver: expressReceiver,
processBeforeResponse: true,
});
return app;
}
// Initializes your app with your bot token and app token
const app = createOAuthApp();
module.exports.handler = require('serverless-http')(expressReceiver.app);
Implement DynamoDB Installation Store
You may have noticed that createDynamoDBInstallationStore
is used but not yet defined. This function is a simple interface that interacts with the actual storage, such as DynamoDB, since Bolt.js is not aware of our runtime environment.
Reference for the InstallationStore
type we are going to implement: https://tools.slack.dev/node-slack-sdk/reference/oauth/interfaces/installationstore
Implement the Interface
First, we’ll create a thin abstraction over the database level to interact with DynamoDB.
const TABLE_NAME = 'codeReviewReminderBotSlackInstalls';
const INSTALLATION_TYPE_ENTERPRISE = 'enterprise';
const INSTALLATION_TYPE_TEAM = 'team';
const database = {
async get(key, installationType) {
console.log('[DYNAMODB] GET INSTALLATION REQUEST', key, installationType);
return getItemByCompositeKey(
TABLE_NAME,
{
team_or_enterprise_id: key,
installation_type: installationType
}).then((data) => {
const result = JSON.parse(data.Item?.installation);
return result;
});
},
async delete(key, installationType) {
console.log("[DYNAMODB] DELETE INSTALLATION", key, installationType);
return deleteItemsByCompositeKey(
TABLE_NAME,
{
team_or_enterprise_id: key,
installation_type: installationType
});
},
async set(key, installationType, value) {
console.log("[DYNAMODB] ADD INSTALLATION", value);
return putItem(
TABLE_NAME,
{
team_or_enterprise_id: key,
installation_type: installationType,
installation: JSON.stringify(value)
});
}
};
The actual methods for interacting with DynamoDB are defined in this file: https://github.com/maksouth/code-review-reminder-steps/blob/step-8-public-installation/src/dynamoDB.js
Next, we’ll implement the InstallationStore
interface, which includes three straightforward functions: store
, retrieve
, and delete
. These functions handle installation details such as the auth token and workspace details.
The implementation requires checking whether the app is installed in an organization (enterprise installation) or a single workspace. However, this distinction won’t impact any other parts of your bot.
const createDynamoDBInstallationStore = () => {
return {
storeInstallation: async (installation) => {
// Bolt will pass your handler an installation object
// Change the lines below so they save to your database
if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
// handle storing org-wide app installation
return await database.set(installation.enterprise.id, INSTALLATION_TYPE_ENTERPRISE, installation);
}
if (installation.team !== undefined) {
// single team app installation
return await database.set(installation.team.id, INSTALLATION_TYPE_TEAM, installation);
}
throw new Error('Failed saving installation data to installationStore');
},
fetchInstallation: async (installQuery) => {
// Bolt will pass your handler an installQuery object
// Change the lines below so they fetch from your database
if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
// handle org wide app installation lookup
return await database.get(installQuery.enterpriseId, INSTALLATION_TYPE_ENTERPRISE);
}
if (installQuery.teamId !== undefined) {
// single team app installation lookup
return await database.get(installQuery.teamId, INSTALLATION_TYPE_TEAM);
}
throw new Error('Failed fetching installation');
},
deleteInstallation: async (installQuery) => {
// Bolt will pass your handler an installQuery object
// Change the lines below so they delete from your database
if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
// org wide app installation deletion
return await database.delete(installQuery.enterpriseId, INSTALLATION_TYPE_ENTERPRISE);
}
if (installQuery.teamId !== undefined) {
// single team app installation deletion
return await database.delete(installQuery.teamId, INSTALLATION_TYPE_TEAM);
}
throw new Error('Failed to delete installation');
},
};
}
Now, we can import the implementation of InstallationStore
and use it with ExpressReceiver
.
Define the Table
It’s a good practice to store installation details in a separate DynamoDB table, as this is a distinct concern that isn’t directly related to our bot logic (unless you prefer a single-table design for NoSQL storage).
Let’s update the serverless.yml
file to define this new table.
codeReviewReminderBotSlackInstallsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: codeReviewReminderBotSlackInstalls
AttributeDefinitions:
- AttributeName: team_or_enterprise_id
AttributeType: S
- AttributeName: installation_type
AttributeType: S
KeySchema:
- AttributeName: installation_type
KeyType: HASH
- AttributeName: team_or_enterprise_id
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 2
WriteCapacityUnits: 2
Additionally, update the resources
section under iamRoleStatements
in the serverless.yml file to ensure that our Lambda function can interact with the new DynamoDB table.
Make sure to grant the necessary permissions for actions like GetItem
, PutItem
, and DeleteItem
on the table you defined.
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- { "Fn::GetAtt": [ "codeReviewReminderBotSlackInstallsTable", "Arn" ] }
Expose Two OAuth Endpoints
Define in serverless.yml
Update the serverless.yml file to expose two new endpoints that Bolt.js will use for the OAuth process. These endpoints will handle installation requests and the authentication result.
Make sure to specify the correct HTTP methods and path for each endpoint, so they can be properly utilized during the OAuth flow.
functions:
slack:
handler: app.handler
events:
- http:
path: slack/events
method: post
- http:
method: get
path: /slack/install
- http:
method: get
path: /slack/oauth_redirect
Deploy to AWS and Test the Real Version
We’re finished with the code changes and can now deploy to AWS to test the public installation.
Run the following command: serverless deploy
Update Slack App Settings
We’ve declared and configured the endpoints in our Lambda app to handle installation and authentication requests. Now, we need to inform Slack where to find these endpoints to complete the OAuth handshake.
- Go to Slack Settings -> OAuth & Permissions.
- Under Redirect URLs, add the AWS API Gateway URL that corresponds to your OAuth endpoints.
Make sure the URL is correct to ensure a smooth authentication process.
https://<your-api-id>.execute-api.<your-region>.amazonaws.com/dev/slack/oauth_redirect
Test: Try to Install the App in Different Workspaces!
First, let’s test the public installation in the workspace we’ve been using for testing.
- Delete the App from Your WorkspaceGo to Configuration.Click on Remove App.
2. Install the App Using OAuth
Navigate to the install endpoint that we declared in the serverless.yml
file.
The URL should look like this, matching the base URL from your API Gateway for the Lambda function:
https://<your-api-id>.execute-api.<your-region>.amazonaws.com/dev/slack/install
You’ll see a prompt to install the app to your organization. Follow the on-screen instructions to complete the installation process
Success screen, click “Open Slack”
As a bonus, you can check that a new entry has been added to the DynamoDB table for installations. This confirms that the installation process was successful and that your bot is now registered in the new workspace.
In the same way, you can now install the app in any workspace. Feel free to share the installation link with your friends and invite them to use the bot in their organizations!