Communications · Convex component
convex-slack
Install your app into any number of Slack workspaces, sync channels/messages/reactions/users into Convex reactively, and drive the full Slack platform -- messaging, threads, Block Kit modals, slash commands, shortcuts, and files -- directly from Convex fu
npm install convex-slackDownloads
Download history
Build this
Build with convex-slack
The install and setup steps from the package documentation.
- 1
Install
shnpm install convex-slack - 2
Quick Start
1. Add the component
ts// convex/convex.config.ts import { defineApp } from "convex/server"; import slack from "convex-slack/convex.config"; const app = defineApp(); app.use(slack); export default app;2. Create your Slack app
Create an app at (or from an app manifest). Under OAuth & Permissions, add the bot scopes your app needs (at minimum
chat:write; addchannels:history,reactions:read,users:read,files:write, and others as your app requires) and a Redirect URL pointing at your OAuth callback (step 5). Under Event Subscriptions, turn events on and point the Request URL at youreventsHandler(step 4) — Slack verifies it live, so deploy first. Under Interactivity & Shortcuts, do the same forinteractivityHandler. Under Slash Commands, create each command with its Request URL pointed atcommandsHandler.3. Set environment variables
shnpx convex env set SLACK_CLIENT_ID xxxxxxxxxxxx.xxxxxxxxxxxx npx convex env set SLACK_CLIENT_SECRET xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx npx convex env set SLACK_SIGNING_SECRET xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx4. Mount the HTTP handlers
ts// convex/http.ts import { httpRouter } from "convex/server"; import { components } from "./_generated/api"; import { Slack } from "convex-slack"; const slack = new Slack(components.convexSlack, { clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); const http = httpRouter(); http.route({ path: "/slack/events", method: "POST", handler: slack.eventsHandler }); http.route({ path: "/slack/interactivity", method: "POST", handler: slack.interactivityHandler }); http.route({ path: "/slack/commands", method: "POST", handler: slack.commandsHandler }); export default http;5. Wire up the OAuth install flow
ts// convex/slackOAuth.ts import { httpAction } from "./_generated/server"; import { components } from "./_generated/api"; import { Slack } from "convex-slack"; const slack = new Slack(components.convexSlack, { clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); // Link to this from an "Add to Slack" button: // slack.getAuthorizationUrl({ redirectUri, scopes: ["chat:write", "channels:history"] }) export const oauthCallback = httpAction(async (ctx, request) => { const url = new URL(request.url); const code = url.searchParams.get("code"); if (!code) return new Response("Missing code", { status: 400 }); await slack.exchangeCode(ctx, { code, redirectUri: `${process.env.CONVEX_SITE_URL}/slack/oauth/callback`, }); return new Response("Slack app installed! You can close this tab."); });ts// add to convex/http.ts http.route({ path: "/slack/oauth/callback", method: "GET", handler: oauthCallback });6. Initialize the client
ts// convex/example.ts import { action, query } from "./_generated/server"; import { components } from "./_generated/api"; import { Slack } from "convex-slack"; import { v } from "convex/values"; const slack = new Slack(components.convexSlack, { clientId: process.env.SLACK_CLIENT_ID!, clientSecret: process.env.SLACK_CLIENT_SECRET!, signingSecret: process.env.SLACK_SIGNING_SECRET!, }); export const sendMessage = action({ args: { teamId: v.string(), channel: v.string(), text: v.string() }, handler: async (ctx, args) => { return await slack.postMessage(ctx, args); }, }); export const listMessages = query({ args: { teamId: v.string(), channelId: v.string() }, handler: async (ctx, args) => { return await slack.listMessagesByChannel(ctx, args); }, }); - 3
Usage
Send a message
tsawait slack.postMessage(ctx, { teamId, channel: "C0123456789", text: "Deal closed — invoice sent.", blocks: [ { type: "section", text: { type: "mrkdwn", text: "*Deal closed* — invoice sent." } }, ], });Reply in a thread
tsawait slack.postMessage(ctx, { teamId, channel, threadTs: parentTs, text: "Receipt attached.", });Open a modal from a slash command
Your
commandsHandleralready recorded the invocation (with itstrigger_id) to thecommandstable — react to it from an action that runs right after, while thetrigger_idis still valid (3 seconds):tsawait slack.openView(ctx, { teamId, triggerId, view: { type: "modal", callback_id: "invoice_modal", title: { type: "plain_text", text: "New invoice" }, submit: { type: "plain_text", text: "Send" }, blocks: [ { type: "input", block_id: "amount", label: { type: "plain_text", text: "Amount" }, element: { type: "plain_text_input", action_id: "value" }, }, ], }, });React to a button click
interactivityHandlerrecords everyblock_actionspayload tointeractions; have your own action poll or subscribe to that table (or callrespondToUrldirectly with the payload'sresponse_urlonce you've processed it):tsawait slack.respondToUrl(responseUrl, { text: "Got it — invoice sent.", replace_original: true, });Upload a file
tsawait slack.uploadFile(ctx, { teamId, filename: "invoice.pdf", content: pdfBytes, // Uint8Array channelId: "C0123456789", initialComment: "Here's the invoice.", });