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

@fmaplabs/convex-slack

v0.1.5

Published

A slack component for Convex.

Readme

Developing this component

@fmaplabs/convex-slack is a Convex component, published on npm. To work on it, run a dev process in the bundled example project:

pnpm i
pnpm run dev

pnpm run dev starts a file watcher that rebuilds the component and runs the example backend (which installs and uses the component). Run pnpm run dev:frontend to interact with it through a Vite app.

The component code lives in src/component/ (its schema, queries, mutations, and actions) and src/client/ (the Slack class consumers use). Publish with pnpm run alpha or pnpm run release.

Component directory structure

.
├── README.md           documentation of this component
├── package.json        component name, version number, other metadata
├── pnpm-lock.yaml      committed (this repo uses pnpm)
├── src
│   ├── component/
│   │   ├── _generated/        Files here are generated for the component.
│   │   ├── convex.config.ts   Names the component and uses other components
│   │   ├── lib.ts             Component functions (enqueue, list, ...)
│   │   └── schema.ts          Schema specific to this component
│   └── client/
│       └── index.ts           The Slack class the consuming app imports
├── example/            example Convex app that uses this component
│   └── convex/
│       ├── _generated/        Files here are generated for the example app.
│       ├── convex.config.ts   Imports and uses this component
│       ├── example.ts         Functions that use the component
│       └── schema.ts          Example app schema
└── dist/               Publishing artifacts will be created here.

Convex Slack

npm version

Durable Slack notifications for Convex apps. Enqueue a message inside any mutation and the component delivers it through a nested workpool — with retries, idempotency, and a silent no-op when no Slack credentials are configured.

Found a bug? Feature request? File it here.

Installation

Create a convex.config.ts file in your app's convex/ folder, declare the Slack env vars, and bind them to the component by reference (live values, set later with npx convex env set):

// convex/convex.config.ts
import { defineApp } from "convex/server";
import { v } from "convex/values";
import slack from "@fmaplabs/convex-slack/convex.config.js";

const app = defineApp({
  env: {
    SLACK_WEBHOOK_URL: v.optional(v.string()),
    SLACK_BOT_TOKEN: v.optional(v.string()),
    SLACK_DEFAULT_CHANNEL: v.optional(v.string()),
  },
});

app.use(slack, {
  env: {
    SLACK_WEBHOOK_URL: app.env.SLACK_WEBHOOK_URL,
    SLACK_BOT_TOKEN: app.env.SLACK_BOT_TOKEN,
    SLACK_DEFAULT_CHANNEL: app.env.SLACK_DEFAULT_CHANNEL,
  },
});

export default app;

Transports: an Incoming Webhook (SLACK_WEBHOOK_URL) or a bot token (SLACK_BOT_TOKEN → chat.postMessage, with SLACK_DEFAULT_CHANNEL). The bot token is preferred when both are set; with neither set, sends are a silent no-op. See docs/consumer-integration.md for a full walkthrough.

Usage

Construct one Slack client for your app, then call send from any mutation. send returns the message id, or null when no SLACK_* env var is configured (a no-op):

import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { Slack } from "@fmaplabs/convex-slack";

// No credentials here — the component reads the env vars bound in convex.config.ts.
const slack = new Slack(components.slack);

export const notify = mutation({
  args: { text: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    return await slack.send(ctx, {
      text: args.text,
      // Optional Block Kit (pass-through). Always send `text` as a fallback.
      blocks: [
        {
          type: "section",
          text: { type: "mrkdwn", text: args.text },
        },
      ],
      // A repeat call with the same key is a no-op — derive it from the
      // chokepoint so a retried job can't double-post.
      idempotencyKey: `notify:${args.text}`,
    });
  },
});

See more example usage in example.ts.

HTTP Routes

The Slack client exposes HTTP handlers that you mount from your own convex/http.ts — your app owns the route paths (no httpPrefix config):

// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { slack } from "./example"; // wherever you construct `new Slack(...)`

const http = httpRouter();

// "Add to Slack" install + OAuth callback (see "OAuth installation" below).
http.route({
  path: "/slack/install",
  method: "GET",
  handler: httpAction((ctx, req) => slack.handleInstall(ctx, req)),
});
http.route({
  path: "/slack/oauth_redirect",
  method: "GET",
  handler: httpAction((ctx, req) => slack.handleOAuthRedirect(ctx, req)),
});

export default http;

The handlers run in your app's httpAction and delegate to the component (which still reads its own bound env — no credentials pass through the client). Mount the OAuth callback as a sibling named oauth_redirect.

OAuth installation ("Add to Slack")

The transports above use a single token you paste once. To let end users install your app into their own Slack workspace — each getting its own bot token — use the OAuth v2 flow. This mounts two more HTTP routes under your httpPrefix and stores a per-workspace installation.

1. Configure the Slack app. In your app's settings at api.slack.com:

  • Under OAuth & Permissions → Redirect URLs, add <your-site>/slack/oauth_redirect (the .convex.site origin + your httpPrefix + oauth_redirect). It must byte-match exactly.
  • Add the Bot Token Scopes you need (at minimum chat:write).
  • Copy the Client ID and Client Secret.

2. Set the env vars (bound through app.use(slack, { env }) alongside the others — see the example app's convex.config.ts):

npx convex env set SLACK_CLIENT_ID     <client-id>
npx convex env set SLACK_CLIENT_SECRET <client-secret>
npx convex env set SLACK_SCOPES        chat:write
# optional: where to send the browser after a successful install
npx convex env set SLACK_INSTALL_SUCCESS_URL https://your-app.example.com/installed

All optional — with SLACK_CLIENT_ID/SLACK_CLIENT_SECRET unset, the install routes return a clear "not configured" message and oauth sends no-op.

3. Mount the routes and render the install link. Mount handleInstall / handleOAuthRedirect in your convex/http.ts (see HTTP Routes above). Then point a button at slack.installUrl(siteUrl) — where siteUrl is your CONVEX_SITE_URL (the .convex.site origin) — which builds <site>/slack/install. Hitting it 302s the user to Slack's consent screen; on approval Slack calls your oauth_redirect route, which exchanges the code for the workspace's bot token and stores an installation row.

4. Send to an installed workspace. OAuth is multi-tenant, so send requires an explicit teamId — there's no ambiguous auto-select:

await slack.send(ctx, {
  text: "Build finished ✅",
  teamId: "T0123ABCD", // the installed workspace's team id
  transport: "oauth",
});

The token is looked up at delivery time, so reinstalls are picked up automatically. A missing installation fails the message with no_installation (no retries).

Rather than hardcoding the teamId, discover it with slack.listInstallations(ctx) — a reactive, token-free read of the installed workspaces (it never returns the bot token). Take the first entry for a single workspace, or map each teamId to your own account model. It also shows an install has completed — but it reflects installs, not uninstalls (no app_uninstalled handling yet), so read it as "installed at least once," not "connected right now":

const [install] = await slack.listInstallations(ctx);
if (install) {
  await slack.send(ctx, { text: "Build finished ✅", teamId: install.teamId, transport: "oauth" });
}

Token storage: bot tokens are stored in plaintext in the component's installations table — the same as any database-backed Slack InstallationStore. If your threat model requires encryption at rest, encrypt before storage; the schema leaves room (refreshToken/expiresAt) for future token rotation.

Run the example:

pnpm i
pnpm run dev