{
 "id": "18.4",
 "title": "Slack",
 "department": "Integrations and ecosystem",
 "department_slug": "integrations-and-ecosystem",
 "direction": null,
 "branded": false,
 "status": "draft",
 "owner": "Param",
 "last_verified": "2026-10-26",
 "holds": "The Slack app, events, approvals, the relay",
 "urls": {
  "html": "https://handbook.joinsquareup.com/integrations-and-ecosystem/slack",
  "markdown": "https://handbook.joinsquareup.com/integrations-and-ecosystem/slack.md",
  "json": "https://handbook.joinsquareup.com/integrations-and-ecosystem/slack.json"
 },
 "other_directions": [],
 "tldr": {
  "headline": "Slack is the one connector that writes, and it needs no review",
  "points": [
   {
    "icon": "chat",
    "text": "It posts cards and jams a team picks, and thread replies return to the jam."
   },
   {
    "icon": "check",
    "text": "Public since 16 Oct; any workspace can install it with no Slack review."
   },
   {
    "icon": "lock",
    "text": "Slack never counts as evidence for a Play, and reads only the workspace and channel."
   },
   {
    "icon": "warn",
    "text": "Anyone in the channel can press Approve; Param chose to keep it."
   },
   {
    "icon": "gear",
    "text": "The manifest now says plays, but a push to Slack is not recorded."
   }
  ],
  "stat": {
   "value": "16 Oct",
   "label": "made public"
  },
  "decide": "Who may press Approve?",
  "mood": "solid"
 },
 "sources": [
  "brew origin/main d6154b3: connectors/slack.js:1-64; slack-routes.js:1-51, 81, 90; docs/slack-app-manifest.json; docs/connectors-approvals.md:15, 36; docs/production.md:21, 119-123; tools/prod-env.sh:9; SCOPE.md:14; test/slack-relay.test.js",
  "memory brew-real-connectors, brew-go-live-2026-10-17, brew-live-stores",
  "handbook content/engineering/connectors.md, read for consistency"
 ],
 "open_questions": [
  "**List the app in the Slack Marketplace?** Proposal: no until the name is picked; the listing would carry the old name.",
  "**Who may press Approve?** Param chose anyone in the channel on 17 Oct. Proposal: keep it, and record who pressed it in the jam as it does now.",
  "**The manifest now says plays, and still names the app Brew.** Proposal: push the new description with `tools/slack-app.mjs update` now, and rename the app in one more update once the name is picked."
 ],
 "parts": {
  "Answer": "Slack is the one connector that writes: the Brew Slack app posts the cards and jams a team chooses to send, brings thread replies and Approve back into the jam, needs no Slack review, and is never evidence for a Play.",
  "Today": "### The app\n\n| Fact | Value | Source |\n|---|---|---|\n| App | Brew, A0C98G07J1M, in the joinsquareup workspace, built from `docs/slack-app-manifest.json` with the Slack CLI through `tools/slack-app.mjs create` or `update` | [src: memory brew-real-connectors] |\n| Keys | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET` in Secret Manager; the connector switches on when the first two are set | [src: brew/connectors/slack.js:5-6, 11] [src: brew/docs/connectors-approvals.md:15] |\n| Distribution | Public since 16 Oct: any workspace can install it; no review needed | [src: memory brew-real-connectors] [src: brew/connectors/slack.js:5] |\n| Slack Marketplace listing | Parked; Slack does not require it | [src: brew/docs/connectors-approvals.md:36] |\n| Bot scopes | `chat:write`, `chat:write.public`, `incoming-webhook`, `channels:join`, `channels:read`, `groups:read`, `channels:history`, `groups:history`, `users:read`, `users:read.email`, `team:read` | [src: brew/connectors/slack.js:12] [src: brew/docs/slack-app-manifest.json] |\n| Bot events | `message.channels`, `message.groups`, `app_uninstalled`, `tokens_revoked` | [src: brew/docs/slack-app-manifest.json] |\n| Redirects | `/connect/slack/callback` on brew. and feed. | [src: brew/docs/slack-app-manifest.json] |\n| Manifest copy | Since 26 Oct says \"marketing plays\" and \"We read a brand's own store\"; the app name and the bot are still Brew, left as is until the name is picked. Not yet pushed to Slack in any source we read | [src: brew/docs/slack-app-manifest.json:4-10] [src: brew commit ca2bc7b] |\n| Messages | Slack refusals and replies say we and us, and the name in `Invite @Brew` comes from `PRODUCT_NAME` | [src: brew/connectors/slack.js:7, 32-34] [src: brew/slack-routes.js:49-51] |\n\n### What happens\n\n1. The brand signs in with Continue with Slack and picks a channel on Slack's own screen; Brew joins that channel itself, because Slack only sends a bot replies from channels it is in [src: brew/connectors/slack.js:1-3, 50-52].\n2. Share to Slack and Take it to Slack post for real, with an Approve button [src: memory brew-real-connectors].\n3. Thread replies land in the jam and fold into the brief; what is written in the jam is mirrored to the thread [src: memory brew-real-connectors].\n4. Each thread message is sorted by one cheap call: chatter folds into the brief; a change or any paid image or video becomes a proposal with Make it and Not now, run once on a yes; @Brew applies a text change at once [src: memory brew-real-connectors] [src: brew/slack-routes.js:37-39].\n5. Approve moves the jam to Ready; the button must come from the workspace the brand connected, on the jam's own post [src: brew/slack-routes.js:45-51].\n6. Slack's read is the workspace and channel only: no cards, no ledger rows [src: brew/connectors/slack.js:3-4, 58-62].\n\n### Events and the relay\n\n- Slack sends one app's events to one address: `feed.joinsquareup.com/slack/events` and `/slack/actions` [src: brew/docs/slack-app-manifest.json].\n- Every request is checked against the signing secret, at most 5 minutes old, and answered within Slack's 3 seconds [src: brew/connectors/slack.js:23-29] [src: brew/slack-routes.js:1-6].\n- Production handles workspaces connected to its brands and relays any other to staging byte for byte, with the signature headers, so staging checks it the same way; staging never relays; a forged request is never relayed [src: brew/slack-routes.js:19-26, 81, 90] [src: brew/test/slack-relay.test.js] [src: brew/tools/prod-env.sh:9].\n- Retried events are skipped for an hour by event id [src: brew/slack-routes.js:29-30].\n- A brand at home on one site gets no Slack from the other [src: brew/docs/production.md:21].\n\n### Known limits\n\n| Limit | Source |\n|---|---|\n| Approve can be pressed by anyone in the channel; Param said keep it | [src: memory brew-go-live-2026-10-17] |\n| A real Approve click in the Slack interface was checked only by a signed replay | [src: memory brew-real-connectors] |\n| A brand connected before `channels:join` existed needs `/invite @Brew` | [src: memory brew-real-connectors] |\n| `apps.manifest.update` does not verify the events URL; press Retry on the app's Event Subscriptions page, then Save | [src: memory brew-real-connectors] |\n| The demo site stays simulated | [src: memory brew-real-connectors] |",
  "Tomorrow": "| Item | Version | Tag |\n|---|---|---|\n| Post only where the team sends it, replies only from Brew's own threads | v1 | Stays |\n| Manifest and messages say plays, done on origin/main | v1 | Stays |\n| App name and bot follow the chosen name | v1 | Changes |\n| Approve limited to the brand's team members | v1 | Changes, if Param reverses his call |\n| Weekly digest posted to the channel | v1 | Stays |\n| Scheduled posts to Slack after a yes | v2 | New |\n\nRows that change v1 or add v2 are proposals for Param, not decisions.",
  "How we do it": "1. Change the app only through the manifest, then push it:\n\n```bash\nnode tools/slack-app.mjs update\n```\n\n[src: memory brew-real-connectors]\n\n2. After any events URL change, open the app's Event Subscriptions page, press Retry, then Save [src: memory brew-real-connectors].\n3. Test on Cozy in a test channel; never post into a design partner's workspace to test [src: memory brew-live-stores].\n4. When Slack refuses, show its plain words from `why()`, such as invite @Brew to the channel [src: brew/connectors/slack.js:31-34].",
  "Numbers": "| Metric | Definition | Where measured |\n|---|---|---|\n| Connected workspaces | Slack connections in state live | `connections` on production, Proposed |\n| Replies into jams | Thread replies taken per week | Server log `[slack] … a reply from`, Proposed |\n| Approvals from Slack | Approve presses that moved a jam to Ready | Server log, Proposed |\n| Relayed events | Events production passed to staging | Server log `[slack] relayed`, Proposed |",
  "Open": "1. **List the app in the Slack Marketplace?** Proposal: no until the name is picked; the listing would carry the old name.\n2. **Who may press Approve?** Param chose anyone in the channel on 17 Oct. Proposal: keep it, and record who pressed it in the jam as it does now.\n3. **The manifest now says plays, and still names the app Brew.** Proposal: push the new description with `tools/slack-app.mjs update` now, and rename the app in one more update once the name is picked."
 }
}