/slack-edge

Slack app development framework for edge functions with streamlined TypeScript support

Primary LanguageTypeScriptMIT LicenseMIT

Slack Edge

npm version deno module

The slack-edge library is a Slack app development framework designed specifically for the following runtimes:

Not only does it work with the above, but it also functions with the latest versions of Deno, Bun, and Node.js.

This framework draws significant inspiration from Slack's Bolt framework, but its design does not strictly follow the bolt-js blueprint. Key differences include:

  • Edge function ready: Out-of-the-box edge function (e.g., Cloudflare Workers) support
  • TypeScript focused: Enhances type safety and clarifies typings for developers
  • Lazy listener enabled: bolt-python's lazy listener feature is provided out of the box
  • Zero additional dependencies: No other dependencies required beyond TypeScript types and slack-web-api-client (our fetch-function-based Slack API client)

Getting Started

Currently, you can use this library for three different platforms.

Cloudflare Workers

Cloudflare Workers is an edge function platform for quickly deploying an app.

The only additional thing you need to do is to add the slack-cloudflare-workers package to your Cloudflare Workers app project. The slack-cloudflare-workers package enhances slack-edge with Cloudflare Workers-specific features, such as KV-based installation stores.

npm install -g wrangler@latest
npx wrangler generate my-slack-app
cd my-slack-app
npm i slack-cloudflare-workers@latest

A simple app that handles a slash command can be structured like this:

import { SlackApp, SlackEdgeAppEnv } from "slack-cloudflare-workers";

export default {
  async fetch(
    request: Request,
    env: SlackEdgeAppEnv,
    ctx: ExecutionContext
  ): Promise<Response> {
    const app = new SlackApp({ env })
      .command("/hello-cf-workers",
        async (req) => {
          // sync handler, which is resposible to ack the request
          return ":wave: This app runs on Cloudflare Workers!";
          // If you don't have anything to do here, the function doesn't need to return anything
          // This means in that case, simply having `async () => {}` works for you
        },
        async ({ context: { respond } }) => {
          // Lazy listener, which can be executed asynchronously
          // You can do whatever may take longer than 3 seconds here
          await respond({ text: "This is an async reply. How are you doing?" });
        }
      );
    return await app.run(request, ctx);
  },
};

Also, before running this app, don't forget to create a .dev.vars file with your env variables for local development:

SLACK_SIGNING_SECRET=....
SLACK_BOT_TOKEN=xoxb-...
SLACK_LOGGING_LEVEL=DEBUG

When you run wrangler dev --port 3000, your app process spins up, and it starts handling API requests at http://localhost:3000/slack/events. You may want to run Cloudflare Tunnel or something equivalent to forward public endpoint requests to this local app process.

Refer to https://github.com/seratch/slack-cloudflare-workers/blob/main/docs/index.md for further guidance.

Vercel Edge Functions

Vercel Edge Functions is an edge function platform, which is part of Vercel's Frontend Cloud.

Create a new Next.js project w/ slack-edge library by following these steps:

npx create-next-app@latest --typescript
cd your-app-name
npm i slack-edge

Create a new source file app/api/slack.ts that contains the following code:

import type { NextRequest } from "next/server";
import { waitUntil } from '@vercel/functions';
import { SlackApp } from "slack-edge";

export const config = {
  runtime: "edge",
};

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

app.command("/hello-edge",
  async (req) => {
    // sync handler, which is resposible to ack the request
    return ":wave: This app runs on Vercel Edge Function platform!";
    // If you don't have anything to do here, the function doesn't need to return anything
    // This means in that case, simply having `async () => {}` works for you
  },
  async ({ context: { respond } }) => {
    // Lazy listener, which can be executed asynchronously
    // You can do whatever may take longer than 3 seconds here
    await respond({ text: "This is an async reply. How are you doing?" });
  }
);

export async function POST(req: NextRequest) {
  return await app.run(req, { waitUntil });
}

