Chat SDK
Run Chat SDK bots on Supabase with a Postgres state adapter, an inbox adapter for the in-app widget, and helpers that record Slack, WhatsApp and SMS messages in the inbox.
better-supabase/chat-sdk connects Chat SDK to
Supabase. It adds no tables of its own: the state lives in the
chat-sdk-state SQL module, and conversations live in
the inbox.
pnpm add chat
pnpm better-supabase sql add chat-sdk-state inboxchat is an optional peer (>=4.41 <5), so apps that don't run a bot
never install it. Every platform adapter, such as @chat-adapter/slack, is
the app's own dependency.
| Page | Covers |
|---|---|
| State | createSupabaseState, the StateAdapter on Postgres |
| Inbox adapter | inboxAdapter, a bot that answers in the in-app widget |
| Channels | webhook, inboundHandler, deliver, maintain and installations |
| React | better-supabase/chat-sdk/react, the inbox hooks for chat interfaces |
How a message flows
- A platform webhook reaches the route from
webhook(), which verifies it, stores it in the inbox's event table and answers at once. inboundHandler().drain()replays the stored event into Chat SDK. Your handlers callhandler.mirror(thread, message), which records the message in the inbox, so staff see it next to widget conversations.- A staff reply queues an
inbox_outboundjob;deliver()posts it with the platform adapter and records the delivery. - In
botmode, a contact's message queues aninbox_botjob instead, andinboxAdapter().dispatch()runs your bot's handlers on it. maintain()on a cron reopens snoozed conversations, replays events a crashed request left, purges old events and deletes expired state.
A minimal bot on the widget:
import "server-only";
import { Chat } from "chat";
import { createInbox, sqlTransport } from "better-supabase/blocks/inbox";
import { createSupabaseState, inboxAdapter } from "better-supabase/chat-sdk";
const transport = sqlTransport(postgres.asService());
export const inbox = createInbox({ transport });
export const inboxBot = inboxAdapter({ inbox, userName: "Acme bot" });
export const bot = new Chat({
userName: "Acme bot",
adapters: { inbox: inboxBot },
state: createSupabaseState({ transport }),
});
bot.onNewMention(async (thread, message) => {
await thread.post(`You wrote: ${message.text}`);
});Run the bot jobs from a jobs drain route. Declare
inbox_bot in your queues with the InboxBotJob shape
(conversation_id, message_id), then:
import "server-only";
import { inboxBot } from "@/lib/bot";
import { jobs } from "@/lib/jobs";
export const GET = jobs.drainRoute({
secret: process.env.CRON_SECRET,
handlers: {
inbox_bot: (payload) => inboxBot.dispatch(payload),
},
});Credentials
Platform tokens never sit in the inbox tables. An installation stores a
credential_ref, and channelAdapter asks the
credential provider for the token each time
it builds an adapter, so a rotated or revoked token takes effect on the
next message.
Last updated on