Skip to main content

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 featureURL
Event Subscriptionshttps://<your-host>/ingress/slack/events
/async slash commandhttps://<your-host>/ingress/slack/commands
Interactivity & Shortcutshttps://<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​

ScopeNeeded for
chat:writechat.postEphemeral (nudges) and chat.postMessage (thread cards, charter post)
commandsthe /async slash command
channels:historymessage events in public channels the bot is in
groups:historymessage events in private channels the bot is in
users:readauthor display names
im:writeconversations.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.

Interactivity is required for one-click rewrites

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​

  1. Install App → Install to Workspace → authorize.

  2. Copy the Signing Secret — Settings → Basic Information.

  3. Copy the Bot User OAuth Token (xoxb-…) — Features → OAuth & Permissions.

  4. Put both in the environment:

    export SLACK_SIGNING_SECRET=...
    export SLACK_BOT_TOKEN=xoxb-...
  5. 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​

KeyRequiredDefaultMeaning
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/apiWeb API root; override to point at a mock server
bot_user_id—resolved via auth.testPre-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:

  1. X-Slack-Request-Timestamp must be recent (stale timestamps are rejected — this is the replay guard).
  2. X-Slack-Signature must equal v0=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 payloadOutcome
url_verificationChallenge — echoed verbatim
event_callback → message, no subtypean InboundEvent
event_callback → message_changedan InboundEvent with is_edit = true, carrying the original message's ts
event_callback → member_joined_channel naming our botJoined → the charter is posted
any bot-authored or otherwise-subtyped messageIgnored
/async … (form-encoded)Command
a Block Kit button pressInteraction

Bot messages — including Ever Async's own — are never classified and never nudged.

Egress​

Pipeline actionSlack call
Ephemeral, EphemeralWithButtonschat.postEphemeral
ThreadReply, Postchat.postMessage
DirectMessageconversations.open → chat.postMessage
ReplacePOST 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​

SymptomLikely cause
Manifest URL validation failsServer 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_failedSlash-command URL wrong, or the server returned non-200
Buttons do nothingInteractivity disabled, or its request URL is missing/wrong
No nudges in a channelBot not invited, /async off, quiet hours, or a context card resolved instead — check /async status
missing_scope in the logsReinstall 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.