Secrets and Config
Setting SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET as Worker secrets, never as vars or client-bundled values.
Overview
A Slack integration needs exactly two confidential values: the bot token (SLACK_BOT_TOKEN, xoxb-…) used to call the Web API, and the signing secret (SLACK_SIGNING_SECRET) used to verify inbound requests (see Verifying Requests). Both belong to the Worker as secrets, set with wrangler secret put — never as vars in wrangler.toml, and never referenced anywhere a bundler could pull them into client-shipped JS.
wrangler secret put
npx wrangler secret put SLACK_BOT_TOKEN
npx wrangler secret put SLACK_SIGNING_SECRETEach command prompts for the value and pushes it straight to the deployed Worker; it is never written to wrangler.toml or any file in the repo. Per Cloudflare's secrets docs, wrangler secret put creates a new Worker version and deploys it immediately — for a gradual/staged rollout, wrangler versions secret put creates the version without deploying it.
Never put a Slack token or signing secret in vars
wrangler.toml's [vars] block is plaintext, checked into the repo, and visible in the Cloudflare dashboard. Cloudflare's own guidance is explicit:"Do not use vars to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead."SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET are exactly the kind of value that rule is for.
Local dev: .dev.vars
wrangler dev reads local-only secrets from a .dev.vars file next to wrangler.toml, using dotenv formatting:
SLACK_BOT_TOKEN="xoxb-your-dev-workspace-token"
SLACK_SIGNING_SECRET="your-dev-workspace-signing-secret"# .gitignore
.dev.vars*.dev.vars (and .env, if used) must never be committed — the same file that makes local dev convenient is a plaintext secret if it lands in git history. Cloudflare also supports .dev.vars.<environment-name> for per-environment local overrides, which load before the generic .dev.vars; the glob above covers those variants too — a bare .dev.vars entry does not.
Typed Env
Which path generates a correct, typed Env depends on the installed Wrangler version.
Preferred: declare required secrets in config, then wrangler types
Wrangler's secrets configuration property lets you declare the names of required secrets — never values — directly in wrangler.toml:
[secrets]
required = [ "SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET" ]With this in place, wrangler types generates typed bindings from secrets.required instead of inferring names from .dev.vars — so type generation works in CI or anywhere .dev.vars isn't present — and writes them into the generated worker-configuration.d.ts. Treat that generated file as the single source of truth for Env once this is set up; don't also hand-write the interface below, or the two can drift. secrets adds runtime validation too: wrangler dev warns if a required secret is missing from .dev.vars, and wrangler deploy / wrangler versions
upload refuse to ship if a required secret isn't actually set on the Worker. (Source: Cloudflare's Wrangler configuration reference and the secrets-config-property changelog entry.)
A related but separate flag is worth knowing about here: wrangler types
--strict-vars (default true) generates literal/union types for vars values — pass --strict-vars=false if a vars value legitimately differs across environments and the literal-union type gets in the way. It governs vars typing only and has no effect on secrets.required.
Fallback: hand-written interface (older Wrangler)
Without the secrets config property, secrets are never declared anywhere Wrangler can see (only vars and bindings are) — hand-write the Env fields instead so every handler still gets compile-time checking instead of stringly-typed env["SLACK_BOT_TOKEN"] lookups:
export interface Env {
SLACK_BOT_TOKEN: string;
SLACK_SIGNING_SECRET: string;
// ... other bindings: KV, D1, etc.
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// env.SLACK_BOT_TOKEN and env.SLACK_SIGNING_SECRET are typed strings here
return await handleRequest(request, env, ctx);
},
} satisfies ExportedHandler<Env>;What a real app ends up needing
SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET cover the minimum for calling the Web API and verifying inbound requests, but a production integration's Env surface grows well past those two:
Channel IDs. A source channel to read from, a destination channel to post to, and any others the integration is scoped to. These aren't secrets, but they're per-workspace and belong in config, not hardcoded.
The bot's own user ID (
SLACK_BOT_USER_ID). Without it, an Events API handler that reacts to activity in a channel the bot also posts to can loop on its own output — it sees its own message or reaction event and reacts to that. Filtering on the bot's user ID (orbot_id) needs that ID available as config, not hardcoded or looked up on every request.
Vars are strings — a boolean-ish flag needs an explicit comparison
Every vars value arrives as a string, whatever it looks like inwrangler.toml. A flag meant to gate a feature must be compared against the literal string — env.FEATURE_FLAG === "true" — not a truthiness check."false" is a non-empty string, so it's truthy: a truthiness check silently re-enables exactly the behavior the flag was supposed to gate off.
Precedence for local overrides. A wrangler dev --var NAME:value CLI override beats .dev.vars, which beats the config file's default vars value. This lets local development opt into a fallback value a deployed Worker structurally can't reach — there's no --var flag at deploy time — useful as a local-only escape hatch, but worth knowing so a --var reached for during one debugging session doesn't quietly become "how this always gets run locally."
Fail closed on a missing secret. A handler that reads env.SLACK_BOT_TOKEN and gets undefined because someone forgot wrangler secret put should return an explanatory 5xx that names the missing secret and the exact command to fix it (wrangler secret put
SLACK_BOT_TOKEN), not silently no-op or fall back to some default token. A committed default is acceptable only when it's gated behind an explicit, local-dev-only var — never as an unconditional fallback that could activate in production.
Rotating a leaked or expiring token
Re-running wrangler secret put SLACK_BOT_TOKEN overwrites the old value and deploys immediately — there is no separate "revoke" step on the Worker side. Rotate the token in Slack's app management console first (or immediately after, if the old one is confirmed leaked), then push the new value with the same command.
Preview deployments share every production binding
Unless CI explicitly deploys preview builds to a separate Worker environment with its own bindings, an un-promoted preview version of the same Worker shares every production binding — not just secrets, but D1, KV, and the real Slack channels the bot token points at. A preview-invoked endpoint has production side effects: in a production reference integration, a preview build's admin endpoint once delivered messages to the production channel, because the preview Worker was calling the Web API with the production SLACK_BOT_TOKEN against the production channel ID — nothing about "preview" scoped either one.
Because the preview shares the production D1 database too, schema changes must stay additive-only: dropping or renaming a column production still reads can break the moment a preview version — which may be running slightly older or newer code — touches the same table. See Preview Deploys and D1 Migrations for the full additive-only rule, the CI guard that enforces it, and the escape-hatch label for the rare migration that has to be destructive.
Don't assume a PR preview is a sandboxed workspace unless a separate environment with its own secrets and bindings was actually set up for it.
Gotchas
varsand secrets look identical in code —env.SLACK_BOT_TOKENreads the same either way. The difference is entirely in how the value got there; get the declaration right, because the code won't warn you..dev.varsvalues never reach production — they exist only forwrangler dev. Forgetting to also runwrangler secret putafter adding a new secret to.dev.varsis a common "works locally, 500s in prod" gap.A secret set via the dashboard and one set via CLI both work identically — but only one should be treated as the source of truth per project, or the two can silently drift out of sync across environments.
wrangler secret putdeploys immediately; avarsedit needs a redeploy. Pushing a new secret creates a new Worker version and activates it right away, but changing avarsvalue inwrangler.tomlonly takes effect on the nextwrangler deploy— a common source of "I updated the var but nothing changed" confusion when the two are conflated.