Skip to content
Back to skills

Stacks Chat

ASecurity

Use when implementing chat messaging in Stacks - sending messages to Slack (webhooks, bot tokens, block kit), Discord (webhooks, bot tokens, embeds), Microsoft Teams (adaptive cards, webhooks), the BaseChatDriver abstraction, retry logic, or multi-channel chat routing. Covers @stacksjs/chat.

  • 631 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmenttypescriptgobashapi

Works with

  • api

Security analysis

A100/100

Scanned September 3, 2026

npx -y skills add stacksjs/stacks --skill stacks-chat --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stacks Chat?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stacks Chat
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stacksjs-stacks-chat/badge)](https://www.skillsdirectory.com/skills/stacksjs-stacks-chat)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stacks-chat
description: Use when implementing chat messaging in Stacks - sending messages to Slack (webhooks, bot tokens, block kit), Discord (webhooks, bot tokens, embeds), Microsoft Teams (adaptive cards, webhooks), the BaseChatDriver abstraction, retry logic, or multi-channel chat routing. Covers @stacksjs/chat.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks Chat

Multi-driver chat messaging with Slack, Discord, and Microsoft Teams support. Each driver supports both webhook and bot token modes, with retry logic and structured message formats.

## Key Paths
- Core package: `storage/framework/core/chat/src/`
- Types: `storage/framework/core/types/src/chat.ts`

## Source Files
```
chat/src/
├── index.ts              # router, configure functions, type re-exports
└── drivers/
    ├── base.ts           # BaseChatDriver abstract class
    ├── slack.ts          # SlackDriver + sendWebhook()
    ├── discord.ts        # DiscordDriver + sendWebhook() + sendEmbed()
    └── teams.ts          # TeamsDriver + sendWebhook() + sendCard()
```

## Router (index.ts)

The main `send()` function routes to the specified driver. Default driver is `'slack'`:

```typescript
import { send, sendToSlack, sendToDiscord, sendToTeams } from '@stacksjs/chat'

// Route to driver (default: 'slack')
await send(message, { driver: 'slack' })
await send(message, { driver: 'discord' })
await send(message, { driver: 'teams' })

// Direct webhook helpers
await sendToSlack(webhookUrl, text, options?)
await sendToDiscord(webhookUrl, content, options?)
await sendToTeams(webhookUrl, text)
```

Configure functions:
```typescript
import { configureSlack, configureDiscord, configureTeams } from '@stacksjs/chat'
configureSlack({ webhookUrl: '...', botToken: '...' })
configureDiscord({ webhookUrl: '...', botToken: '...' })
configureTeams({ webhookUrl: '...' })
```

Exported types: `ChatDriver` (union), `SendOptions`, `SlackConfig`, `SlackMessage`, `SlackBlock`, `SlackAttachment`, `DiscordConfig`, `DiscordMessage`, `DiscordEmbed`, `TeamsConfig`, `TeamsMessage`, `TeamsAdaptiveCard`, `TeamsCardElement`, `TeamsCardAction`

## BaseChatDriver (base.ts)

Abstract base class all drivers extend. Implements `ChatDriver` interface from `@stacksjs/types`:

```typescript
abstract class BaseChatDriver implements ChatDriver {
  public abstract name: string
  protected config: Required<ChatDriverConfig>

  constructor(config?: ChatDriverConfig)  // defaults: maxRetries=3, retryTimeout=1000

  configure(config: ChatDriverConfig): void
  abstract send(message: ChatMessage, options?: RenderOptions): Promise<ChatResult>

  // Protected helpers
  protected validateMessage(message: ChatMessage): boolean  // throws if no `to` or no `content`/`template`
  protected async handleError(error: unknown, message: ChatMessage): Promise<ChatResult>
  protected async handleSuccess(message: ChatMessage, messageId?: string): Promise<ChatResult>
}
```

- `handleError()` logs the error, then calls `message.onError()` if defined, returns `ChatResult` with `success: false`
- `handleSuccess()` calls `message.handle()` first, then `message.onSuccess()` if defined, returns `ChatResult` with `success: true`
- Both handlers merge any partial result from callbacks into the final `ChatResult`

## ChatMessage Interface (from @stacksjs/types)

```typescript
interface ChatMessage {
  to: string | string[]                // recipient (channel ID, user ID, etc.)
  from?: { id?: string, name?: string, avatar?: string }
  subject?: string                      // title for rich messages
  content?: string                      // plain text content
  template?: string                     // template for rich message rendering
  data?: Record<string, any>           // template data
  attachments?: ChatAttachment[]
  onSuccess?: () => void | Promise<void> | Partial<ChatResult>
  onError?: (error: Error) => void | Promise<void> | Partial<ChatResult>
  handle?: () => void | Promise<void> | Partial<ChatResult>
  [key: string]: any                    // custom platform-specific fields
}
```

## ChatResult Interface (from @stacksjs/types)

```typescript
interface ChatResult {
  success: boolean
  message: string
  provider: string                      // 'slack' | 'discord' | 'teams'
  messageId?: string
  data?: Record<string, any>
}
```

## Slack Driver

### SlackConfig
```typescript
interface SlackConfig {
  webhookUrl?: string
  botToken?: string
  maxRetries?: number       // default: 3
  retryTimeout?: number     // default: 1000ms
}
```

### SlackMessage (for webhook payloads)
```typescript
interface SlackMessage {
  channel?: string
  text?: string
  blocks?: SlackBlock[]
  attachments?: SlackAttachment[]
  username?: string
  iconEmoji?: string
  iconUrl?: string
  threadTs?: string
  mrkdwn?: boolean
}
```

### SlackBlock
```typescript
interface SlackBlock {
  type: 'section' | 'divider' | 'header' | 'context' | 'actions' | 'image'
  text?: { type: 'plain_text' | 'mrkdwn', text: string, emoji?: boolean }
  accessory?: any
  elements?: any[]
  block_id?: string
}
```

### SlackAttachment
```typescript
interface SlackAttachment {
  color?: string
  pretext?: string
  author_name?: string
  author_link?: string
  author_icon?: string
  title?: string
  title_link?: string
  text?: string
  fields?: Array<{ title: string, value: string, short?: boolean }>
  image_url?: string
  thumb_url?: string
  footer?: string
  footer_icon?: string
  ts?: number
}
```

### Slack Sending Modes

1. **Webhook mode**: If `config.webhookUrl` is set, sends via `POST` to webhook URL. Payload includes `text`, `username`, `mrkdwn: true`. If `message.template` exists, builds Block Kit blocks.
2. **Bot token mode**: If `config.botToken` is set, sends via Slack API `https://slack.com/api/chat.postMessage` with `Authorization: Bearer <token>`. Returns `ts` (timestamp) as message ID.
3. Throws `Error('Slack not configured: provide webhookUrl or botToken')` if neither is set.

### Direct Webhook Function
```typescript
async function sendWebhook(webhookUrl: string, text: string, options?: Partial<SlackMessage>): Promise<ChatResult>
```
Merges `options` into payload, sends POST to the provided webhook URL.

### Block Building
When `message.template` is set, `buildBlocks()` creates:
- A `header` block from `message.subject` (if present)
- A `section` block from `message.content` with `mrkdwn` type

### Exports
- `SlackDriver` class (also as `Driver`)
- `driver` -- pre-instantiated `SlackDriver` singleton
- `send()`, `sendWebhook()`, `configure()` functions

## Discord Driver

### DiscordConfig
```typescript
interface DiscordConfig {
  webhookUrl?: string
  botToken?: string
  maxRetries?: number       // default: 3
  retryTimeout?: number     // default: 1000ms
}
```

### DiscordEmbed
```typescript
interface DiscordEmbed {
  title?: string
  description?: string
  url?: string
  color?: number              // integer color value (e.g. 0x5865F2)
  timestamp?: string          // ISO 8601
  footer?: { text: string, icon_url?: string }
  image?: { url: string }
  thumbnail?: { url: string }
  author?: { name: string, url?: string, icon_url?: string }
  fields?: Array<{ name: string, value: string, inline?: boolean }>
}
```

### DiscordMessage
```typescript
interface DiscordMessage {
  content?: string
  username?: string
  avatarUrl?: string
  tts?: boolean
  embeds?: DiscordEmbed[]
  allowedMentions?: {
    parse?: Array<'roles' | 'users' | 'everyone'>
    roles?: string[]
    users?: string[]
  }
}
```

### Discord Sending Modes

1. **Webhook mode**: POST to `config.webhookUrl`. Handles `204` (empty success) responses. Returns `id` from response.
2. **Bot token mode**: POST to `https://discord.com/api/v10/channels/{channelId}/messages` with `Authorization: Bot <token>`. Uses `message.to` as the channel ID.

### Embed Building
When `message.subject` or `message.template` is set, `buildEmbed()` creates an embed with:
- `title` from `message.subject`
- `description` from `message.content`
- Default color: `0x5865F2` (Discord blurple)
- `timestamp`: current ISO date

### Extra Functions
```typescript
async function sendWebhook(webhookUrl: string, content: string, options?: Partial<DiscordMessage>): Promise<ChatResult>
async function sendEmbed(webhookUrl: string, embed: DiscordEmbed, options?: Partial<DiscordMessage>): Promise<ChatResult>
```
`sendEmbed()` wraps the embed inside `sendWebhook()` with empty content.

### Exports
- `DiscordDriver` class (also as `Driver`)
- `driver` -- pre-instantiated singleton
- `send()`, `sendWebhook()`, `sendEmbed()`, `configure()` functions

## Teams Driver

### TeamsConfig
```typescript
interface TeamsConfig {
  webhookUrl?: string
  maxRetries?: number       // default: 3
  retryTimeout?: number     // default: 1000ms
}
```

### TeamsAdaptiveCard
```typescript
interface TeamsAdaptiveCard {
  type: 'AdaptiveCard'
  version: string           // '1.4'
  body: TeamsCardElement[]
  actions?: TeamsCardAction[]
  $schema?: string          // 'http://adaptivecards.io/schemas/adaptive-card.json'
}
```

### TeamsCardElement
```typescript
interface TeamsCardElement {
  type: 'TextBlock' | 'Image' | 'Container' | 'ColumnSet' | 'Column' | 'FactSet' | 'ImageSet'
  text?: string
  size?: 'Small' | 'Default' | 'Medium' | 'Large' | 'ExtraLarge'
  weight?: 'Lighter' | 'Default' | 'Bolder'
  color?: 'Default' | 'Dark' | 'Light' | 'Accent' | 'Good' | 'Warning' | 'Attention'
  wrap?: boolean
  url?: string
  altText?: string
  items?: TeamsCardElement[]
  columns?: TeamsCardElement[]
  width?: string
  facts?: Array<{ title: string, value: string }>
  images?: Array<{ type: 'Image', url: string, size?: string }>
}
```

### TeamsCardAction
```typescript
interface TeamsCardAction {
  type: 'Action.OpenUrl' | 'Action.Submit' | 'Action.ShowCard'
  title: string
  url?: string
  data?: Record<string, any>
  card?: TeamsAdaptiveCard
}
```

### TeamsMessage
```typescript
interface TeamsMessage {
  type?: 'message'
  summary?: string
  text?: string
  attachments?: Array<{
    contentType: 'application/vnd.microsoft.card.adaptive'
    content: TeamsAdaptiveCard
  }>
}
```

### Teams Sending

- Uses `config.webhookUrl` or falls back to `message.to` as the webhook URL
- Validates URL contains `webhook.office.com`
- Simple messages (no subject/template): sends `{ type: 'message', text: content }`
- Rich messages (with subject or template): builds an Adaptive Card v1.4 with:
  - Title as `TextBlock` (size: Large, weight: Bolder)
  - Content as `TextBlock` (wrap: true)
  - Timestamp as `TextBlock` (size: Small, color: Dark)

### Extra Functions
```typescript
async function sendWebhook(webhookUrl: string, text: string): Promise<ChatResult>
async function sendCard(webhookUrl: string, card: TeamsAdaptiveCard, summary?: string): Promise<ChatResult>
```

### Exports
- `TeamsDriver` class (also as `Driver`)
- `driver` -- pre-instantiated singleton
- `send()`, `sendWebhook()`, `sendCard()`, `configure()` functions

## Retry Logic

All three drivers implement `sendWithRetry()`:
- Retries up to `config.maxRetries` times (default 3)
- Waits `config.retryTimeout` ms between attempts (default 1000ms)
- On final failure, throws the error (caught by the outer `send()` which calls `handleError()`)

## Dependencies
- `@stacksjs/types` -- `ChatMessage`, `ChatResult`, `RenderOptions`, `ChatDriver`, `ChatDriverConfig`
- `@stacksjs/logging` -- `log` for info/warn/error logging

## Gotchas
- The default driver for `send()` is `'slack'`, not auto-detected
- Each driver has two modes: webhook (simpler) and bot token (richer, returns message IDs)
- Slack bot token mode uses `https://slack.com/api/chat.postMessage`, not a webhook
- Discord webhook returns `204` on success with no body -- this is handled as success
- Discord bot token mode uses `message.to` as the channel ID
- Teams validates that the webhook URL contains `webhook.office.com`
- Teams uses `message.to` as fallback webhook URL if `config.webhookUrl` is not set
- All drivers set `mrkdwn: true` or equivalent by default
- Block Kit blocks are only built when `message.template` is set (Slack) or when `message.subject`/`message.template` is set (Discord/Teams)
- `validateMessage()` requires both `to` and either `content` or `template`
- The `handle()` callback runs before `onSuccess()` -- both results are merged
- `ChatMessage` supports `onSuccess`, `onError`, and `handle` callbacks that can return partial `ChatResult` objects
- Each driver module exports a pre-instantiated `driver` singleton and both class-based and function-based APIs
- Discord embed default color is `0x5865F2` (Discord blurple), not customizable via the auto-build path
- Teams Adaptive Cards use version `1.4` with the official JSON schema URL

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.