Your own Slack app
Around fifteen minutes. At the end, your users can connect Slack in TeamHUD through an app your workspace owns, get live updates when a DM or channel message arrives, and (optionally) sign in to TeamHUD with their Slack account instead of a password.
Read INTEGRATIONS.md first. It covers the hostname requirement and the desktop setting without which none of this is used.
TeamHUD does not ship a Slack app. A self-hosted box has none of ours (no client id or secret is in the image), so everything below needs an app you create in your own workspace. Until you do, Slack features are off and Sign in with Slack answers “not set up on this server”.
Throughout, https://teamhud.example.com stands for your PUBLIC_BASE_URL.
What the app is for
Section titled “What the app is for”There are three parts. You can stop after any of them.
| Part | Needs | Without it |
|---|---|---|
| Connect (steps 1–4): users read and send Slack messages from TeamHUD | client id + secret | No Slack in TeamHUD |
| Live updates (“doorbell”): new messages appear immediately | signing secret, ENT_SECRET, Slack can reach your box |
Works, but by polling: slower, and more Slack API calls |
| Sign in with Slack: no TeamHUD password | the same app, three more scopes, one GoTrue setting | Users sign in with email + password |
TeamHUD never reads message text on your server. The server receives events, turns each into a content-free “something changed in channel X” signal, and the user’s desktop fetches the message itself with its own token.
1. Create the app from a manifest
Section titled “1. Create the app from a manifest”https://api.slack.com/apps → Create New App → From a manifest → pick your
workspace → paste the JSON below, after replacing all 5 occurrences of
teamhud.example.com with your hostname.
Prefer not to edit by hand? The repo generates this same manifest for any hostname:
npx tsx apps/slack-app/generate-manifest.mts --self-host --base-url https://teamhud.example.comIt is built from the code that requests the scopes, so it cannot go stale when TeamHUD adds one. The copy below is checked against its output in CI.
{ "display_information": { "name": "TeamHUD", "description": "Your team's TeamHUD server", "background_color": "#101317" }, "features": { "bot_user": { "display_name": "TeamHUD", "always_online": false }, "slash_commands": [ { "command": "/teamhud", "url": "https://teamhud.example.com/slack/commands", "description": "Start a TeamHUD call in this channel", "usage_hint": "[call title]", "should_escape": false } ], "shortcuts": [ { "name": "Start a TeamHUD call", "type": "message", "callback_id": "teamhud_start_call", "description": "Start a TeamHUD call from this thread" } ] }, "oauth_config": { "redirect_urls": [ "https://teamhud.example.com/slack/oauth/callback", "https://teamhud.example.com/auth/v1/callback" ], "scopes": { "bot": [ "channels:history", "groups:history", "channels:read", "groups:read", "reactions:read", "channels:manage", "groups:write", "users:read", "chat:write", "commands" ], "user": [ "users:read", "users.profile:read", "users.profile:write", "channels:read", "groups:read", "im:read", "mpim:read", "channels:history", "groups:history", "im:history", "mpim:history", "chat:write", "im:write", "mpim:write", "channels:write.invites", "groups:write.invites", "files:read", "files:write", "reactions:write", "reactions:read", "emoji:read", "pins:read", "pins:write", "search:read", "search:read.files", "openid", "email", "profile" ] } }, "settings": { "event_subscriptions": { "request_url": "https://teamhud.example.com/webhooks/slack/events", "bot_events": [ "message.channels", "message.groups", "member_joined_channel", "member_left_channel", "reaction_added", "reaction_removed", "user_change" ], "user_events": [ "message.im", "message.mpim", "message.channels", "message.groups", "reaction_added", "reaction_removed" ] }, "interactivity": { "is_enabled": true, "request_url": "https://teamhud.example.com/slack/interactivity" }, "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false }}Notes on what you just pasted:
- Two redirect URLs.
/slack/oauth/callbackis for connecting Slack./auth/v1/callbackis for Sign in with Slack. Keep the second even if you skip sign-in now, because it costs nothing. openid,email,profileare needed only for Sign in with Slack. Remove them if you are sure you will never want it. Adding scopes later means every user re-authorizes.- The request URL may show “not verified” while creating the app. That is expected if your server is not up yet, or not yet on this hostname. Slack re-checks it when you open Event Subscriptions later (step 5). Your server answers Slack’s challenge even before the signing secret is set.
/teamhudand the “Start a TeamHUD call” message shortcut (theslash_commands,shortcutsandinteractivityentries) start a call from Slack; see Calls from Slack.chat:writeandcommands(bot) let the app post the call card and update it, and run/teamhud.- Change the name to whatever your users will recognise, since they see it on Slack’s consent screen. “Acme TeamHUD” is better than a name they might mistake for a phishing app.
Keep scopes exactly as listed. The desktop asks Slack for the user scopes above by name. If the app lacks one, Slack rejects the whole request with
invalid_scope, and nobody can connect. If a TeamHUD upgrade’s release notes add a scope, add it here too.
2. Install it to your workspace
Section titled “2. Install it to your workspace”Install App → Install to Workspace → Allow. If your workspace restricts app installs, a workspace admin has to approve it first (Manage apps in Slack admin).
You do not need Manage Distribution → Public Distribution unless people outside this one workspace will connect. An app that stays in its own workspace needs no Slack review.
3. Collect the credentials
Section titled “3. Collect the credentials”Basic Information → App Credentials:
| Slack calls it | You put it in |
|---|---|
| Client ID | SLACK_CLIENT_ID |
| Client Secret | SLACK_CLIENT_SECRET |
| Signing Secret | SLACK_SIGNING_SECRET |
4. Put the credentials in .env
Section titled “4. Put the credentials in .env”SLACK_CLIENT_ID=1234567890.1234567890123SLACK_CLIENT_SECRET=...SLACK_SIGNING_SECRET=...SLACK_EVENT_SOURCE=eventsENT_SECRET=... # openssl rand -hex 32 — if you have not alreadydocker compose up -ddocker compose logs server | grep -i slackYou want to see:
Slack doorbell: Events API webhook ingest enabled (POST /webhooks/slack/events)and not SLACK_SIGNING_SECRET is unset — webhook will reject all requests.
At this point connecting works. On a desktop configured per INTEGRATIONS.md, click Connect Slack. A browser opens on Slack’s consent screen showing your app’s name. After consent, the browser shows a confirm page naming the TeamHUD account the connection goes to; click Confirm. (It is there so nobody can send someone a consent link and collect their Slack account.)
The person must be signed in to TeamHUD on this same server. Connect sends the
desktop’s TeamHUD session, and the server checks it against its own accounts. A desktop
whose Connect requests go to one server while it is signed in to another (for example,
still pointed at TeamHUD’s hosted server while signed in to yours) gets
authentication_required and cannot connect Slack or Google at all.
5. Confirm Slack can reach you
Section titled “5. Confirm Slack can reach you”Event Subscriptions in the Slack app settings: the request URL should read
Verified ✓. If not, click Retry. Slack must be able to reach
https://teamhud.example.com/webhooks/slack/events from the internet, so a firewall
allowlist that blocks Slack stops it here. If your box cannot accept inbound traffic from
Slack at all, see Socket Mode.
Save changes. If Slack shows a banner asking you to reinstall, do it: adding user events always requires a reinstall.
Calls from Slack
Section titled “Calls from Slack”With the manifest above, anyone in your workspace can type /teamhud Standup in a channel (or use
⋯ → Start a TeamHUD call on a message to post into its thread). TeamHUD makes a call and posts a card
with a Join call button.
- Who can start one: only a person who signed in to TeamHUD with Slack on your server (the link between their Slack and TeamHUD accounts is the one your server verified at sign-in). Anyone else is shown a private “Sign in to TeamHUD with Slack” prompt and no call is made.
- Who can join: signed-in TeamHUD members only, and the card says so. Teammates of the person who started it walk in; other signed-in members knock and are admitted by the host.
- Live participant count: the app posts the card as itself (
chat:write) and edits it as people come and go. Invite the TeamHUD app to a channel for the live card; where it is not a member, the card is posted without the count and never changes. - Upgrading an existing app: a Slack app created before these scopes were added must be updated with the new
manifest and reinstalled (Install App → Reinstall). Until it is,
/teamhudstill works with a static card (no count); TeamHUD org admins who run it are told once a day to reconnect Slack. - The three request URLs Slack must reach are
/slack/commands,/slack/interactivityand/webhooks/slack/eventsonhttps://teamhud.example.com. Slack signs each with your signing secret, soSLACK_SIGNING_SECRETmust be set (step 4).
Live updates (the doorbell)
Section titled “Live updates (the doorbell)”With steps 1–5 done, live updates need nothing further. For completeness, this is what each piece does. When updates do not arrive, the answer is almost always one of these:
SLACK_EVENT_SOURCE=events: without it the server starts no ingest. The default isoff.SLACK_SIGNING_SECRET: every event Slack sends is verified against it. A wrong value means every event gets401and nothing rings. Also keep the host clock right: requests more than five minutes off are rejected as replays.ENT_SECRET: when a user connects Slack, your server signs them a 30-day entitlement for their workspace, and renews it automatically. Their desktop presents it to subscribe to live updates. WithoutENT_SECRETevery subscription is refused (no_ent_secret), and the desktop quietly falls back to polling. Change it and every user must reconnect Slack.- User events (
message.im,message.mpim): the only way anything learns about a DM. A DM has no bot in it, so a bot-only app can never see one.
Group DMs and shared channels
Section titled “Group DMs and shared channels”By default, a group DM or channel message rings only one of your users who are in it. Slack truncates the event’s recipient list to one entry. To ring everyone:
Basic Information → App-Level Tokens → Generate Token and Scopes, name it
audience, add scope authorizations:read, generate, and set:
SLACK_APP_AUDIENCE_TOKEN=xapp-1-...Use this token for nothing else.
No inbound traffic: Socket Mode
Section titled “No inbound traffic: Socket Mode”If Slack cannot reach your server (no inbound internet, strict firewall), the server can instead open an outbound connection to Slack:
-
Settings → Socket Mode → enable.
-
Basic Information → App-Level Tokens → a token with scope
connections:write. -
In
.env:Terminal window SLACK_EVENT_SOURCE=socketSLACK_APP_TOKEN=xapp-1-...
Limitation: Socket Mode rings channels only, not DMs. DMs fall back to polling. Prefer the Events API whenever Slack can reach you.
Sign in with Slack
Section titled “Sign in with Slack”Lets your users create and sign in to their TeamHUD account with Slack. It uses the same app. Only the auth service (GoTrue) needs to know about it:
# offer the buttonSOCIAL_PROVIDERS=slack_oidc # not "slack". Add others comma-separated if you configure them.
# GoTrue does the sign-in itselfGOTRUE_EXTERNAL_SLACK_OIDC_ENABLED=trueGOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID=<same Client ID>GOTRUE_EXTERNAL_SLACK_OIDC_SECRET=<same Client Secret>docker compose up -d # not deploy.sh: GoTrue must be recreatedThe sign-in screen now shows Slack. The button appears only when both are true: the
id is in SOCIAL_PROVIDERS, and GoTrue reports that provider enabled. So a typo in either
place shows no button, not an error.
If someone still gets “Sign-in with Slack is not set up on this server.” Their app is
out of date, or they followed an old link, or the setting changed within the last minute
(GoTrue’s answer is cached for 60 s). The server refuses to start a Slack sign-in unless
both SOCIAL_PROVIDERS lists slack_oidc and all three GOTRUE_EXTERNAL_SLACK_OIDC_*
values are set. Check those, run docker compose up -d, and have the person try again.
Who may sign up. Pick one:
| You want | Set |
|---|---|
| Anyone may sign up, with a password or with Slack | nothing (SIGNUP_MODE=open is the default) |
| Slack only: no self-service passwords, but Slack sign-in creates accounts | SIGNUP_MODE=sso_only and GOTRUE_DISABLE_SIGNUP=false |
Nobody new; admins create accounts at /admin |
SIGNUP_MODE=closed |
sso_only needs GOTRUE_DISABLE_SIGNUP=false because GoTrue is what creates the account
when someone first signs in with Slack. That does not open a back door: the gateway never
forwards GoTrue’s own sign-up endpoint from outside. The legacy DISABLE_SIGNUP=true
blocks Slack sign-up too, so do not use it on a Slack-first box.
Know before you rely on it: each person who signs in with Slack gets their own personal organization by default. Teammates from the same Slack workspace join one organization only if an org admin turns auto-join on for their org (it is always off for a new link). The first time an admin opens Sign-in & channels after their org is created through Slack, the dashboard asks once, with nothing pre-selected, whether to let the workspace join automatically; ignoring it leaves it off. Otherwise, invite them to your org as usual.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause |
|---|---|
| Consent screen shows TeamHUD’s app name, not yours | The desktop is not pointed at your server. See INTEGRATIONS.md |
invalid_scope on connect |
The app is missing a user scope the desktop requests. Compare against the manifest above |
redirect_uri_mismatch / “redirect_uri did not match” |
The Slack app lacks https://<your PUBLIC_BASE_URL>/slack/oauth/callback, e.g. after a hostname change. Add it, or pin the old one with SLACK_REDIRECT_URI |
Connect returns authentication_required (401) |
Not signed in to TeamHUD, or signed in to a different server than the one Connect goes to. Point the desktop at your server (see INTEGRATIONS.md) and sign in there |
Connect returns scope_not_allowed |
The desktop asked for a Slack/Google scope this server does not allow. The desktop and server versions differ; update the older one |
Connect button returns slack_oauth_not_configured |
SLACK_CLIENT_ID is unset or did not reach the container. Check .env, then docker compose up -d |
| Connected, but messages arrive slowly or only on refresh | Doorbell off: check SLACK_EVENT_SOURCE, SLACK_SIGNING_SECRET, ENT_SECRET and the request URL (step 5), in that order |
/teamhud says “dispatch_failed” or the card never appears |
Slack cannot reach /slack/commands, or SLACK_SIGNING_SECRET is unset. Check docker compose logs server; a bad signature answers 401 |
| The card appears but never shows who joined | The app lacks chat:write (reinstall it after updating the manifest) or is not a member of that channel |
| Channels ring, DMs never do | Socket Mode (DMs unsupported there), or the user events were added without reinstalling the app |
| A group DM rings only one person | SLACK_APP_AUDIENCE_TOKEN not set |
| Event request URL will not verify | Slack cannot reach the box: DNS, firewall, or TLS. curl -X POST https://teamhud.example.com/webhooks/slack/events -H 'content-type: application/json' -d '{"type":"url_verification","challenge":"x"}' from outside should print {"challenge":"x"} |
| “Sign-in with Slack is not set up on this server” | slack_oidc is missing from SOCIAL_PROVIDERS, or a GOTRUE_EXTERNAL_SLACK_OIDC_* value is unset so GoTrue has Slack off. Set all, then docker compose up -d |
| “The sign-in service is not reachable right now” | The server cannot reach GoTrue. Check docker compose ps and the auth container’s logs |
| Sign in with Slack returns to the site root and never completes | The redirect target is missing from GOTRUE_URI_ALLOW_LIST. The default covers PUBLIC_BASE_URL; if you changed APP_URL to another host, add it |
| Sign in with Slack works, first-time user is refused | GOTRUE_DISABLE_SIGNUP=true or DISABLE_SIGNUP=true. Use SIGNUP_MODE=sso_only + GOTRUE_DISABLE_SIGNUP=false |
| Testing a local stack through a tunnel | Register the tunnel’s URL in the app and set SLACK_REDIRECT_URI=https://<tunnel>/slack/oauth/callback |
Rotating secrets
Section titled “Rotating secrets”- Client secret: regenerate in Basic Information, update
.env,docker compose up -d. Connected users are unaffected. - Signing secret: same steps. Events fail between the Slack change and your restart.
ENT_SECRET: invalidates every live-update entitlement. Users keep Slack but lose live updates until they reconnect. Rotate only if it leaked.