Chat-interactive deploy approvals
When a deploy or promotion targets a protected environment, Levelrail requires a second, distinct user to approve it (see docs/deploy-safety.md for the two-person approval gate itself). By default an approver has to open the dashboard's Approvals queue to act. This feature adds a real Approve/Deny button to the Slack or Discord message Levelrail can send when a request comes in, so an approver can decide from their phone without opening the dashboard.
This is an opt-in addition to an existing notification channel (Settings -> Notification channels), not a separate integration: connect a Slack or Discord channel first the normal way, then turn on "Interactive approval buttons" on that channel's edit dialog.
What you need
Slack
Levelrail's existing Slack notification channel sends through an Incoming Webhook. To receive button clicks back, that webhook needs to belong to a Slack app with Interactivity turned on:
Create a Slack app at https://api.slack.com/apps (or use the one your Incoming Webhook already belongs to).
Under Interactivity & Shortcuts, turn Interactivity on and set the Request URL to:
https://<your-levelrail-host>/api/v1/webhooks/slack/interactionsUnder Basic Information, copy the Signing Secret. This is what Levelrail uses to verify an incoming button click really came from your Slack app; paste it into the channel's "Interactive approval secret" field.
Make sure the same app owns the Incoming Webhook URL the channel is already configured with (
api.slack.com/apps/<app>/incoming-webhooks).
Discord
Discord's equivalent of a Request URL is an application's Interactions Endpoint URL, and verification uses the application's public key rather than a shared secret:
Open your application at https://discord.com/developers/applications (the same one your Incoming Webhook belongs to; a webhook is always owned by an application).
Under General Information, copy the Public Key and paste it into the channel's "Interactive approval secret" field.
Set Interactions Endpoint URL to:
https://<your-levelrail-host>/api/v1/webhooks/discord/interactionsDiscord sends a verification ping to this URL the moment you save it and refuses to save the URL if the ping isn't answered correctly, so this step must happen after the channel's secret is already saved in Levelrail with interactive approvals turned on.
Turning it on
In Settings -> Notification channels, edit an existing Slack or Discord channel, paste the secret from the step above, and turn on "Interactive approval buttons." It's off by default: without a secret configured there is nothing to verify an incoming click against, so the toggle requires one.
What it actually does, and what it doesn't
- A deploy or promotion that needs approval posts a message with Approve and Deny buttons to every channel that has this turned on.
- Clicking a button decides the approval through the exact same code path the dashboard's own Approve/Deny buttons use (the same not-pending check, the same "the requester can't also approve their own request" check, the same freeze gate). It cannot apply a change the dashboard path wouldn't also allow.
- Identity scope, stated plainly: Levelrail has no mechanism linking a Slack or Discord user to a Levelrail account. A verified click is authorized at the channel level, not the individual level: anyone who can click the button in that Slack channel or Discord server can decide the approval, the same way anyone with access to a configured webhook URL can already trigger an outbound notification. The clicking user's name is recorded on the decision for visibility, but it is not what authorizes the decision. Treat the Slack channel or Discord server you point this at as the trust boundary, not individual membership in it.
- A forged or replayed request cannot approve anything: every incoming request is signature-verified against the channel's own secret before anything else happens, and an already-decided approval cannot be decided again (a second click, or a retried delivery, gets told "no longer pending" rather than re-applying).
Endpoints
| Platform | Endpoint | Auth |
|---|---|---|
| Slack | POST /api/v1/webhooks/slack/interactions | X-Slack-Signature + X-Slack-Request-Timestamp, HMAC-SHA256 against the channel's signing secret |
| Discord | POST /api/v1/webhooks/discord/interactions | X-Signature-Ed25519 + X-Signature-Timestamp, Ed25519 against the channel's public key |
Both routes are unauthenticated by session or API token, the same way POST /api/v1/webhooks/github/{name} is: neither platform can present one. Their own signature check is what stands in for auth, and every request that fails it is rejected outright with no state change.