How to Build a Multi-Workspace Slack Application in Go
Learn to create a multi-workspace Slack application using Go. This guide covers setting up, coding, and distributing your app across multiple Slack workspaces.
David Abramov
May 31, 2022
 â˘Â
10
 min read
Share this post
Recently, I started working on a Slack application that connects Slack and our own Blink platform. If youâre not familiar, Blink is a no-code automation platform for cloud engineering teams, and it enables users to build automations within managed workspaces for different teams or projects. In order to support this use case, I needed a way to build a multi-workspace application in Slack.
When I started reading about and playing with the Slack platform, it suddenly became clear to me that Slack is so much bigger and more complex than Iâd ever realized.
The Slack API Docs seem very large and complex at first glance, but they are actually quite detailed and user-friendly. Unfortunately, the official Bolt family of SDKs doesnât include Go, my recent language of choice, and, although I did find some useful guides on the web, none of them really targeted my use case of building an app which can be distributed across multiple workspaces and eventually listed on the Slack app store, the App Directory.
After my research, I ended up using the Slack API in Go to build the needed Slack application, and Iâm writing this hands-on article to share my experience. While I built this application to support Blinkâs particular use case, Iâve written the rest of this blog post as a general how-to guide for anyone building a similar Slack application.
Give your app a name and select the workspace you will be developing your app in.
NOTE: As our app will be publicly distributed, the workspace selection here is not very important, and weâll build our app in a way which will allows us to install it on any workspace, regardless of the choice you make in this phase.
The Scenario
Letâs imagine that you are developing a really awesome REST application that accepts as input 2 integers and returns their sum.
Weâll use the Gin web framework to write our app:
go get -u github.com/gin-gonic/gin
Hereâs the code:
package main
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
type Input struct {
Num1 int `json:"num1"`
Num2 int `json:"num2"`
}
type Output struct {
Sum int `json:"sum"`
}
func handle(c *gin.Context) {
input := &Input{}
if err := json.NewDecoder(c.Request.Body).Decode(input); err != nil {
c.String(http.StatusBadRequest, "error reading request body: %s", err.Error())
return
}
c.JSON(http.StatusOK, &Output{
Sum: input.Num1 + input.Num2,
})
}
func main() {
app := gin.Default()
app.POST("/", handle)
_ = app.Run()
}
By default, if we donât specify an argument for the âRunâ method, the app will listen on port 8080.
Letâs try it:
go run main.go
And then:
curl -X POST http://localhost:8080 --data '{"num1":1,"num2":2}' -i
And hereâs the result:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 21 May 2022 17:46:26 GMT
Content-Length: 9
{"sum":3}
Letâs call this app our âCoreâ service that we want to integrate with Slack, and create a standalone Slack dedicated service that will connect the users of our Slack app to the âCoreâ app.
Letâs visualize this:
The above diagram shows the data flow. Each time a user will interact with our Slack app, the Slack platform will access our Slack service which will in turn access our âCoreâ service to process the input and return a response which will be returned to the Slack platform (and then to the user).
Supporting Multiple Slack Workspaces
Every access to the Slack API requires the caller to provide an access token.
There are several types of tokens. Our app will work only with bot tokens.
Every time our app will be installed on some workspace, weâll get such a bot token from the Slack platform.
The installation process on some workspace is actually an OAuth authorization flow, in which we send the user to Slack to authorize our app on his workspace, and once he approves our app, weâll need to exchange our temporary access code for a permanent access token which weâll use in every operation which requires access to the Slack API.
Letâs start with a small snippet of our Slack service code.
To store these tokens, weâll use the popular and quite simple Bolt DB framework:
go get github.com/boltdb/bolt/...
Letâs initialize the DB and create a bucket of tokens, in which tokens are mapped to their workspaces (by ID):
NOTE: When developing a real app, consider storing your access tokens in a more secure way!
Developing Locally
As weâre developing locally and the Slack platform requires us to provide publicly accessible URLs, we need to create some connection between our local device and the wider network. For this we can use ngrok:
ngrok http 5400
Running the above command should print a URL which exposes port 5400 on your local device to the outer world.
Configuring Our App
To keep things simple, weâll register a Slash Command for our app:
Navigate to Features â Slash Commands and click the âCreate New Commandâ button:
Fill out the following form:
Provide the name of the command (e.g. âplusâ), the ngrok URL from the previous section suffixed with â/cmd/â and the command name and a description (e.g. âSum 2 Integersâ). The rest of the fields are optional.
Setting Up App Installation
As weâre developing a multi workspace app, we need to enable the OAuth installation flow.
Navigate to Features â OAuth & Permissions:
And click the âAdd New Redirect URLâ button in the âRedirect URLsâ section:
Enter your ngrok URL with a â/installâ suffix attached to it (e.g. âhttps://03e2-109-66-212-180.ngrok.io/installâ), click âAddâ and then click âSave URLsâ.
Scopes
The scopes requested by our app are those that our app will request upon each installation to some workspace. You can see in the âScopesâ section that we already have the âcommandsâ bot scope, as weâve added a Slash Command to our app.
Enabling Public Distribution
Navigate to Settings â Manage Distribution:
And under the âShare Your App with Other Workspacesâ section, make sure that the hard coded information removal checkbox is checked:
Finally, click the âActivate Public Distributionâ button.
Connecting It All Together
Now that we have our awesome âCoreâ Service and weâve configured our Slack app, we can go ahead and continue writing the code of our Slack service.
Weâve already configured our storage mechanism, and youâve probably noticed that we are already committed to implementing some endpoints we shared with the slack platform:
/install - to install our app using the OAuth flow.
/cmd/plus - to forward the request to sum 2 integers to our âCoreâ service, using the input from the user on Slack.
Implementing the OAuth Installation Flow
As with every standard OAuth flow, we need a pair of client ID and secret. You can view both under the âApp Credentialsâ section after navigating to Settings â Basic Information:
We need to access the Slack platform, so letâs import the Go SDK:
Letâs break it down. First, we check for the presence of the âerrorâ query parameter. If this parameter is present, then it means the installation failed for some reason (e.g. the user declined to authorize our app). In that case weâll abort the rest of the flow and just notify of an error.
Next, as in every standard OAuth flow, weâre checking for the temporary code by looking for the âcodeâ query parameter, and if it is present we use the Slack Go SDK to exchange that code for an access token, and store that token in our storage, mapped to the workspace ID.Finally, we redirect the user to our app page in his Slack client using Deep linking.
Notice how weâre reading the âCLIENT_IDâ and âCLIENT_SECRETâ env variables to perform the exchange operation.
IMPORTANT: When developing a real app, consider adding a âstateâ parameter to your OAuth flow for extra security!
Implementing the Slash Command
Before we implement our slash command endpoint, letâs implement some middleware that will make sure that requests to this endpoint are really coming from Slack.
There are several methods provided by the Slack platform to achieve such verification, but weâll use the recommended and straightforward way of using the Signing Secret present under the âApp Credentialsâ section in Settings â Basic Information:
Hereâs the signature verification middleware code:
First, weâre reading the signature verification secret from the âSIGNATURE_SECRETâ env variable.
Then, weâre reading the request body, while copying it and preserving it for later reads which may be invoked by other handlers in the chain, and finally we verify the signature of the request body. If all is well, weâre calling the âNextâ method to invoke any other handlers in the chain.
Letâs have a look at the code of the actual handler:
Letâs break it down. Weâre parsing the request body to get the slash command input.
Then, weâre parsing the parameters passed in with the command to verify we have 2 integers (e.g. â/plus 1 2â is valid, but â/plus 1 aâ is not).
Finally, weâre using the 2 provided parameters to invoke the endpoint on our âCoreâ service to get the sum of the 2 given integers, sending the user a direct message with the calculated sum returned by the âCoreâ service.
In order to send a direct message to the user we are reading the token mapped to the relevant workspace from the DB.
Also, we need to add the âchat:writeâ bot scope for our app, otherwise our app will not be able to send direct messages to users.
Navigate to Settings â OAuth & Permissions, and under the âScopesâ section add the required scope to the list of bot scopes required by your app:
Finally, letâs register our slash command handler under a new âcmdâ API group. Hereâs the current code of our Slack service:
For convenience, we copied the definitions of the âInputâ and âOutputâ structs from our âCoreâ service.
Notice that when weâre sending the direct message to the user weâre using the access token of the workspace the user invoked the command from.
Playing With Our App
Run your app using the following command, providing values for the required env variables:
PORT=5400 CLIENT_ID=... CLIENT_SECRET=... SIGNATURE_SECRET=... go run main.go
The value of the âPORTâ variable is used by the gin framework if no argument is passed to the âRunâ method.
Letâs first install our app on some workspace. To do that, we need to initiate the OAuth installation flow. To do that, Navigate to Settings â Manage Distribution:
Press the âCopyâ button in the âSharable URLâ section and paste the copied URL into a new tab in your browser.
This should open a page which resembles the following page, allowing you to choose a workspace and install the app to it:
Press âAllowâ and in the following window:
Press the âOpen Slackâ button, which should take you to the âAboutâ page of your app in your Slack client:
Now, navigate to some public channel in your workspace and type in â/plusâ:
Press enter to choose the â/plusâ command of your app, suffix the â/plusâ command with 2 integers of your choice and send the message, e.g.:
Once you send the message, you should receive a direct message from your app, notifying you of the result:
Listening to Events
The Slack Events API is an integral part of any Slack application, especially those that are intended to be distributed across multiple workspaces.
There are 3 kinds of events:
URL Verification - The Slack platform sends a challenge string, and our app should respond with that string and with a 200 OK status code. In this way the Slack platform checks that our app is alive.
Rate Limiting - The Slack platform informs our app of some rate limit being reached on some API endpoint. We can just acknowledge this event with a 200 OK status code.
Callback - The âinterestingâ events. These events have an inner event which can have various types.
Letâs register our app to a basic app deletion event.
Hereâs our Slack service code, including our event handler:
When an event is sent, we check the type, and respond accordingly. The only callback event weâre going to handle is app uninstallation. We handle it by deleting the access token we used to access the Slack platform when we needed to interact with the workspace the app was deleted from.
Run the updated version of the app which includes the event handler and then navigate to Features â Event Subscriptions:
Enable events and under the âRequest URLâ section, enter your ngrok URL suffixed with â/event/handleâ (e.g. âhttps://03e2-109-66-212-180.ngrok.io/event/handleâ) and wait for the Slack platform to verify your events URL:
Then, under the âSubscribe to bot eventsâ section, add the âapp_uninstalledâ event.
Deleting Our App
From the direct message channel your app opened with your user to inform you of the result of summing 2 integers, click on the app name:
Press âConfigurationâ, which should open a browser window with the details of the app installation on your workspace:
And press the âRemove Appâ button under the section with the same name.
Open your workspace from your Slack client. You should notice that the direct message channel with your app is not there anymore:
Also, you can see in the output of your Slack service, that a request was made to the events endpoint and that it was responded with a 200 OK status code: