This example runs a tiny Slack bot with Postgres-backed runtime state. When the bot is mentioned in a channel, or messaged directly if you enable the DM event, it subscribes the thread and replies. Later messages in that subscribed thread are routed through Postgres-backed state.
Set up a Slack app as in the tutorial:
Steps 2–3 create the app and collect credentials, and
Use HTTP Instead Of Socket Mode
exposes your machine and sets the request URL. This example also replies to
follow-up messages in subscribed threads, so add the channels:history scope
and message.channels event from
Step 6 too.
| Setting | Value |
|---|---|
| Bot token scopes | chat:write, app_mentions:read, channels:history (add im:history for direct messages) |
| Bot events | app_mention, message.channels (add message.im for direct messages) |
| Request URL | https://YOUR_PUBLIC_HOST/webhooks/slack |
SLACK_BOT_TOKEN |
OAuth & Permissions → Bot User OAuth Token (xoxb-…) |
SLACK_SIGNING_SECRET |
Basic Information → App Credentials → Signing Secret |
Reinstall the app from OAuth & Permissions after changing scopes or events.
From the repository root:
cd examples/slack-postgres-state
docker compose up -d postgresOr let Pitchfork supervise the service:
cd examples/slack-postgres-state
pitchfork start postgresThe database URL for the local Compose service is:
postgres://chat_sdk_go:chat_sdk_go@127.0.0.1:54330/chat_sdk_go?sslmode=disable
To stop Postgres:
docker compose downTo delete the Postgres volume:
docker compose down -vFrom this example directory:
export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."
export DATABASE_URL="postgres://chat_sdk_go:chat_sdk_go@127.0.0.1:54330/chat_sdk_go?sslmode=disable"
export PORT=8080
go run .In Slack, mention the bot in a channel where it is present:
@your-bot hello
The bot replies in the same thread with portable Markdown and subscribes the thread:
**hello** _world_ from Postgres state. This thread is now subscribed.
Send another message in the same Slack thread. The bot should route it as a subscribed message and reply:
Postgres remembered this subscribed thread.
- Runtime state is stored in Postgres, so subscriptions, dedupe data, and locks can survive process restarts while Postgres is still running.
- The Postgres state adapter creates its runtime coordination tables on startup.
- Slack URL verification is handled by the Slack adapter at
/webhooks/slack. - Slack request signatures are verified with
SLACK_SIGNING_SECRET. - The bot token is used for
auth.testduring adapter startup andchat.postMessagewhen replying.
package main in its own module, github.com/coder/chat/examples/slack-postgres-state
[go.mod:module]. It is built from main.go and takes no CLI flags.
It imports github.com/coder/chat, github.com/coder/chat/adapters/slack,
github.com/coder/chat/state/postgres (as chatpostgres), and github.com/jackc/pgx/v5/pgxpool [main.go:main].
| Symbol | Role | Source |
|---|---|---|
main() |
Connects to Postgres, builds the state, the Slack adapter, and the chat.Chat runtime, registers both handlers, and serves the webhook. |
[main.go:main] |
OnNewMention handler |
Subscribes the thread, then posts chat.Markdown("**hello** _world_ from Postgres state. This thread is now subscribed."). |
[main.go:main] |
OnSubscribedMessage handler |
Posts chat.Markdown("Postgres remembered this subscribed thread."). |
[main.go:main] |
/webhooks/slack |
The only HTTP route, on http.DefaultServeMux. It serves bot.Webhook("slack"), which answers 405 to any method but POST. |
[main.go:main] |
mustEnv |
Reads a required environment variable. | [main.go:mustEnv] |
compose.yaml |
Local postgres service and volume. |
[compose.yaml:services.postgres] |
pitchfork.toml |
Pitchfork daemon postgres that wraps the Compose service. |
[pitchfork.toml:daemons.postgres] |
DATABASE_URL,SLACK_SIGNING_SECRET, andSLACK_BOT_TOKENmust be non-empty. OtherwisemustEnvpanics with<NAME> is required[main.go:mustEnv].- An error from
pgxpool.New,chatpostgres.New,slack.New,chat.New,bot.Webhook("slack"), orhttp.ListenAndServepanics [main.go:main]. - The state keys use the
slack-examplenamespace:chatpostgres.NewwithPoolandNamespace: "slack-example"[main.go:main]. - The mention handler subscribes the thread before it posts the reply [main.go:main].
go.modreplacesgithub.com/coder/chatwith../..andgithub.com/coder/chat/state/postgreswith../../state/postgres, so the example builds against the local tree [go.mod:replace].
| Environment variable | Default | Use | Source |
|---|---|---|---|
DATABASE_URL |
none (required) | Postgres connection | [main.go:main] |
SLACK_SIGNING_SECRET |
none (required) | slack.Options.SigningSecret |
[main.go:main] |
SLACK_BOT_TOKEN |
none (required) | slack.Options.BotToken |
[main.go:main] |
PORT |
8080 |
Listen address ":" + PORT |
[main.go:main] |
- Network:
pgxpool.New(ctx, DATABASE_URL)opens a connection pool [main.go:main].maindoes not close it directly: the deferredbot.ShutdowncallsState.Shutdown, which closes it [main.go:main], [state/postgresState.Shutdown]. - Network:
http.ListenAndServeon:$PORTwith the default server, so no server timeouts are set [main.go:main]. - Network:
slack.Newbuilds the Slack adapter, and each handler posts a reply withThread.Post[main.go:main]. - State: runtime state goes to Postgres through
chatpostgres.New[main.go:main]. - Logging:
slog.Default()goes to the adapter and the runtime.mainlogslisteningandchat shutdown failed[main.go:main]. - Lifecycle: a deferred
bot.Shutdown(context.Background())runs whenmainreturns or panics.maininstalls no signal handler [main.go:main]. - Compose service
postgres: imagepostgres:17-alpine, port127.0.0.1:54330:5432, volumepostgres-dataat/var/lib/postgresql/data[compose.yaml:services.postgres]. - The Compose environment sets
POSTGRES_DB,POSTGRES_USER, andPOSTGRES_PASSWORDtochat_sdk_go[compose.yaml:services.postgres]. - Compose healthcheck:
pg_isready -U chat_sdk_go -d chat_sdk_goevery 2s, timeout 5s, 30 retries [compose.yaml:services.postgres.healthcheck]. - Pitchfork daemon
postgresin namespacechat-sdk-go-slack-postgres-state: runsdocker compose up postgres, waits ondocker compose exec -T postgres pg_isready -U chat_sdk_go -d chat_sdk_go, and hasauto = ["start", "stop"][pitchfork.toml:daemons.postgres].
This example is a separate Go module with no _test.go files, so go test
only builds it. From the repository root:
- Run:
cd examples/slack-postgres-state && go test ./... - Run:
cd examples/slack-postgres-state && go vet ./...