← All packages

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-slack
25downloads last week
493downloads last 12 months
v0.0.3latest, Sep 18, 2026
3releases since Sep 18, 2026

Downloads

Download history

Loading download history…

Build this

Build with convex-slack

The install and setup steps from the package documentation.

  1. 1

    Install

    sh
    npm install convex-slack
  2. 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; add channels: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 your eventsHandler (step 4) — Slack verifies it live, so deploy first. Under Interactivity & Shortcuts, do the same for interactivityHandler. Under Slash Commands, create each command with its Request URL pointed at commandsHandler.

    3. Set environment variables

    sh
    npx convex env set SLACK_CLIENT_ID xxxxxxxxxxxx.xxxxxxxxxxxx
    npx convex env set SLACK_CLIENT_SECRET xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    npx convex env set SLACK_SIGNING_SECRET xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    4. 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. 3

    Usage

    Send a message

    ts
    await 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

    ts
    await slack.postMessage(ctx, {
      teamId,
      channel,
      threadTs: parentTs,
      text: "Receipt attached.",
    });

    Open a modal from a slash command

    Your commandsHandler already recorded the invocation (with its trigger_id) to the commands table — react to it from an action that runs right after, while the trigger_id is still valid (3 seconds):

    ts
    await 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

    interactivityHandler records every block_actions payload to interactions; have your own action poll or subscribe to that table (or call respondToUrl directly with the payload's response_url once you've processed it):

    ts
    await slack.respondToUrl(responseUrl, {
      text: "Got it — invoice sent.",
      replace_original: true,
    });

    Upload a file

    ts
    await slack.uploadFile(ctx, {
      teamId,
      filename: "invoice.pdf",
      content: pdfBytes, // Uint8Array
      channelId: "C0123456789",
      initialComment: "Here's the invoice.",
    });