Skip to content

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.

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.

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:

Terminal window
npx tsx apps/slack-app/generate-manifest.mts --self-host --base-url https://teamhud.example.com

It 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/callback is for connecting Slack. /auth/v1/callback is for Sign in with Slack. Keep the second even if you skip sign-in now, because it costs nothing.
  • openid, email, profile are 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.
  • /teamhud and the “Start a TeamHUD call” message shortcut (the slash_commands, shortcuts and interactivity entries) start a call from Slack; see Calls from Slack.
  • chat:write and commands (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.

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.

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
Terminal window
SLACK_CLIENT_ID=1234567890.1234567890123
SLACK_CLIENT_SECRET=...
SLACK_SIGNING_SECRET=...
SLACK_EVENT_SOURCE=events
ENT_SECRET=... # openssl rand -hex 32 — if you have not already
Terminal window
docker compose up -d
docker compose logs server | grep -i slack

You 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.

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.

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, /teamhud still 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/interactivity and /webhooks/slack/events on https://teamhud.example.com. Slack signs each with your signing secret, so SLACK_SIGNING_SECRET must be set (step 4).

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 is off.
  • SLACK_SIGNING_SECRET: every event Slack sends is verified against it. A wrong value means every event gets 401 and 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. Without ENT_SECRET every 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.

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:

Terminal window
SLACK_APP_AUDIENCE_TOKEN=xapp-1-...

Use this token for nothing else.

If Slack cannot reach your server (no inbound internet, strict firewall), the server can instead open an outbound connection to Slack:

  1. Settings → Socket Mode → enable.

  2. Basic Information → App-Level Tokens → a token with scope connections:write.

  3. In .env:

    Terminal window
    SLACK_EVENT_SOURCE=socket
    SLACK_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.

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:

Terminal window
# offer the button
SOCIAL_PROVIDERS=slack_oidc # not "slack". Add others comma-separated if you configure them.
# GoTrue does the sign-in itself
GOTRUE_EXTERNAL_SLACK_OIDC_ENABLED=true
GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID=<same Client ID>
GOTRUE_EXTERNAL_SLACK_OIDC_SECRET=<same Client Secret>
Terminal window
docker compose up -d # not deploy.sh: GoTrue must be recreated

The 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.

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
  • 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.