A Vite + React SPA on Cloudflare Workers that uses block-kitchen to compose Slack messages and post them via slack-hono + slack-web-api-client. Validates every send against slack-block-kit-validator for defense in depth.
- A visual builder UI at
/— drag blocks, edit them in popovers, preview them in real time. - Bot OAuth at
/slack/install→/slack/oauth_redirectfor installing the app into a workspace (bot token stored in KV). - User-token OAuth at
/slack/user-install→/slack/user-oauth-redirectfor "send as me" support (user token stored in a separate KV). - Four Worker API routes the SPA talks to:
GET /api/slack/channelsGET /api/slack/emojis(workspace custom emoji viaemoji.list, normalized to{ name, url, alias }[])GET /api/slack/me/can-send-as-userPOST /api/slack/messages/send(validates blocks, picks bot vs user token, callschat.postMessage)
- A
/slack/eventsingress wired throughslack-hono— empty by default, ready for you to add slash commands, events, and actions.
You'll need a Slack workspace where you can install apps. Options:
- A workspace where you have admin rights
- A free Slack workspace (create one)
- The Slack Developer Program sandbox
- Node.js ≥ 20
- pnpm
- cloudflared for the dev tunnel
- A Cloudflare account (free)
pnpm installpnpm run setup:kvFour namespaces are created:
SLACK_INSTALLATIONS— bot tokens, keyed by team IDSLACK_USER_INSTALLATIONS— user tokens, keyed byteam_id:user_idSLACK_OAUTH_STATE— short-lived OAuth state tokensSLACK_MODAL_VIEWS— composed modal view payloads (7-day TTL), keyed by short ID
Paste the IDs from the output into wrangler.jsonc (four placeholders).
pnpm run dev:tunnelCopy the tunnel URL from the output (e.g. https://xxx-yyy-zzz.trycloudflare.com).
Quick tunnels generate a fresh URL every restart. For a fixed URL during dev, run
pnpm run setup:tunnel <hostname>once (requires a domain on Cloudflare).
pnpm run setup:manifest https://xxx-yyy-zzz.trycloudflare.comUpdates manifest.json with your tunnel URL, copies it to your clipboard, and opens api.slack.com/apps/new. Choose From an app manifest, paste, Next → Create.
Grab Signing Secret, Client ID, Client Secret from your app's Basic Information page.
cp .dev.vars.example .dev.vars
# fill in the valuesRestart the dev server to pick up the new secrets.
pnpm run install-appOpens /slack/install on your tunnel. After OAuth completes, the app sets a workspace-identity cookie and redirects you to /, where the builder is ready to use.
Drag a header + section block into the canvas, click Send, pick a channel, hit confirm — your first Block Kit message lands in Slack.
Stuck? Run
pnpm run setup:doctorfor a preflight that audits KV ids,.dev.vars, manifest URL substitution, interactivity, and bot scopes, and prints a punch list of what still needs doing.
The builder's send dialog has a Send as me toggle. The first time you flip it on, the SPA detects you don't have a user token yet and shows a Sign in with Slack link — that takes you through a second OAuth round-trip (/slack/user-install) that issues a user token with chat:write,im:write scopes. After that round-trip, future sends with the toggle on are posted as you instead of the bot.
User tokens live in their own KV (SLACK_USER_INSTALLATIONS) keyed by team_id:user_id. Revoking happens by deleting the row.
The builder's emoji picker and live preview resolve your workspace's custom emoji. On mount the SPA calls GET /api/slack/emojis, which proxies Slack's emoji.list (using the bot token) and normalizes the response into the shape block-kitchen's customEmojis prop expects:
type CustomEmoji = {
name: string; // codename between colons — `:partyparrot:` → "partyparrot"
url: string | null; // hosted image for a real custom emoji, null for an alias
alias: string | null; // target codename when this entry aliases another, else null
};Slack returns a { name: value } map where value is either an image URL or alias:<target>; the worker splits that into the url / alias fields. The SPA hands the resulting array straight to <BlockKitchen customEmojis={…} /> (see src/client/App.tsx) — a plain prop, resolved up front, rather than a lazy loader like loadChannels / loadSendAsUserStatus / onSend. With it set, the picker gains a Custom category and the preview renders :custom: directives as the workspace image (aliases fall back to their target).
The prop is optional and preview/picker-only — custom-emoji fields are never serialized into the emitted Block Kit JSON, and the builder works unchanged when the array is empty (e.g. before the app is installed). Sourcing emoji live from emoji.list needs the bot emoji:read scope, already declared in manifest.json and requested via SLACK_BOT_SCOPES in wrangler.jsonc. If you installed the app before this change, re-push the manifest and rerun pnpm run install-app so the new scope is granted.
pnpm run deployNote your Worker URL from the output.
Replace YOUR_WORKER_URL in manifest.json (or run pnpm run setup:manifest https://<your-worker-url>), then push the changes:
- https://api.slack.com/apps → your app → App Manifest
- Paste
manifest.json→ Save Changes
pnpm run setup:secretspnpm run logs.github/workflows/deploy.yml auto-deploys on push to main. Set CLOUDFLARE_API_TOKEN in GitHub Secrets (create the token via dash.cloudflare.com/profile/api-tokens with the "Edit Cloudflare Workers" template).
┌─────────────────────────────┐
│ block-kitchen │ React component, all UX
│ (npm package) │
└──────────────┬──────────────┘
│ loadChannels / loadSendAsUserStatus / onSend
▼
┌───────────────┐ fetch ┌─────────────────────┐ chat.postMessage
│ React SPA │ ◀──────────▶ │ Cloudflare Worker │ ──────────────────────▶ Slack API
│ (Vite build) │ │ (Hono + slack-hono) │
└───────────────┘ └─────┬───────────────┘
│
▼
┌──────────────┐
│ KV stores │ bot tokens, user tokens, OAuth state
└──────────────┘
The package never makes Slack API calls — the Worker does. The Worker validates every send against slack-block-kit-validator before it goes out. The SPA carries workspace identity in a cookie set during OAuth.
src/
client/ — Vite React SPA (single page, mounts <BlockKitchen>)
main.tsx
App.tsx
styles.css
worker/ — Cloudflare Worker entry + OAuth helpers
index.ts — Hono app: /slack/install, /slack/*, /api/slack/*, asset fallback
oauth.ts — bot + user OAuth flow (state, code exchange)
cookies.ts — minimal cookie helpers
scripts/ — setup helpers (manifest, secrets, tunnel, install)
manifest.json — Slack app manifest
wrangler.jsonc — Cloudflare Workers config (KV bindings, asset binding)
.dev.vars.example — local secret template
.github/workflows/
deploy.yml — auto-deploy on push to main
MIT. See LICENSE.
Built with block-kitchen, slack-hono, and slack-block-kit-validator. Maintained by the Tightknit team.