Slack Connector
Two-way sync between a Slack workspace and the Fabric graph — channel/DM messages, threads, and reactions. Emits chat.postMessage and reactions.add back.
What it does
- Ingests every message + reaction in channels the bot is invited to, plus DMs and group DMs.
- Backfills the workspace history via
conversations.list→history→replies, page by page. - Emits back when Fabric posts a message or adds a reaction — atomic dedup on echo via a two-tier filter so the bot's own writes never feed back into the graph.
- Survives reinstall — uninstall is transactional: workspace claim, credentials, status all flip atomically; reinstalling the same workspace afterward just works.
One install per Slack workspace across the entire deployment — Slack's single-app identity model means two tenants installing the same workspace would share bot identity at Slack's side, so we refuse it. See Conflicts below.
Prerequisites
- Slack admin who can install apps in the target workspace.
- A reachable deployment URL for OAuth — during install, Slack redirects the installer's browser to your
redirect_uricallback, so the deployment must be reachable from the installer (typically a public URL). - For HTTP transport: Slack must also be able to POST Events API payloads to your
/api/v1/connectors/slack/eventsendpoint — inbound from Slack's network. - For Socket Mode: no inbound traffic needed. The daemon opens an outbound WSS connection to Slack; OAuth is still subject to the redirect rule above.
FABRIC_MASTER_KEYset on the daemon — the Slack bot token is encrypted at rest with this key.
Create the Slack app
- Go to https://api.slack.com/apps → Create New App → From a manifest.
- Paste the contents of
slack-app-manifest.ymlfrom this repo (replaceyour-hostwith your deployment hostname). - Save → Slack provisions the app. Visit the app's Basic Information page and copy:
- Signing Secret → daemon env
SLACK_SIGNING_SECRET(HTTP transport). - App-Level Token (generate one with
connections:write) → daemon envSLACK_APP_TOKEN(Socket Mode).
- Signing Secret → daemon env
Pick a transport
The connector supports two mutually-exclusive transports — pick one per deployment:
HTTP Events API (default)
Slack POSTs events to your deployment over the public internet. Easier to debug (events flow through your normal HTTP stack) and matches how production SaaS apps usually deploy.
export SLACK_CLIENT_ID=… # from app's "Basic Information"
export SLACK_CLIENT_SECRET=…
export SLACK_SIGNING_SECRET=…
export SLACK_APP_TIER=non_marketplace # or "marketplace" if approved
# SLACK_USE_SOCKET_MODE unset / falseSocket Mode
Daemon opens an outbound WSS connection to Slack — no inbound HTTP needed, useful behind a NAT/firewall.
export SLACK_CLIENT_ID=…
export SLACK_CLIENT_SECRET=…
export SLACK_APP_TOKEN=xapp-… # from app's "App-Level Tokens"
export SLACK_APP_TIER=non_marketplace
export SLACK_USE_SOCKET_MODE=true
# SLACK_SIGNING_SECRET unset — HTTP events route won't be mountedAlso flip Settings → Socket Mode → Enable in the Slack app config. Slack enforces mutual exclusion at its side too — having both transports armed would either double-deliver events or silently break depending on the app config order.
The daemon reconnects with exponential backoff (1s → 60s, full jitter) if the WSS connection drops.
Install into a workspace
Once env is set and the daemon is running, walk the OAuth flow per tenant:
# 1. Mint an install URL — the response carries `authorize_url`.
curl -sX POST \
"http://localhost:3000/api/v1/connectors/slack/oauth/start" \
-H "Authorization: Bearer $FABRIC_API_KEY" \
-d '{"redirect_uri": "https://your-host.example.com/api/v1/connectors/oauth/callback"}'
# 2. Open the returned authorize_url in a browser; Slack walks you through
# install + scope consent and redirects back to the callback URL above.
# The callback finalizes the install and writes credentials.
# 3. Verify the install:
curl -s "http://localhost:3000/api/v1/connectors/installs?connector_id=slack" \
-H "Authorization: Bearer $FABRIC_API_KEY"The connector then:
- Encrypts the bot token under
FABRIC_MASTER_KEYand stores it inconnector_credentials. - Inserts a
slack_workspace_installsrow claiming the workspace globally. - Starts the backfill orchestrator (paginated
conversations.list→history→replies). - Begins receiving live events on whichever transport is active.
Conflicts
A second install of the same Slack workspace — even from a different tenant — fails with HTTP 409 at the OAuth callback. The Slack-side bot identity is shared across that one app, so two tenants would step on each other; the conflict is enforced atomically by the slack_workspace_installs.team_id primary key.
Reinstall after uninstall
Uninstall (DELETE /api/v1/connectors/installs/<install_id>) is transactional: the workspace claim row, credentials revocation, and install status flip happen in one Postgres transaction. After uninstall, the next install of the same workspace just works — no 409, no manual cleanup. (Pre-v1 this required operator intervention because the slack_workspace_installs row was left orphaned.)
Emit a message
curl -sX POST \
"http://localhost:3000/api/v1/connectors/installs/<install_id>/emit" \
-H "Authorization: Bearer $FABRIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "slack.message",
"body": {
"channel_id": "C0123456789",
"text": "Hello from Fabric"
}
}'Response carries EmitReceipt.provider_id = Slack's ts (use it to reply in-thread later). Emitted messages are not ingested back into the graph — the two-tier self-emit filter drops them on the inbound side.
Rate limits
Slack throttles chat.postMessage at ~1 msg/sec per channel on top of the workspace-wide write budget. The emit route charges both buckets atomically — a throttled per-channel post returns 429 with a Retry-After header without consuming a workspace-wide token. Burst-posting to unrelated channels still works at the workspace cap (~100/min).
React to a message
curl -sX POST \
"http://localhost:3000/api/v1/connectors/installs/<install_id>/emit" \
-H "Authorization: Bearer $FABRIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "slack.reaction",
"body": {
"channel_id": "C0123456789",
"timestamp": "1736000000.000001",
"name": "thumbsup"
}
}'Uninstall
curl -X DELETE \
"http://localhost:3000/api/v1/connectors/installs/<install_id>" \
-H "Authorization: Bearer $FABRIC_API_KEY"Effects:
- Slack token revoked at Slack (
auth.revoke). - Local credentials marked revoked, install status flipped to
uninstalled, workspace claim DELETEd — all in one transaction. - Backfill worker stops ingesting (it gates on
status == "active").
If the Slack auth.revoke call succeeds but the local transaction then fails, the Slack-side token is dead while the install row stays active. Retrying uninstall converges — auth.revoke is idempotent at Slack's end and the local tx either commits cleanly or surfaces the same error for ops to investigate.
Limitations
- One install per workspace per deployment. Cross-tenant workspace sharing is rejected with 409 — see Conflicts.
- Files are referenced, not downloaded. A message with attachments lands in Fabric with
payload.files = [{ file_id, name, mimetype, size, permalink }]— the connector does not fetch file bytes in v1. - DM coverage requires scope. If
mpim:historyis missing from the install consent, MPIMs silently drop out of backfill; the connector logs the missing scope at install time.
See also
- All connectors — other available connector integrations.
slack-app-manifest.yml— Slack app manifest to paste into the App Directory.- Issue #231 — the design issue for this connector.
