> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ionworks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2026 07 22 project slack notifications design

# 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.postMessage` multi-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

| Decision             | Choice                                                |
| -------------------- | ----------------------------------------------------- |
| Connect UX           | Slack OAuth channel picker (`incoming-webhook` scope) |
| Manual HTTPS         | Keep alongside Slack                                  |
| Slack message body   | Block Kit (pretty); not raw Ionworks JSON             |
| Events after connect | Same event checkboxes as generic destinations         |
| Architecture         | Slack is a `provider` on `project_webhooks`           |
| UI label             | **Notifications** / Destinations                      |

## Data model

Extend `project_webhooks` (no new destination table):

* `provider` `text NOT NULL DEFAULT 'generic'`
  `CHECK (provider IN ('generic', 'slack'))`
* `display_name` `text NULL` — e.g. `#pipeline-alerts` for Slack rows

Unchanged:

* `url` — for Slack, the `hooks.slack.com/...` URL from OAuth
* `secret` — still generated; **not used** when posting to Slack (URL is
  the credential). List/create API must not expose Slack URLs; show
  `display_name` instead
* `events[]`, `enabled`, delivery log / `delivery_key` behavior

Cap of **5 enabled** destinations per project still applies to Slack rows.

## OAuth + create flow

1. User selects events in Project Settings → **Notifications**.
2. Clicks **Add to Slack**.
3. Frontend calls
   `GET /projects/{project_id}/webhooks/slack/authorize?events=...`
   (requires `project:update`).
4. Backend returns Slack authorize URL with:
   * scope: `incoming-webhook`
   * signed, short-lived `state` binding `project_id`, events, user id,
     nonce
5. User picks workspace + channel on Slack → Allow.
6. Slack redirects to
   `GET /api/integrations/slack/callback?code=&state=`
   (or equivalent public callback route).
7. Backend verifies `state`, exchanges code, reads
   `incoming_webhook.{url, channel, channel_id}`.
8. Inserts `project_webhooks` row:
   `provider=slack`, `url`, `display_name` from channel, `events` from
   state.
9. Redirects browser to Project Settings → Notifications with success
   toast. On deny/error, redirect with failure toast.

Platform env (Infisical): `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)
* No one-time HMAC secret dialog for Slack creates
* Test action: Slack-formatted ping for Slack rows; existing test for
  generic

Backend routes/table names stay `/webhooks` and `project_webhooks` in
v2 to avoid churn on #1345. Optional public rename later.

## Delivery

Shared `TaskType.WEBHOOK` worker:

| Provider  | Body                                                                  | Auth headers                  | SSRF                                                                                |
| --------- | --------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------- |
| `generic` | Existing signed Ionworks JSON                                         | HMAC + delivery/event headers | Existing guards                                                                     |
| `slack`   | Block Kit JSON (`text` fallback + blocks: title, name, project, link) | None (URL secret)             | Allow `hooks.slack.com` (host allowlist); do not apply private-IP deny to that host |

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_id` in 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=slack` row with expected fields
* No live Slack workspace in CI

## Implementation stack

```
main
 └── feat/project-pipeline-webhooks   #1345
      └── feat/project-slack-notifications   ← this work
```

Suggested implementation slices (for the plan, not separate stacks
unless the PR grows):

1. Migration + provider field + API list/create shapes
2. Slack OAuth authorize + callback + row create
3. Delivery Block Kit branch + host allowlist
4. Frontend Notifications rename + Add to Slack button
