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). - Three Worker API routes the SPA talks to:
GET /api/slack/channelsGET /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.
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.