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'
// 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:
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) // 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 callsmessage.onError()if defined, returnsChatResultwithsuccess: falsehandleSuccess()callsmessage.handle()first, thenmessage.onSuccess()if defined, returnsChatResultwithsuccess: true- Both handlers merge any partial result from callbacks into the final
ChatResult
ChatMessage Interface (from @stacksjs/types)
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)
interface ChatResult {
success: boolean
message: string
provider: string // 'slack' | 'discord' | 'teams'
messageId?: string
data?: Record<string, any>
}
Slack Driver
SlackConfig
interface SlackConfig {
webhookUrl?: string
botToken?: string
maxRetries?: number // default: 3
retryTimeout?: number // default: 1000ms
}
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.webhookUrlis set, sends viaPOSTto webhook URL. Payload includestext,username,mrkdwn: true. Ifmessage.templateexists, builds Block Kit blocks. - Bot token mode: If
config.botTokenis set, sends via Slack APIhttps://slack.com/api/chat.postMessagewithAuthorization: Bearer <token>. Returnsts(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
headerblock frommessage.subject(if present) - A
sectionblock frommessage.contentwithmrkdwntype
Exports
SlackDriverclass (also asDriver)driver-- pre-instantiatedSlackDriversingletonsend(),sendWebhook(),configure()functions
Discord Driver
DiscordConfig
interface DiscordConfig {
webhookUrl?: string
botToken?: string
maxRetries?: number // default: 3
retryTimeout?: number // default: 1000ms
}
DiscordEmbed
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
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. Handles204(empty success) responses. Returnsidfrom response. - Bot token mode: POST to
https://discord.com/api/v10/channels/{channelId}/messageswithAuthorization: Bot <token>. Usesmessage.toas the channel ID.
Embed Building
When message.subject or message.template is set, buildEmbed() creates an embed with:
titlefrommessage.subjectdescriptionfrommessage.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
DiscordDriverclass (also asDriver)driver-- pre-instantiated singletonsend(),sendWebhook(),sendEmbed(),configure()functions
Teams Driver
TeamsConfig
interface TeamsConfig {
webhookUrl?: string
maxRetries?: number // default: 3
retryTimeout?: number // default: 1000ms
}
TeamsAdaptiveCard
interface TeamsAdaptiveCard {
type: 'AdaptiveCard'
version: string // '1.4'
body: TeamsCardElement[]
actions?: TeamsCardAction[]
$schema?: string // 'http://adaptivecards.io/schemas/adaptive-card.json'
}
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?: string }>
}
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.webhookUrlor falls back tomessage.toas 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)
- Title as
Extra Functions
async function sendWebhook(webhookUrl: string, text: string): Promise<ChatResult>
async function sendCard(webhookUrl: string, card: TeamsAdaptiveCard, summary?: string): Promise<ChatResult>
Exports
TeamsDriverclass (also asDriver)driver-- pre-instantiated singletonsend(),sendWebhook(),sendCard(),configure()functions
Retry Logic
All three drivers implement sendWithRetry():
- Retries up to
config.maxRetriestimes (default 3) - Waits
config.retryTimeoutms between attempts (default 1000ms) - On final failure, throws the error (caught by the outer
send()which callshandleError())
Dependencies
@stacksjs/types--ChatMessage,ChatResult,RenderOptions,ChatDriver,ChatDriverConfig@stacksjs/logging--logfor 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
204on success with no body -- this is handled as success - Discord bot token mode uses
message.toas the channel ID - Teams validates that the webhook URL contains
webhook.office.com - Teams uses
message.toas fallback webhook URL ifconfig.webhookUrlis not set - All drivers set
mrkdwn: trueor equivalent by default - Block Kit blocks are only built when
message.templateis set (Slack) or whenmessage.subject/message.templateis set (Discord/Teams) validateMessage()requires bothtoand eithercontentortemplate- The
handle()callback runs beforeonSuccess()-- both results are merged ChatMessagesupportsonSuccess,onError, andhandlecallbacks that can return partialChatResultobjects- Each driver module exports a pre-instantiated
driversingleton 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.4with the official JSON schema URL