| 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':
import { send, sendToSlack, sendToDiscord, sendToTeams } from '@stacksjs/chat'
await send(message, { driver: 'slack' })
await send(message, { driver: 'discord' })
await send(message, { driver: 'teams' })
await sendToSlack(webhookUrl, text, options?)
await sendToDiscord(webhookUrl, content, options?)
await sendToTeams(webhookUrl, text)
Configure functions:
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:
abstract class BaseChatDriver implements ChatDriver {
public abstract name: string
protected config: Required<ChatDriverConfig>
constructor(config?: ChatDriverConfig)
configure(config: ChatDriverConfig): void
abstract send(message: ChatMessage, options?: RenderOptions): Promise<ChatResult>
protected validateMessage(message: ChatMessage): boolean
protected async handleError(error: unknown, message: ChatMessage): Promise<ChatResult>
protected async handleSuccess(message: ChatMessage, ?: ): <>
}
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)
interface ChatMessage {
to: string | string[]
from?: { id?: string, name?: string, avatar?: string }
subject?: string
content?: string
template?: string
data?: Record<string, any>
attachments?: ChatAttachment[]
onSuccess?: () => void | Promise<void> | Partial<ChatResult>
onError?: (error: Error) => void | Promise<void> | Partial<ChatResult>
handle?: () => void | Promise<void> | Partial<>
[: ]:
}
ChatResult Interface (from @stacksjs/types)
interface ChatResult {
success: boolean
message: string
provider: string
messageId?: string
data?: Record<string, any>
}
Slack Driver
SlackConfig
interface SlackConfig {
webhookUrl?: string
botToken?: string
maxRetries?: number
retryTimeout?: number
}
SlackMessage (for webhook payloads)
interface SlackMessage {
channel?: string
text?: string
blocks?: SlackBlock[]
attachments?: SlackAttachment[]
username?: string
iconEmoji?: string
iconUrl?: string
threadTs?: string
mrkdwn?: boolean
}
SlackBlock
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
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
- 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.
- 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.
- Throws
Error('Slack not configured: provide webhookUrl or botToken') if neither is set.
Direct Webhook Function
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
interface DiscordConfig {
webhookUrl?: string
botToken?: string
maxRetries?: number
retryTimeout?: number
}
DiscordEmbed
interface DiscordEmbed {
title?: string
description?: string
url?: string
color?: number
timestamp?: string
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
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
- Webhook mode: POST to
config.webhookUrl. Handles 204 (empty success) responses. Returns id from response.
- 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
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
interface TeamsConfig {
webhookUrl?: string
maxRetries?: number
retryTimeout?: number
}
TeamsAdaptiveCard
interface TeamsAdaptiveCard {
type: 'AdaptiveCard'
version: string
body: TeamsCardElement[]
actions?: TeamsCardAction[]
$schema?: string
}
TeamsCardElement
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?: }>
}
TeamsCardAction
interface TeamsCardAction {
type: 'Action.OpenUrl' | 'Action.Submit' | 'Action.ShowCard'
title: string
url?: string
data?: Record<string, any>
card?: TeamsAdaptiveCard
}
TeamsMessage
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
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