Corsair
PluginsSlack

Slack

Extend Corsair to Slack Integrations

Connect Slack to your agent or app. Corsair handles auth, data syncing, and webhook verification — you write the logic.

What you get out of the box:

  • Send and receive messages
  • Query channels, users, files, and reactions — live or from local cache
  • Real-time webhook events (messages, joins, reactions, and more)
  • Automatic signature verification for every incoming event

Setup

Install

pnpm install @corsair-dev/slack

Add the plugin

corsair.ts
import { createCorsair } from 'corsair';
import { slack } from '@corsair-dev/slack';
import Database from 'better-sqlite3';

export const corsair = createCorsair({
    plugins: [slack()],
    database: new Database('corsair.db'),
    kek: process.env.CORSAIR_KEK!,
});

Get your credentials

You need a Slack bot token (xoxb-...) to make API calls, and a signing secret to verify incoming webhooks.

See Get Credentials for step-by-step instructions on creating a Slack app and copying both values.

Store credentials

pnpm corsair setup --slack bot_token=xoxb-your-bot-token webhook_signature=your-signing-secret

Run this once. Credentials are encrypted with your KEK and stored in your database.


Authentication

The Slack plugin supports two auth methods:

The default. Create a Slack app, install it to your workspace, and copy the bot token.

corsair.ts
slack({
    authType: 'api_key',  // default, can be omitted
})

Store the token:

pnpm corsair setup --slack bot_token=xoxb-your-bot-token

See Get Credentials for where to find this in the Slack dashboard.

Use OAuth when users connect their own Slack workspaces (multi-tenant apps).

corsair.ts
slack({
    authType: 'oauth_2',
})

Store your OAuth app credentials, then start the flow:

pnpm corsair setup --slack client_id=your-client-id client_secret=your-client-secret
pnpm corsair auth --plugin=slack

The CLI will print an authorization URL. Once the user approves, tokens are stored automatically per tenant. See Multi-Tenancy for the full setup.


Send a message

await corsair.slack.api.messages.post({
    channel: 'C1234567890',  // channel ID
    text: 'Hello from Corsair!',
});

Use channel IDs, not names

Slack's API requires channel IDs (e.g. C1234567890), not names like #general. Query corsair.slack.api.channels.list() once to get the IDs, then cache them locally.


Query data

Corsair syncs Slack data to your local database. Query it instantly — no API call needed every time.

Direct Slack API calls — always fresh, always counts against rate limits:

// List channels
const channels = await corsair.slack.api.channels.list({
    exclude_archived: true,
});

// List users
const users = await corsair.slack.api.users.list();

// Post a message
await corsair.slack.api.messages.post({
    channel: 'C1234567890',
    text: 'Hello!',
});

Every API call automatically saves the response to your local database.

Query your local database — instant, no rate limit, works offline:

// All channels you've fetched
const channels = await corsair.slack.db.channels.findAll();

// Find a specific channel by name
const results = await corsair.slack.db.channels.findMany({
    where: { data: { name: { contains: 'general' } } },
});

// Find messages in a channel
const messages = await corsair.slack.db.messages.findMany({
    where: { data: { channel: 'C1234567890' } },
});

// Find a user by name
const users = await corsair.slack.db.users.findMany({
    where: { data: { name: { contains: 'john' } } },
});

See Database Reference for full query options.


Receive webhooks

Set up a single webhook endpoint — Corsair routes and verifies everything:

app/api/webhook/route.ts
import { processWebhook } from 'corsair';
import { corsair } from '@/server/corsair';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
    const headers: Record<string, string> = {};
    request.headers.forEach((value, key) => { headers[key] = value; });

    const body = request.headers.get('content-type')?.includes('application/json')
        ? await request.json()
        : await request.text();

    const tenantId = new URL(request.url).searchParams.get('tenantId') ?? undefined;
    const result = await processWebhook(corsair, headers, body, { tenantId });

    if (!result.response) {
        return NextResponse.json({ success: false }, { status: 404 });
    }

    return NextResponse.json(result.response);
}

React to events with webhook hooks:

corsair.ts
slack({
    webhookHooks: {
        messages: {
            message: {
                after: async (ctx, result) => {
                    // Skip bot messages
                    if (result.data.bot_id) return;

                    console.log(`Message from ${result.data.user}: ${result.data.text}`);
                },
            },
        },
        users: {
            teamJoin: {
                after: async (ctx, result) => {
                    // Welcome new team members
                    await corsair.slack.api.messages.post({
                        channel: 'C_GENERAL_CHANNEL_ID',
                        text: `Welcome <@${result.data.user.id}>!`,
                    });
                },
            },
        },
    },
})

Available webhook events:

EventTrigger
messages.messageNew or updated message
channels.createdNew channel created
reactions.addedReaction added to message
users.teamJoinNew user joined team
users.userChangeUser profile updated
files.createdFile uploaded
challenge.challengeURL verification (handled automatically)

See Slack Webhooks for full payload shapes.


Plugin options

corsair.ts
slack({
    authType: 'api_key',        // 'api_key' | 'oauth_2'
    key: process.env.SLACK_BOT_TOKEN,         // optional, uses DB otherwise
    signingSecret: process.env.SLACK_SIGNING_SECRET, // optional, uses DB otherwise
    hooks: { /* endpoint hooks */ },
    webhookHooks: { /* webhook hooks */ },
    errorHandlers: { /* custom error handlers */ },
})
OptionTypeDescription
authType'api_key' | 'oauth_2'Authentication method (default: 'api_key')
keystringBot token — use DB storage instead for production
signingSecretstringWebhook signing secret — use DB storage instead for production
hooksobjectLogic that runs before/after API calls
webhookHooksobjectLogic that runs before/after webhook events
errorHandlersobjectCustom error handling per error type

Multi-tenancy

If you're building a product where users connect their own Slack workspaces:

corsair.ts
export const corsair = createCorsair({
    multiTenancy: true,
    plugins: [slack()],
    // ...
});
usage.ts
const tenant = corsair.withTenant('user_abc123');

// All calls scoped to this user
await tenant.slack.api.messages.post({
    channel: 'C1234567890',
    text: 'Hello!',
});

const channels = await tenant.slack.db.channels.findAll();

Point each user's Slack webhook to your endpoint with their ID:

https://your-app.com/api/webhook?tenantId=user_abc123

See Multi-Tenancy for the full guide.


What's next