Slack
Slack is the reference channel: everything Ever Async does — signature-verified
ingress, ephemeral nudges with buttons, thread cards, the /async command, the
charter-on-join post, DM digests — is implemented here first.
Ingress URLs
The plugin routes on the path suffix after /ingress/slack. Three suffixes are
handled; anything else is ignored (a 200, not an error):
| Slack feature | URL |
|---|---|
| Event Subscriptions | https://<your-host>/ingress/slack/events |
/async slash command | https://<your-host>/ingress/slack/commands |
| Interactivity & Shortcuts | https://<your-host>/ingress/slack/interactions |
All three must be HTTPS and publicly reachable — Slack refuses plain HTTP. Put a reverse proxy or tunnel in front of the server; see Self-hosting → Reverse proxy / TLS.
Replace <your-host> below with your public hostname.
1. Create the Slack app
api.slack.com/apps → Create New App → From a manifest, pick your workspace, and paste:
display_information:
name: EverAsync
description: The context layer for async work — auto-context and private nudges.
background_color: "#0f1419"
features:
bot_user:
display_name: EverAsync
always_online: true
slash_commands:
- command: /async
url: https://YOUR-HOST/ingress/slack/commands
description: Control Ever Async in this conversation
usage_hint: "status | on | off | sensitivity <level> | delay <secs> | unfurl on|off | charter | digest"
should_escape: false
oauth_config:
scopes:
bot:
- chat:write
- commands
- channels:history
- groups:history
- users:read
- im:write
settings:
event_subscriptions:
request_url: https://YOUR-HOST/ingress/slack/events
bot_events:
- message.channels
- message.groups
- member_joined_channel
interactivity:
is_enabled: true
request_url: https://YOUR-HOST/ingress/slack/interactions
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
Why each scope
| Scope | Needed for |
|---|---|
chat:write | chat.postEphemeral (nudges) and chat.postMessage (thread cards, charter post) |
commands | the /async slash command |
channels:history | message events in public channels the bot is in |
groups:history | message events in private channels the bot is in |
users:read | author display names |
im:write | conversations.open — opening the DM used to deliver digests |
member_joined_channel is what lets Ever Async notice its own invite and
answer with the charter. It resolves its own user id
through auth.test (cached for the process lifetime); if that call fails it
degrades gracefully rather than dropping the request.
Nudges carry Post this / Dismiss buttons. Without the interactivity request URL the nudge still arrives with its reasons and suggestion, but the buttons have nowhere to report to. Enable it.
When Slack validates request_url it sends a url_verification challenge —
the server must already be running at that URL. Ever Async echoes the
challenge automatically.
2. Install and collect credentials
-
Install App → Install to Workspace → authorize.
-
Copy the Signing Secret — Settings → Basic Information.
-
Copy the Bot User OAuth Token (
xoxb-…) — Features → OAuth & Permissions. -
Put both in the environment:
export SLACK_SIGNING_SECRET=...
export SLACK_BOT_TOKEN=xoxb-... -
Reference them from
everasync.toml— the file names the variables, the environment holds the values:[channels.slack]
signing_secret_env = "SLACK_SIGNING_SECRET"
bot_token_env = "SLACK_BOT_TOKEN"
Configuration keys
| Key | Required | Default | Meaning |
|---|---|---|---|
signing_secret_env | ✅ | — | Env var holding the Slack signing secret |
bot_token_env | ✅ | — | Env var holding the bot token (xoxb-…) |
api_base | — | https://slack.com/api | Web API root; override to point at a mock server |
bot_user_id | — | resolved via auth.test | Pre-seeds the bot's own user id and skips one round trip at startup |
3. Invite the bot and confirm
/invite @EverAsync
/async status
Ever Async is per-channel opt-in — it only ever sees channels the bot is a member of. A status reply confirms the full round trip: HTTPS ingress, signature verification, and the bot token.
On invite, Ever Async posts the charter once. That is deliberate: nobody's first contact with the tool should be being nudged by it.
How signature verification works
Every ingress request — events, commands and interactions — is verified
with Slack's v0 scheme before anything is parsed:
X-Slack-Request-Timestampmust be recent (stale timestamps are rejected — this is the replay guard).X-Slack-Signaturemust equalv0=HMAC-SHA256(signing_secret, "v0:" + timestamp + ":" + raw_body).
The HMAC is computed over the raw, undecoded body. That is the single most common deployment failure: a proxy that re-encodes, pretty-prints or re-chunks the body invalidates every signature. Forward requests unmodified.
A bad signature is a hard Verification error → HTTP 401, counted in
ever_async_ingress_rejected_total{reason="verification"}. It never falls
through to processing.
What Slack events become
| Slack payload | Outcome |
|---|---|
url_verification | Challenge — echoed verbatim |
event_callback → message, no subtype | an InboundEvent |
event_callback → message_changed | an InboundEvent with is_edit = true, carrying the original message's ts |
event_callback → member_joined_channel naming our bot | Joined → the charter is posted |
| any bot-authored or otherwise-subtyped message | Ignored |
/async … (form-encoded) | Command |
| a Block Kit button press | Interaction |
Bot messages — including Ever Async's own — are never classified and never nudged.
Egress
| Pipeline action | Slack call |
|---|---|
Ephemeral, EphemeralWithButtons | chat.postEphemeral |
ThreadReply, Post | chat.postMessage |
DirectMessage | conversations.open → chat.postMessage |
Replace | POST to the interaction's response_url |
Slack signals most Web API failures as HTTP 200 with {"ok": false}, so
the ok field is checked on every call — an HTTP status alone would report
success on a failed post. The response_url path is different: it carries no
bearer token (the URL's own one-time token is the authorization) and answers
with the bare body ok. Because that URL is a secret, it is stripped out of
every error message and log line.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Manifest URL validation fails | Server not reachable over HTTPS at request_url, or the server was not running when Slack sent the challenge |
| Events arrive but are rejected (401) | SLACK_SIGNING_SECRET mismatch, a proxy modifying the body, or host clock skew tripping the timestamp check |
/async says dispatch_failed | Slash-command URL wrong, or the server returned non-200 |
| Buttons do nothing | Interactivity disabled, or its request URL is missing/wrong |
| No nudges in a channel | Bot not invited, /async off, quiet hours, or a context card resolved instead — check /async status |
missing_scope in the logs | Reinstall the app after editing scopes; scope changes need a fresh install |
Nothing at all, plugins: [] | [channels.slack] absent or misspelled — an absent section silently disables its plugin |
Run everasync doctor to health-check the token in one step: it calls
auth.test and exits non-zero on failure.