Neural network background

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.listhistoryreplies, 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_uri callback, 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/events endpoint — 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_KEY set on the daemon — the Slack bot token is encrypted at rest with this key.

Create the Slack app

  1. Go to https://api.slack.com/appsCreate New AppFrom a manifest.
  2. Paste the contents of slack-app-manifest.yml from this repo (replace your-host with your deployment hostname).
  3. 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 env SLACK_APP_TOKEN (Socket Mode).

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 / false

Socket 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 mounted

Also 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_KEY and stores it in connector_credentials.
  • Inserts a slack_workspace_installs row claiming the workspace globally.
  • Starts the backfill orchestrator (paginated conversations.listhistoryreplies).
  • 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:history is missing from the install consent, MPIMs silently drop out of backfill; the connector logs the missing scope at install time.

See also