TODO: Add description
Trying the fake Slack UI (dev)
Run the self-contained demo server from the project root:
mix slackbox.demo
Then open http://localhost:4000. You'll see a Slack-like UI — a channel
sidebar, messages rendered with Block Kit (section text + buttons), and a
per-message "{ } raw" toggle that shows the outgoing Slack payload. Seeded
messages land in #alerts and #deploys.
Messages update in real time (LiveView). To push your own from an IEx session:
iex -S mix
Slackbox.Demo.start()
Slackbox.Demo.post("hello", "#alerts")The new message appears in the browser instantly. Under the hood the
Slackbox.Adapters.Local adapter writes messages into the in-memory
Slackbox.Store, which broadcasts over Phoenix.PubSub to the dashboard.
Interactive components (inbound loop)
Block Kit buttons in the dashboard are live. Clicking one fires a realistic
simulated Slack block_actions interaction — a real HTTP POST
(application/x-www-form-urlencoded, single payload field, optionally signed
with HMAC-SHA256) to your app's interactivity URL. Your app replies to the
interaction's response_url, and that reply updates the originating message
live in the dashboard.
In mix slackbox.demo this whole loop is wired for you: a sample app
(Slackbox.Demo.SlackApp, mounted at /demo/interactivity) acknowledges the
click and posts back to the response_url, so clicking Acknowledge or
Rollback appends a ✅ ...clicked by @U_DEMO line to the message in front of
you — no browser automation, real HTTP end to end.
The dashboard reads its simulation config from
Application.get_env(:slackbox, :simulator), a map with these keys:
:interactivity_url— where the simulated interaction POST is sent (your app's Slack interactivity request URL).:response_base— base URL forresponse_url; the store token is appended. MountSlackbox.ResponsePlughere to route callbacks back into the store.:signing_secret— Slack signing secret; when set, requests carryx-slack-request-timestamp+x-slack-signature.nil= unsigned.:user— the simulated Slack user id (default"U_DEMO").
A real app wires the same values from its own config, e.g.:
# config/dev.exs
config :my_app, MyApp.Slack,
simulate: [
interactivity_url: "http://localhost:4000/slack/interactivity",
response_base: "http://localhost:4000/slackbox/response",
signing_secret: System.get_env("SLACK_SIGNING_SECRET")
]then mounts Slackbox.ResponsePlug (at response_base) in its router to close
the loop, and puts the resolved map into :slackbox, :simulator before serving
the dashboard.
Modals & events
The dashboard also simulates modals (views) and the Events API.
Modal loop. When your app calls open_view(trigger_id, view) (or, in the
demo, when a block_actions handler does), the modal is registered in the store
and pops up as an overlay in the dashboard. The user fills the inputs and clicks
Save, firing a real HTTP view_submission POST to your interactivity URL;
your app reads view.state.values and reacts. Close fires view_closed
instead. In mix slackbox.demo: open #alerts, click Open config → a
"Configure alert" modal appears → type a name → Save, and the sample app
posts a 🛠️ Config saved: name = … message back into #alerts.
Events. The message-pane header has a ⚡ Simulate event button. Clicking
it delivers an event_callback (an app_mention) to your app's Events API URL
as an application/json body (signed over the raw body when a secret is set).
The demo app replies with a 👋 …you rang? message. This needs one extra
simulator config key:
:events_url— your app's Slack Events API request URL. Events use a JSON body (not form-urlencoded); leave unset to disable the Simulate-event button.
Unit-testing your endpoints (Slackbox.Test)
Slackbox.Test builds the same inbound payloads without the UI or a server, so
you can drive your controllers/plugs directly:
# block_actions — form-urlencoded interaction body
payload = Slackbox.Test.block_actions(action_id: "retry", user: "U1", channel: "#alerts")
conn = post(conn, "/slack/interactivity", Slackbox.Test.form_body(payload))
# view_submission
state = %{"name_block" => %{"name" => %{"type" => "plain_text_input", "value" => "prod"}}}
payload = Slackbox.Test.view_submission(callback_id: "config_modal", state: state)
# Events API — JSON body
payload = Slackbox.Test.event("app_mention", text: "<@U_BOT> hi", channel: "#alerts")
conn = post(conn, "/slack/events", Jason.encode!(payload))Slackbox.Test.signature_headers/2 builds matching signing headers for tests
that verify signature checking.
Usage (outbound + tests)
Define a notifier:
defmodule MyApp.Slack do
use Slackbox.Notifier, otp_app: :my_app
endConfigure the adapter per environment:
# config/test.exs
config :my_app, MyApp.Slack, adapter: Slackbox.Adapters.TestSend messages through the one choke point:
import Slackbox.Message
new()
|> to_channel("#alerts")
|> text("Build failed on main")
|> blocks([
section("Build *failed* on `main`"),
actions([button("Retry", action_id: "retry_build", value: "1234")])
])
|> MyApp.Slack.post_message()Assert in tests:
import Slackbox.TestAssertions
test "notifies #alerts on failure" do
MyApp.Notifier.on_build_failed(build)
assert_message_sent(channel: "#alerts", text: ~r/failed/)
refute_message_sent(channel: "#general")
endSending to real Slack (prod)
Swap in the Live adapter and give it a bot token — it's a thin
Req-based Slack Web API client:
# config/prod.exs
config :my_app, MyApp.Slack,
adapter: Slackbox.Adapters.Live,
token: System.get_env("SLACK_BOT_TOKEN")The same MyApp.Slack.post_message(...) (and update, delete,
post_ephemeral, open_view, respond) code runs in every environment — only
the configured adapter changes:
| Env | Adapter | What happens |
|---|---|---|
| dev | Slackbox.Adapters.Local | renders in the fake Slack dev UI |
| test | Slackbox.Adapters.Test | captured for assert_message_sent/1 |
| prod | Slackbox.Adapters.Live | posts to the real Slack Web API |
Live maps each action to its Slack method (chat.postMessage, chat.update,
chat.delete, chat.postEphemeral, views.open, and a direct POST to a
response_url for respond), returns {:ok, %{ts:, channel:}} /
{:ok, %{view_id:}} on success, and tagged errors otherwise —
{:error, {:slack, reason}}, {:error, {:rate_limited, retry_after}},
{:error, {:http, status}}, or {:error, :missing_token}. Extra config keys:
:base_url (defaults to https://slack.com/api) and :req_options (merged
into every Req request, e.g. timeouts/retries).