Project Slack notifications (stacked on webhooks)
Date: 2026-07-22 Status: Approved design Stack:feat/project-pipeline-webhooks (#1345) โ
feat/project-slack-notifications
Goal
Make Slack the easiest notification destination: click Add to Slack, pick a channel on Slackโs OAuth screen, choose events, done. Keep manual HTTPS endpoints. Surface this in the UI as Notifications (not โWebhooksโ).Non-goals (v2)
- In-app channel picker / changing channel without re-auth
- Bot token +
chat.postMessagemulti-channel routing - Slack Marketplace listing / interactive Slack actions
- Renaming API/DB from
webhooksโnotifications(UI only for now) - Org-level Slack install separate from per-project destinations
Product decisions
Data model
Extendproject_webhooks (no new destination table):
providertext NOT NULL DEFAULT 'generic'CHECK (provider IN ('generic', 'slack'))display_nametext NULLโ e.g.#pipeline-alertsfor Slack rows
urlโ for Slack, thehooks.slack.com/...URL from OAuthsecretโ still generated; not used when posting to Slack (URL is the credential). List/create API must not expose Slack URLs; showdisplay_nameinsteadevents[],enabled, delivery log /delivery_keybehavior
OAuth + create flow
- User selects events in Project Settings โ Notifications.
- Clicks Add to Slack.
- Frontend calls
GET /projects/{project_id}/webhooks/slack/authorize?events=...(requiresproject:update). - Backend returns Slack authorize URL with:
- scope:
incoming-webhook - signed, short-lived
statebindingproject_id, events, user id, nonce
- scope:
- User picks workspace + channel on Slack โ Allow.
- Slack redirects to
GET /api/integrations/slack/callback?code=&state=(or equivalent public callback route). - Backend verifies
state, exchanges code, readsincoming_webhook.{url, channel, channel_id}. - Inserts
project_webhooksrow:provider=slack,url,display_namefrom channel,eventsfrom state. - Redirects browser to Project Settings โ Notifications with success toast. On deny/error, redirect with failure toast.
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, plus
a state-signing secret (dedicated or reused app secret).
One Ionworks Slack App is installed into customer workspaces via this
OAuth flow (distributed app, not per-customer app creation).
UI
Project Settings โ Notifications- Short description: notify on selected project events via Slack or HTTPS
- Event checkboxes (existing pipeline events)
- Primary: Add to Slack
- Secondary: URL field + Add HTTPS endpoint
- Destinations list:
- Slack rows:
Slack ยท #channel(+ events, enabled toggle, menu) - HTTPS rows: host/path truncation (existing behavior)
- Slack rows:
- No one-time HMAC secret dialog for Slack creates
- Test action: Slack-formatted ping for Slack rows; existing test for generic
/webhooks and project_webhooks in
v2 to avoid churn on #1345. Optional public rename later.
Delivery
SharedTaskType.WEBHOOK worker:
Status accents: completed green, failed red, canceled gray; test ping
neutral.
Retries: unchanged (5xx / network raise; 4xx permanent fail). Map Slack
errors like
channel_not_found / no_service to permanent failure.
Idempotency: same delivery_key scheme.
Edge cases
- User denies Slack โ toast, no row
- App install blocked by workspace admin โ surface Slack error + help
- Re-adding the same channel creates another row (no upsert by
channel_idin v2) - Change channel: delete destination + Add to Slack again
- Enabled-cap race: existing advisory lock covers Slack inserts that enable immediately
Testing
- Unit: OAuth state sign/verify; Block Kit builder; delivery branch (Slack vs generic)
- Callback handler with mocked Slack token exchange โ creates
provider=slackrow with expected fields - No live Slack workspace in CI
Implementation stack
- Migration + provider field + API list/create shapes
- Slack OAuth authorize + callback + row create
- Delivery Block Kit branch + host allowlist
- Frontend Notifications rename + Add to Slack button