For operators — someone wiring @temper mentions in Slack against a Temper
deployment.
By the end of this playbook you will have a Slack bot that responds to @temper
mentions. A user who mentions the bot for the first time receives a private
connect link; completing it links their Slack identity to their Temper profile.
A linked user who mentions the bot gets a real answer: the bot dispatches a full
model turn under that user's Temper identity, so every lookup runs as them.
One deployment serves exactly one Slack workspace. This is a hard ceiling
in the eve runtime, not a setup choice. Self-hosted (one app : one workspace :
one Temper) is the natural shape.
Three pieces are tied together by one shared secret:
| Piece | What it is | Where it lives |
|---|---|---|
| Temper API | Serves the OAuth callback, resolves the user, holds the encrypted grant vault. | Your Temper deployment. |
| The mention agent | An eve app that watches Slack, asks Temper "what do I say to this user?", and posts the connect link. | Its own Vercel project. |
| The Slack app | The bot users mention. Created from the manifest below. | api.slack.com, one app per workspace. |
The shared secret is SLACK_LINK_SECRET, which must be byte-identical on
the Temper API and the agent. A mismatch produces a 401 on every mention, not
a warning.
The link flow is an OAuth client of whatever issuer fronts your instance,
using Authorization Code + PKCE with no client secret (it mirrors the Temper
CLI).
Auth0 mode:
<PUBLIC_BASE_URL>/api/auth/slack/callbackoffline_access.Temper AS mode (self-hosted): register the same redirect_uri in the
client's configuration; endpoints are derived on the instance itself.
The requested scopes are fixed: openid profile email offline_access.
email/profile let the link resolve the user by the IdP's email;
offline_access returns the refresh token the vault stores.
Set these on your Temper deployment. All four are required as a unit — any
missing, or a malformed vault key, disables the whole link flow (fail-closed).
There is no half-on state.
| Variable | What it is |
|---|---|
SLACK_LINK_CLIENT_ID | The public client from the previous step. |
SLACK_LINK_SECRET | Shared HMAC secret gating the link-state endpoint. Generate with openssl rand -hex 32. Must match the agent's copy. |
PUBLIC_BASE_URL | This instance's public origin, e.g. https://temperkb.io. The callback is <PUBLIC_BASE_URL>/api/auth/slack/callback. |
SLACK_VAULT_ENC_KEY | AEAD key encrypting each stored refresh token. 32 bytes, base64 — openssl rand -base64 32. |
One optional variable names vault keys you have deliberately retired. It is
not secret and is not part of the four:
| Variable | What it is |
|---|---|
SLACK_VAULT_RETIRED_KEY_FINGERPRINTS | Comma-separated fingerprints (16 hex characters each) of previous SLACK_VAULT_ENC_KEYs, plus optionally unstamped. A stored grant sealed under one of them is answered with a reconnect link instead of an error. Copy each fingerprint from the boot log line slack vault key fingerprint: … before you rotate the key. A malformed list is ignored, with an error in the log. See After a vault key rotation. |
A fifth secret gates the mint (vending an access token acting as the
mentioning human) and is deliberately not part of the all-or-nothing four, so
an instance that has not set it keeps a fully working link flow and loses only
minting:
| Variable | What it is |
|---|---|
SLACK_MINT_SECRET | Shared HMAC secret gating the mint endpoint. openssl rand -hex 32. Must match the agent's copy. Unset disables minting only. |
These secrets must all hold different values, and Temper refuses to boot if
any two match. The split is the security property:SLACK_LINK_SECRETgates
an endpoint that answers "is this principal linked?",SLACK_MINT_SECRET
gates one that hands back a token carrying that human's entire Temper reach,
andSLACK_VAULT_ENC_KEYdecrypts every stored refresh token. One value across
two of them means whoever holds the cheap capability already holds the
expensive one. Generate each one separately.
Set
SLACK_VAULT_ENC_KEYas part of the deploy that ships the vault, not
after. Because the four link vars are all-or-nothing, deploying vault code
to an instance already running the link flow turns the link flow off until the
key is present.
The agent is its own git-connected Vercel project:
packages/agent-workflows/mentionmain — mergingmain deploys it; any branch push gives a preview. Deploy by git push, nevereve deploy or vercel deploy from the agent directory.Note the deployment host — you need it later. Every request will 401 until
the signing secret is set; that is expected.
This is a two-phase manifest, and the order is not the obvious one. The eve
runtime verifies the request signature first and fails closed: with no
SLACK_SIGNING_SECRETthe route returns401and never reaches Slack's
url_verificationhandshake. But the signing secret only exists once the app
exists. Declaring a request URL up front is a deadlock — Slack will not save a
URL it cannot verify, and the URL cannot verify without a secret that does not
exist yet. The way out: create the app with no event subscriptions, collect the
secret, deploy, then declare the URL.
Go to https://api.slack.com/apps → Create New App → From a manifest,
choose the workspace, and paste the following unmodified. The
event/interactivity block is omitted on purpose — leave it out.
display_information:
name: temper
description: Ask temper about your team's knowledge base, from Slack.
background_color: "#2c2d30"
features:
bot_user:
display_name: temper
always_online: false
oauth_config:
scopes:
bot:
- app_mentions:read
- chat:write
- im:history
- im:write
- channels:history
settings:
socket_mode_enabled: false
org_deploy_enabled: false
token_rotation_enabled: false
With no event subscriptions declared, Slack has nothing to verify and the app is
created cleanly.
xoxb-…).On the mention agent's Vercel project, set:
| Variable | What it is |
|---|---|
SLACK_BOT_TOKEN | The xoxb-… token from the previous step. |
SLACK_SIGNING_SECRET | The signing secret from the previous step (HMAC-verifies inbound webhooks). |
TEMPER_API_URL | The Temper API origin, e.g. https://temper-cloud.vercel.app. |
SLACK_LINK_SECRET | The same value as the Temper API's SLACK_LINK_SECRET. |
SLACK_MINT_SECRET | The same value as the Temper API's SLACK_MINT_SECRET — and a different value from SLACK_LINK_SECRET. |
vercel env add SLACK_BOT_TOKEN production
vercel env add SLACK_SIGNING_SECRET production
vercel env add TEMPER_API_URL production
vercel env add SLACK_LINK_SECRET production
vercel env add SLACK_MINT_SECRET production
Then redeploy (push to main, or redeploy from the dashboard) so the functions
pick them up.
TEMPER_API_URLmust point at the API origin, not the UI origin. The
agent calls/internal/slack/link-state, and the UI proxy does not forward
/internal. Point it at the UI origin and the internal call hits the UI shell
and fails. (The Temper API'sPUBLIC_BASE_URLdoes stay the public UI origin,
because the callback lives under/api, which the UI proxy forwards. The two
are genuinely different origins.)
Now the signing secret is in place, so the handshake will succeed. In the Slack
app's App Manifest editor, add the event/interactivity block below, replacing
<YOUR-DEPLOYMENT-HOST> with the agent host from the Vercel project step, and
save:
event_subscriptions:
request_url: https://<YOUR-DEPLOYMENT-HOST>/eve/v1/slack
bot_events:
- app_mention
- message.im
interactivity:
is_enabled: true
request_url: https://<YOUR-DEPLOYMENT-HOST>/eve/v1/slack
Slack POSTs a url_verification challenge; eve verifies the signature and
answers with the raw challenge on the first try. Adding message.im may prompt
a reinstall to grant im:history — if Slack asks, reinstall and re-copy the
bot token if it changed.
/invite @temper.@temper from an account that has not linked.TEMPER_API_URL andSLACK_LINK_SECRET.@temper again. You should get an answer to the@your-handle, never with another connect link. A second connect link| Symptom | Cause |
|---|---|
| Slack says the request URL didn't verify | SLACK_SIGNING_SECRET unset or wrong, or you declared the URL before the signing secret was deployed. The eve runtime returns 401; Slack reports it as a failed handshake. |
| Bot never responds, no error | The mention was dropped (bot-authored or authorless events are silent by design), or a SLACK_LINK_SECRET mismatch is 401ing every mention. Check the agent function logs. |
| "Sign-in could not be completed" on the callback | The IdP client is confidential; make it a public/PKCE client. |
| "No temper account is linked to this login" | The user has no Temper account yet — the link is lookup-only; sign in at the Temper UI first. |
| "Account linking is not configured" | One of the four Temper API vars is missing or malformed. |
Linked users get "try again in a moment" on every mention; temper-api logs the vault grant was sealed under key …, but SLACK_VAULT_ENC_KEY is … (and at boot, SLACK_VAULT_ENC_KEY (…) sealed none of the … most recently sealed vault rows) | The deployed SLACK_VAULT_ENC_KEY is not the key that sealed the stored grants. The mint answers 500 and leaves every grant untouched. If the key changed by accident, restore the old one and every user works again. If you rotated it on purpose, list the old fingerprint (named in the log line) in SLACK_VAULT_RETIRED_KEY_FINGERPRINTS, and affected users get a reconnect link. Do not ask users to reconnect until you know which case it is: reconnecting overwrites a grant that restoring the key would otherwise heal. |
One user is told to reconnect, and temper-api logs a vault grant sealed under the configured key will not open at error | That user's stored grant fails under the key that sealed it: the row was altered in the database. Reconnecting fixes the user. Investigate how the row changed. |
| Everything green but no logs on failure | The eve runtime swallows handler errors, and serverless logs surface HTTP events, not app stdout — "no error in the logs" proves nothing. Diagnose from observable Slack behavior. |