# Slack integration

> Approve query requests from Slack with a Slack app or an incoming webhook — buttons, live message updates, /queryproxy, signed callbacks, single-use tokens.

- Documentation: Guides
- Updated: 2026-11-04
- Source: https://queryproxy.com/docs/slack-integration/
- Language: en-US
- Author: Muhammet Şafak

---
With the Slack integration, new query requests are announced in a channel with
**Approve / Reject** buttons, and decisions flow back into QueryProxy — with the
same rules as the web (role checks, no self-approval), cryptographically
verified.

There are two ways to connect a team, and they can exist side by side:

| | Slack app | Incoming webhook |
| :-- | :-- | :-- |
| Set up by | An administrator once for the installation, then a DBA per team with **Add to Slack** | A DBA per team |
| Messages | Posted by the bot and **updated in place** through the whole life of the request | Posted once; updated after the decision |
| Extras | Thread summary, opt-in direct messages, the `/queryproxy` command, identity suggestions | None |
| Secret | The app's signing secret, in the environment | A signing secret per team, in the database |

When a team has both, the app is used. The webhook mode is unchanged from earlier
versions and is described [below](#incoming-webhook-setup).

## The Slack app

One Slack app serves every team of an installation. The operator creates it once
and sets its credentials in the environment; each team then installs it into its
own workspace with one click.

### Create the app

1. Create a Slack app at [api.slack.com/apps](https://api.slack.com/apps).
2. Under **OAuth & Permissions**, add the redirect URL
   `https://your-queryproxy-host/integrations/slack/callback` and these bot
   scopes: `chat:write`, `chat:write.public`, `commands`, `im:write`,
   `incoming-webhook`, `users:read` and `users:read.email`.
3. Under **Interactivity & Shortcuts**, turn interactivity on with the request URL
   `https://your-queryproxy-host/webhooks/slack/interactions`.
4. Under **Slash Commands**, create `/queryproxy` with the request URL
   `https://your-queryproxy-host/webhooks/slack/commands`.
5. Under **Event Subscriptions**, set the request URL
   `https://your-queryproxy-host/webhooks/slack/events` and subscribe to the bot
   events `app_uninstalled` and `tokens_revoked`.
6. Copy the client id, the client secret and the signing secret from **Basic
   Information** into the environment and restart the containers:

   ```dotenv
   QUERYPROXY_SLACK_CLIENT_ID=...
   QUERYPROXY_SLACK_CLIENT_SECRET=...
   QUERYPROXY_SLACK_SIGNING_SECRET=...
   ```

Until these are set, the ChatOps page says which variables are missing and shows
no **Add to Slack** button.

### Install it for a team

A DBA of the team (or a system administrator) opens **ChatOps** and presses
**Add to Slack**. Slack asks for consent and for the channel that should receive
the messages; the browser then returns to QueryProxy. The page shows the
workspace, the channel and who installed it. Pressing the button again installs
into another channel or workspace.

The install uses OAuth v2 with a single-use `state` tied to the browser session
and valid for 10 minutes; a callback with a missing, stale or foreign `state`
is refused with `403` and Slack is not contacted. The bot token is stored
encrypted, per team. The webhook URL that Slack issues for the channel is not
stored. The audit log records `slack.installed`, never a token.

### What the app does

- **Cards that stay current.** The request card is posted by the bot and edited
  in place as the request moves: approved, rejected, cancelled, running,
  completed or failed. There is no new message per step. A card replaced by a
  reminder is edited to say *Moved: use the newer message*, with its buttons
  removed.
- **A thread summary.** When a run finishes, the bot replies in the card's
  thread with the status, the row count, the duration and a link. The reply has
  no SQL, no result data and no error text.
- **Direct messages to approvers.** An opt-in switch on the ChatOps page sends
  each team DBA who has a Slack member ID (except the requester) a short message
  with a link. It carries no SQL and no buttons.

### The /queryproxy command

`/queryproxy` opens a form in Slack with the connections the person may use. The
submission files an ordinary request, marked as coming from Slack, that passes
the same guards and waits for the same approval. Before the form opens, and again
when it is submitted, QueryProxy checks that the Slack user is matched to an
active QueryProxy user, that the connection's team has installed the app in that
workspace, and that the user may use the connection; otherwise only that person
sees an error. Sending the same form twice does not file two requests. If several
QueryProxy teams share one Slack workspace, the form lists the usable connections
of all of them, up to 100.

### Matching Slack users to QueryProxy users

Slack clicks act as QueryProxy users through the **Slack member ID** on the user
(see the last step of [the webhook setup](#incoming-webhook-setup)). After an
install, and once a week, QueryProxy looks up each user who has none by e-mail
address and suggests a match. Suggestions appear under **Slack matching
suggestions** on the Users page. A system administrator **approves** one, which
links the member ID and is audited as `chat_identity.linked` with the source
`slack_suggestion`, or **rejects** it, which deletes the suggestion (the weekly
scan can propose it again). Nothing is linked automatically.

### Removing the app

**Remove app** on the ChatOps page (DBA or administrator) deletes the stored bot
token and the workspace's suggestions and writes `slack.uninstalled` with the
reason `removed_by_user` and the person. It does **not** tell Slack to revoke the
token, so also remove the app from the workspace in Slack. If the team has a
webhook configured, it keeps working.

QueryProxy also drops the install on its own when Slack sends `app_uninstalled`
or `tokens_revoked`, or rejects the bot token (`slack.uninstalled`, reason
`token_rejected`).

### Settings

| Variable | Default | Meaning |
| :-- | :-- | :-- |
| `QUERYPROXY_SLACK_CLIENT_ID` | none | Client id of the Slack app |
| `QUERYPROXY_SLACK_CLIENT_SECRET` | none | Client secret of the Slack app |
| `QUERYPROXY_SLACK_SIGNING_SECRET` | none | Signing secret of the Slack app; verifies every request Slack sends |
| `QUERYPROXY_SLACK_RATE_LIMIT` | `300` | Requests per minute and Slack workspace (interactions, commands and events), counted after the signature is verified |

Before the signature is checked, each address may send 120 requests per minute to
the Slack endpoints; beyond that the answer is `429`. The two limits are
separate so that forged traffic cannot use up a workspace's budget. The Teams
endpoint keeps its own limit.

Upgrading runs two migrations that make a team's webhook URL and signing secret
optional, so a team can use the app alone. A team that has the app but no
signing secret of its own cannot save a webhook URL until a system administrator
enters one; no current secret is asked for in that first entry.

## Incoming webhook: setup

1. **Create a Slack app** for your workspace at
   [api.slack.com/apps](https://api.slack.com/apps).
2. Enable **Incoming Webhooks** and add a webhook for your approvals channel
   (e.g. `#db-approvals`).
3. Enable **Interactivity** and set the request URL to:

   ```
   https://your-queryproxy-host/webhooks/slack/interactions
   ```

4. In QueryProxy, open **ChatOps**. A DBA sets the **webhook URL** and the
   enabled flag; the app's **signing secret** can only be entered by a **system
   administrator**. Both are stored encrypted. An integration cannot be saved
   without a secret, so the first save is an administrator's. The webhook URL
   must be `https://` and its host must be on the allowlist (`hooks.slack.com`,
   `*.webhook.office.com`, `*.logic.azure.com` and `*.powerplatform.com` by default; the list is shared
   across chat providers; extend it with `QUERYPROXY_CHAT_WEBHOOK_ALLOWED_HOSTS`).
5. As a system administrator, open **Teams & Users → Manage users** and fill
   each reviewer's **Slack member ID** (e.g.
   `U0123ABC` — visible in a Slack profile under *Copy member ID*). This maps
   Slack clicks to QueryProxy users.

> **Breaking change — the signing secret is administrator-only.** From this
> version a DBA can no longer enter or rotate it, and rotating an existing
> secret requires proving the current one. The reason is what an HMAC can and
> cannot say: it proves the caller knows the secret, never that a particular
> person approved. Anyone who could *choose* the secret could sign a callback
> approving their own request in a colleague's name — and the audit trail would
> record the colleague. If a DBA configured ChatOps on an existing install, they
> already know the current value: rotate it once, as an administrator, after
> upgrading.

**Removing** an integration is administrator-only for the same reason: putting
it back needs a signing secret, which only an administrator can set, so a delete
would be a one-way door out of ChatOps for the whole team. The DBA's day-to-day
control is the **Enabled** flag — untick it and save to switch the integration
off, with everything else kept.

## What it looks like

When a request is submitted, QueryProxy posts a message with the requester,
the connection, a SQL preview (cut at 400 characters; string and number
literals show as `?` by default, and `QUERYPROXY_CHAT_INCLUDE_SQL` switches the
preview to `full` or `none`) and three actions: **Approve**, **Reject** and
**Open in QueryProxy**. After a decision the message is updated in place —
buttons disappear and the outcome is shown, so a request can't be decided twice
from a stale message.

Rejections from Slack record the generic reason "Rejected via Slack by
&lt;name&gt;". For a written justification, reject from the web UI.

An **Approve** press that would be an admin **override** (the approver is not a
DBA of the team, or decides their own request) is not applied. Every override
needs a written reason, so Slack answers with a link to the web form where you
enter it; the action token is not spent. A request that stays `pending` for
`QUERYPROXY_PENDING_REMIND_AFTER` hours (default `4`) gets one reminder with a
fresh message, and a button of an earlier message that has expired answers with
a pointer to the newer one.

> **Breaking change — buttons in messages posted before this version no longer
> work.** They carry no action token, so their callbacks are refused. Decide
> those requests in the web UI; every message posted from this version onwards
> behaves as described below.

## Security

Every callback from Slack is verified before anything happens:

- **Signature** — the `X-Slack-Signature` header must match
  `v0=HMAC_SHA256("v0:{timestamp}:{raw_body}", signing_secret)`, compared in
  constant time.
- **Replay protection** — requests older than **±5 minutes** (per
  `X-Slack-Request-Timestamp`) are rejected.
- **Single-use action token** — each **Approve** / **Reject** button carries a
  token bound to that one request, valid for **24 hours** and spent on the first
  decision. So a callback can only answer a message QueryProxy actually posted,
  and only once: knowing the signing secret is no longer enough to forge a
  decision or replay a captured one. The token is burned only once the decision is
  actually recorded, so a click turned away by authorization does not use it up.
- **Identity** — the clicking Slack user must be mapped to a QueryProxy user
  who belongs to the integration's team and holds the DBA role there — or is a
  system admin. Unmapped or unauthorized users get a private error message and
  nothing changes.
- **Rules** — self-approval is blocked in Slack exactly as on the web. As on the
  web, an admin may override the DBA-role rule and the self-approval block, and
  an admin's approval that does so is flagged in the audit log.

A forged or stale callback returns `401` and changes no state. Callbacks are
verified with the Slack app's signing secret (`QUERYPROXY_SLACK_SIGNING_SECRET`)
or, for the incoming-webhook mode, with the team's own signing secret. The
`/queryproxy` command, the form it opens and the Events endpoint accept **only**
the app's secret: a team-specific secret is refused there. The Slack endpoints are
rate-limited, as described under [Settings](#settings).
