Slink
A lightweight Slack bot toolkit for Elixir. One event-handling contract, two interchangeable transports:
- Socket Mode (
Slink.SocketMode) — dials out over a WebSocket, no public endpoint needed. Great for development and internal/behind-firewall apps. - Events API (
Slink.EventsApi.Plug) — aPlugthat receives Slack's HTTP event callbacks. Slack's recommended transport for production and distribution.
Write your bot once; pick the transport per environment. Built on
Mint.WebSocket and
Req, with JSON handled by Elixir's built-in
JSON module — no hand-rolled WebSocket protocol, no Jason dependency of our
own.
Why not slack, slack_elixir, or raw WebSockex?
There is no official Slack SDK for Elixir (Bolt covers JS/Python/Java), and the existing options each leave a gap:
slack(Elixir-Slack) is built on Slack's RTM API, which Slack has deprecated — it no longer works for new apps or bots.slack_elixiris a solid, modern client but Socket Mode only — there's no HTTP Events API transport, which Slack requires for Marketplace/distributed apps and recommends for production. Slink gives you both behind one handler contract.- Rolling your own on WebSockex/Mint means reimplementing envelopes, ACKs,
reconnection, and signature verification yourself. You don't need to — Slink is
the thin Slack-specific layer on top of the maintained
Mint.WebSocket: a few hundred lines you own, not a few thousand.
Installation
def deps do
[
{:slink, "~> 0.1"}
]
end
Requires Elixir ~> 1.19 (built-in JSON module, refined compiler type warnings).
Define a bot
defmodule MyBot do
use Slink
alias Slink.Event
@impl true
def handle_event(%Slink.Event{type: :app_mention} = event, context) do
# Reply to what triggered us — threaded if it was in a thread, else inline.
reply(context, "hi <@#{Event.user(event)}> 👋")
end
def handle_event(_event, _context), do: :ok
end
Handlers are stateless. Known Slack event types arrive as atoms
(:app_mention, :message, …); unknown ones stay strings. To respond, call
reply(context, text, opts) (imported by use Slink) — the channel and thread
come from the event carried in the context, opts[:to] picks placement
(:auto, :thread, :channel), and extra keys like blocks: ride along for
rich replies. reply/3 returns :ok, so a clause can end with it. You can also
just return{:reply, text} (or {:reply, text, opts}) and slink sends it.
Replies route through a per-channel rate limiter (Slack's ~1 msg/sec/channel).
For other Web API calls use Slink.API directly.
Both transports acknowledge the event to Slack before your handler runs and dispatch it off-process, so a slow handler never blows Slack's 3-second ACK window.
Quickstart — connect to Slack in 5 minutes
Socket Mode needs no public URL, no ngrok, no webhooks — the bot dials out to Slack. This is the fastest way to see it work.
1. Create the Slack app
- Go to https://api.slack.com/apps → Create New App → From a manifest.
- Pick your workspace.
- Paste the contents of
manifest.json(shipped in this repo) → Next → Create.
That's all your scopes, event subscriptions, and Socket Mode configured in one shot.
Name your bot. The name is entirely yours —
slink(the library) never hardcodes it, it runs purely off tokens. Before creating, edit these fields inmanifest.json(and use your name in place of@slinkbelow):
Field Controls display_information.namethe app's name in Slack's directory features.bot_user.display_namethe bot's @handle (what members @mention)features.slash_commands[].commandthe slash command, e.g. /yourbot
2. Grab two tokens
- Bot token — left sidebar → OAuth & Permissions → Install to Workspace →
Allow → copy the Bot User OAuth Token (starts with
xoxb-). - App token — left sidebar → Basic Information → scroll to App-Level Tokens →
Generate Token and Scopes → add the
connections:writescope → Generate → copy the token (starts withxapp-).
3. Export them
export SLACK_BOT_TOKEN=xoxb-your-bot-token
export SLACK_APP_TOKEN=xapp-your-app-token
4. Run your bot
Add it to your application's supervision tree:
children = [
{Slink.SocketMode,
module: MyBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")}
]
Supervisor.start_link(children, strategy: :one_for_one)
…or try it right now in IEx, no app needed:
iex -S mix
{:ok, _} =
Slink.SocketMode.start_link(
module: Slink.ExampleBot,
app_token: System.fetch_env!("SLACK_APP_TOKEN"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")
)
5. Say hi
In Slack, invite the bot to a channel with /invite @slink, then mention it:
@slink hello — it replies 👋. Done.
Want the bot to auto-join channels on boot? Pass
join: ["C0123456789"]toSlink.SocketMode. Tune the outbound rate limit withconfig :slink, :rate_interval_ms, 1_000.
Going to production — Events API (HTTP)
For production Slack recommends HTTP over Socket Mode (see the table below). You'll need a public HTTPS endpoint. Run the plug with Bandit:
Bandit.start_link(
plug: {Slink.EventsApi.Plug,
module: MyBot,
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")},
port: 4000
)
…or mount it in an existing Plug/Phoenix router:
forward "/slack/events", to: Slink.EventsApi.Plug,
init_opts: [module: MyBot,
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
bot_token: System.fetch_env!("SLACK_BOT_TOKEN")]
Then in the app's manifest/settings: set "socket_mode_enabled": false, add your
public URL as the Request URL under Event Subscriptions
(https://your-host/slack/events), and copy the Signing Secret from
Basic Information into SLACK_SIGNING_SECRET. Slink answers Slack's
url_verification handshake and verifies every request's signature automatically.
The same MyBot works unchanged across both transports.
Transport choice, per Slack's own guidance
| Socket Mode | Events API (HTTP) | |
|---|---|---|
| Public URL required | No | Yes |
| Slack recommends for | development, internal, behind-firewall | production, reliability, scale |
| Marketplace / distributed apps | ✗ not allowed | ✓ required |
| Concurrency | capped at 10 connections/app | scales horizontally |
Development
mix deps.get
mix test
License
MIT — see LICENSE.