With this code, your Request URL for handling requests from Slack API server will be https://{your public domain}/api/slack instead of /slack/events.

Lastly, you can run the app by following these steps:

export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."
npm run dev

If you use Cloudflare Tunnel or something equivalent, you may want to run cloudflared tunnel --url http://localhost:3000 in a different terminal window.

We are currently working on the slack-vercel-edge-functions package, which offers Vercel-specific features in addition to the core ones provided by slack-edge. Until its release, please use slack-edge directly and implement the missing features on your own as necessary.

Supabase Edge Functions

Supabase is a BaaS that provides a full PostgreSQL database, along with other products such as Supabase Edge Functions.

Create a new Supabase project locally by following these steps:

mkdir myapp
cd myapp

brew install supabase/tap/supabase
supabase init
supabase functions new hello-world

Open the source file supabase/functions/hello-world/index.ts and replace it with the following:

import {
  SlackApp,
  SlackEdgeAppEnv,
} from "https://deno.land/x/slack_edge@1.3.2/mod.ts";

const app = new SlackApp<SlackEdgeAppEnv>({
  env: {
    SLACK_SIGNING_SECRET: Deno.env.get("SLACK_SIGNING_SECRET")!,
    SLACK_BOT_TOKEN: Deno.env.get("SLACK_BOT_TOKEN"),
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

app.command("/hello-edge", async (req) => {
  // sync handler, which is resposible to ack the request
  return ":wave: This app runs on Vercel Edge Function platform!";
  // If you don't have anything to do here, the function doesn't need to return anything
  // This means in that case, simply having `async () => {}` works for you
}, async ({ context: { respond } }) => {
  // Lazy listener, which can be executed asynchronously
  // You can do whatever may take longer than 3 seconds here
  await respond({ text: "This is an async reply. How are you doing?" });
});

Deno.serve(async (req) => {
  return await app.run(req);
});

Run locally

Create a supabase/functions/.env file with the following:

export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."

Then run:

# Terminal A
supabase start
supabase functions serve --env-file supabase/functions/.env

# Terminal B
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://127.0.0.1:54321

Run in the cloud

In a browser, login to your Supabase account and create a new project.

Then open a terminal in your myapp's project directory and run:

supabase login

Link the Supabase project you just made:

supabase link

Deploy your hello-world edge function.

supabase functions deploy hello-world --no-verify-jwt

Finally, add your environment variables:

  • Go to your Supabase project
  • Go to the Edge Functions tab, and click on your edge functions
  • Tap on Manage Secrets and then add your SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET

Run Your App with Deno / Bun / Node.js

This library is available not only for edge function use cases but also for any JavaScript runtime use cases. Specifically, you can run an app with Deno, Bun, and Node.js. To learn more about this, please check the example files under the ./test directory.

Run with Bun

import { SlackApp } from "slack-edge";

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

// Add listeners here

export default {
  port: 3000,
  async fetch(request) {
    return await app.run(request);
  },
};

You can run the app by:

# Terminal A
bun run --watch my-app.ts
# Terminal B
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://localhost:3000

Run with Deno

Please refer to README for the Deno module.

Run with Node.js (Socket Mode: Production-ready)

If you need a stable Socket Mode integration, we recommend using @slack/socket-mode along with this package. With Node 20+, the following example works for you:

// npm i slack-edge @slack/socket-mode
import {
  SlackApp,
  fromSocketModeToRequest,
  fromResponseToSocketModePayload,
} from "slack-edge";
import { SocketModeClient } from "@slack/socket-mode";
import { LogLevel } from "@slack/logger";

const app = new SlackApp({
  socketMode: true,
  env: {
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_APP_TOKEN: process.env.SLACK_APP_TOKEN!,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

// Add listeners here
app.command("/hello", async ({}) => {
  return "Hi!";
});

// Start a Socket Mode client
const socketModeClient = new SocketModeClient({
  appToken: process.env.SLACK_APP_TOKEN!,
  logLevel: LogLevel.DEBUG,
});
socketModeClient.on("slack_event", async ({ body, ack, retry_num: retryNum, retry_reason: retryReason }) => {
  const request = fromSocketModeToRequest({ body, retryNum, retryReason });
  if (!request) { return; }
  const response = await app.run(request);
  await ack(await fromResponseToSocketModePayload({ response }));
},);
(async () => {
  await socketModeClient.start();
})();

If you want to build a Slack app this way, this project template should be pretty useful for you!

Run with Remix + Node.js

If you're looking for a way to serve Slack app using Remix, slack-edge is the best way to go! Here is a simple example to run your Slack app as part of a Remix app:

import { LoaderFunctionArgs } from "@remix-run/node";
import { SlackApp } from "slack-edge";

export async function action({ request }: LoaderFunctionArgs) {
  const app = new SlackApp({
    env: {
      SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
      SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    }
  });
  // Add listeners here

  return await app.run(request);
}

You can run the app by following these steps:

  • npx create-remix@latest
  • cd my-remix-app
  • npm i @remix-run/serve @remix-run/node slack-edge
  • Add app/routes/api.slack.tsx (if you go with POST /api/slack) with the above code
  • export SLACK_SIGNING_SECRET=...
  • export SLACK_BOT_TOKEN=xoxb-...
  • npm run dev

Reference

Middleware

This framework offers ways to globally customize your app's behavior, like you do when developing web apps. A common example is to attach extra data to the context object for following listeners.

Pattern Description
app.beforeAuthorize The passed function does something before calling authorize() function. If this method returns SlackResponse, the following middleware and listeners won't be executed.
app.afterAuthorize / app.use / app.middleware The passed function does something right after calling authorize() function. If this method returns SlackResponse, the following middleware and listeners won't be executed.

ack / lazy Functions

You may be unfamiliar with the "lazy listener" concept in this framework. To learn more about it, please read bolt-python's documentation: https://tools.slack.dev/bolt-python/concepts/lazy-listeners

The ack function must complete within 3 seconds, while the lazy function can perform time-consuming tasks. It's important to note that not all request handlers support the ack or lazy functions. For more information, please refer to the following table, which covers all the patterns in detail.

Starting from v0.9, if desired, you can execute lazy listeners after the completion of their ack function. To customize the behavior in this manner, you can pass startLazyListenerAfterAck: true as one of the arguments in the App constructor.

Pattern Description ack function lazy function
app.assistant The passed collection of functions handles an assistant thread request pattern. The feature needs to be turned on in advance. x
app.command The passed function handles a slash command request pattern. If the function returns a message, the message will be posted in the channel where the end-user invoked the command.
app.event The passed function asynchronously does something when an Events API request that matches the constraints comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. x
app.function The passed function asynchronously does something when a "function_executed" event request that matches the constraints comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. x
app.message / app.anyMessage The passed function asynchronously does something when an a message event comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. If the message pattern argument can be any of string, regexp, and undefined. When you pass undefined, the listener matches all messages. x
app.shortcut / app.globalShortcut / app.messageShortcut The passed function handles a global/message shortcut request pattern. Please note that returning a message text in the ack function does not work for shortcuts. Instead, you can use context.respond for it.
app.action The passed function handles a user interaction on a Block Kit component such as button clicks, item selection in a select menu, and so on.
app.options The passed function handles an external data source reqeust for Block Kit select menus. You cannnot respond to this request pattern asynchronously, so slack-edge enables developers to pass only ack function, which must complete within 3 seconds, here. x
app.view / app.viewSubmission / app.viewClosed The passed function handles either a modal data submission or the "Close" button click event. ack function can return various response_actions (errors, update, push, clear) and their associated data. If you want to simply close the modal, you don't need to return anything.