Summary
Slack workspaces connected to Hive now unfurl Hive links inline. When a member pastes a link to a spec, meadow, or forage item, Slack expands the message with a preview of the underlying resource: title, summary or excerpt, status, and a deep link back to Hive.
Pasting a stream of Hive links in a planning channel currently produces a wall of bare URLs, which is hard to scan and offers no signal about what each link is. Unfurling closes that gap without anyone leaving Slack, and it works the same way for every URL Hive owns because each resource declares its own unfurl through a small behaviour.
Design
The flow is the standard Slack one: subscribe to link_shared, look the URL up, post the preview back via chat.unfurl.
The interesting design choice is where the “what does this URL look like in Slack?” logic lives. Two options were on the table:
- A single big
case in the Slack module that pattern-matches every URL shape Hive serves.
- A behaviour each domain implements next to its own schema, with a registry that dispatches to the first module that claims a URL.
I chose option 2. It keeps the Slack module ignorant of spec/meadow/forage shapes (so adding a new resource later doesn’t touch Slack code), and it puts the visibility check next to the policy logic that already lives in each domain. The behaviour itself is one callback, unfurl(URI.t()) :: {:ok, payload} | :skip, so the API surface stays tiny.
Concretely:
Hive.Slack.Unfurl (behaviour) and Hive.Slack.Unfurler (dispatcher) — the dispatcher restricts to the configured Hive host and walks [Hive.Specs.SlackUnfurl, Hive.Meadows.SlackUnfurl, Hive.Forage.SlackUnfurl] until one returns a payload.
Hive.Slack.Workers.UnfurlLinks — Oban worker enqueued from a new link_shared clause in Hive.Slack.Events. The worker batches resolved previews into a single chat.unfurl call and skips the API roundtrip when no URL resolves.
Hive.Slack.API.unfurl/2 — thin wrapper over chat.unfurl.
lib/hive/<domain>/slack_unfurl.ex — per-domain implementations for specs (/specs/:number), meadows (/meadows/:id), and forage items (/forage/items/:origin/:id).
Visibility
Slack workspaces are an external surface: anyone with a workspace login can see the unfurl, not just Hive members. So each module treats the request as anonymous — Specs.effective_visibility/1 for private specs, Meadows.fetch_visible_meadow(id, nil) for private meadows, and Forage.get_item_for_user(id, nil) for organization-only items, private-meadow GitHub issues, and Grafana alerts on private instances. Anything that wouldn’t render for a logged-out visitor on the dashboard returns :skip and Slack shows the bare URL.
Operator changes
Default bot scopes gain links:read and links:write. The self-hosting guide and the JSON manifest in docs/guide/self-hosting/slack.md now include link_shared in the bot event list and add the Hive host to app_unfurl_domains. Existing installations need a reinstall to pick up the new scopes; the README and Slack guide call that out.
Testing
mix test — full suite, 531 passed
mix format
mix credo — no issues
Manual Slack roundtrip not verified locally (would need a publicly reachable Hive + a real workspace install).