Hive Hive
Sign in

feat(slack): unfurl Hive links in connected workspaces

GitHub issue · Closed

Metadata
Source
tuist/hive #49
Updated
Jun 24, 2026
Domains
Hive
Details

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:

  1. A single big case in the Slack module that pattern-matches every URL shape Hive serves.
  2. 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).

Comments
GA
github-actions[bot] Jun 18, 2026

Blick review didn’t run

The blick review step failed before producing a manifest, so there’s no review to post on this PR. This usually means the agent (opencode) couldn’t start — common causes are an expired or suspended model API key, a missing secret, or the workflow timing out.

See the workflow run for details: https://github.com/tuist/hive/actions/runs/27772984430

Commit: 9ff716a5291c40c6ce83c87f37a38f15963570d7