npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-slack

v0.0.3

Published

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

Readme

convex-slack

Install your app into any number of Slack workspaces, sync channels, messages, reactions, and users into your Convex database reactively, and drive the full Slack platform — messaging, threads, Block Kit modals, slash commands, shortcuts, and files — directly from Convex functions.

npm version Convex Component npm downloads License: Apache-2.0

convex-slack demo

const slack = new Slack(components.convexSlack, {
  clientId: process.env.SLACK_CLIENT_ID!,
  clientSecret: process.env.SLACK_CLIENT_SECRET!,
  signingSecret: process.env.SLACK_SIGNING_SECRET!,
});

await slack.postMessage(ctx, {
  teamId,
  channel: "C0123456789",
  text: "Deal closed — invoice sent.",
});

// Stays live from here — messages, reactions, and interactions all update
// reactively as Slack's events arrive.
const messages = useQuery(api.example.listMessages, { teamId, channelId });

What this does

convex-slack gives your Convex app a live, queryable view of every Slack workspace it's installed into, plus a full set of actions for driving the platform — kept in sync by Slack's Events API and interactivity payloads:

  • Multi-workspace OAuth installgetAuthorizationUrl / exchangeCode implement the full OAuth v2 authorization-code flow, storing one bot token per team_id so a single deployment serves every workspace that installs your app. Token rotation (refresh tokens) is handled transparently: resolveToken refreshes and persists a rotated token automatically before it expires.
  • Reactive messages, threads, and reactionsmessage, message_changed, message_deleted, reaction_added, and reaction_removed events keep a messages and reactions table current, so useQuery in your React app re-renders as a conversation happens.
  • Channels and users, synced on demandconversations.list/info and users.list/info calls (yours or this component's own) populate channels and users, plus membership/rename/archive events keep them current without a full resync.
  • Full messagingpostMessage, postEphemeral, updateMessage, deleteMessage, scheduleMessage/deleteScheduledMessage, and addReaction/removeReaction, all callable directly from a Convex action.
  • Block Kit modals and App HomeopenView/pushView/updateView wrap views.open/views.push/views.update for interactive modals, and publishHomeView wraps views.publish for a per-user App Home tab.
  • Slash commands and interactivity, verified and logged — three signed httpAction handlers (eventsHandler, interactivityHandler, commandsHandler) verify every request against your signing secret before anything is recorded, and every button click, modal submission, shortcut invocation, and slash command is logged to interactions / commands for your own handlers to react to.
  • Modern file uploadsuploadFile drives the current files.getUploadURLExternal -> upload -> files.completeUploadExternal sequence (the old files.upload is deprecated and never used here).
  • Resilient Web API calls — every outbound call retries on 429/5xx responses and network failures with exponential backoff and jitter, honoring Slack's Retry-After header (and the JSON-level ratelimited error some methods return).
  • Cryptographically verified inbound requests — every Events API, Interactivity, and Slash Command delivery is verified against Slack's v0=HMAC-SHA256(signingSecret, "v0:{timestamp}:{body}") scheme, with a 5-minute replay window, before it's trusted.

This is a Convex component: its installations, channels, users, messages, reactions, interactions, commands, files, scheduledMessages, and webhookEvents tables live in an isolated schema, not your app's schema, and are only reachable through the functions this component exposes.

Table of Contents

Install

npm install convex-slack

Quick Start

1. Add the component

// 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 api.slack.com/apps (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

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

// 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

// 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.");
});
// add to convex/http.ts
http.route({ path: "/slack/oauth/callback", method: "GET", handler: oauthCallback });

6. Initialize the client

// 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);
  },
});

Usage

Send a message

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

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):

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):

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

Upload a file

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

Token rotation

If your app has token rotation enabled, every method above refreshes an expiring bot token automatically (via resolveToken) and persists the rotated pair — no extra code needed. Apps without rotation enabled simply never populate botRefreshToken/ botTokenExpiresAt, and resolveToken returns the stored token as-is.

What's synced automatically vs. on demand

messages, reactions, user_change/team_join, channel create/rename/archive, and app uninstall/token revocation are all synced automatically as Slack's Events API delivers them — no polling required. channels and users are otherwise populated lazily: the first time your app calls listConversationsFromSlack, getConversationInfo, getUserInfo, or listUsersFromSlack, the same "sync what you're told, backfill on demand" posture as the rest of this component.

Testing

npm run test
npm run typecheck

Tests use convex-test at two levels. src/component/lib.test.ts covers the component's mutations and queries directly: installation lifecycle (install, token rotation, and uninstalledAt on revocation), channel and user upserts, message recording/editing/deletion, reaction deltas (add and remove converging on the right set of reactors), interaction and command logging, file records, scheduled-message status transitions, and webhook idempotency via checkAndRecordEvent. example/convex/http.test.ts separately exercises the three signed httpAction handlers end to end — signing requests with an independent HMAC-SHA256 implementation (not the component's own) to verify each one rejects a missing signature, a wrong secret, a stale timestamp, and a tampered body, correctly answers the Events API's url_verification handshake, dedupes a retried event delivery, dispatches message/reaction/channel/membership events into the right rows, and logs block_actions, view_submission, shortcut, and slash command payloads from interactivityHandler and commandsHandler.

License

Apache-2.0