Slack template
A real Slack-style backend, frontend, and MCP server — RPC Web API, the {ok} envelope, Slack-format ids, and byte-identical events.
The Slack template (apps/slack-clone, name slack) is the reference clone: a
NestJS backend on Postgres, an optional React frontend, and an MCP server. Its
data plane is the real Slack Web API — RPC method names (chat.postMessage),
args in the request body, the { "ok": true, … } envelope, Slack-format object
ids (T…/U…/C…/D…/G…/A…), per-method rate-limit tiers, and byte-identical Events
API webhooks. Auth works two ways: a JWT for the web UI, and Slack-style opaque
bearer tokens (xoxb-… / xoxp-…) for programmatic access. It's faithful
enough at the wire-format level that a client built for @slack/web-api can
drive it.
hone env spin slackSpinning a clone auto-provisions a default app and prints a bot token and a user token you can use immediately — see Apps and tokens.
What's inside
| Piece | Notes |
|---|---|
backend/ | NestJS Slack Web API (RPC) + Socket.io realtime, Postgres via pg. Global prefix /api, health at /api/health, Swagger at /api/docs. |
frontend/ | React + Vite UI. Runs only in full mode. |
mcp-server/ | MCP server exposing the eight reference Slack MCP tools (+ labeled extensions). |
supabase/migrations/ | 001_initial_schema.sql + 002_apps_and_tokens.sql — the schema (incl. gen_slack_id()), run on spin and replayed on reset. |
seeds/ | acme-corp.sql — the bundled deterministic fixture. |
Modes
| Mode | Services | Use it for |
|---|---|---|
api (default) | backend | Agents that talk to the Web API or MCP server. |
full | backend + frontend | When you also want the UI. |
hone env spin slack --mode fullSeeding
hone env spin slack --seed acme-corp # deterministic fixture on create
hone env seed slack-a1b2 --ai # realistic data via the Web APIAI seeding generates users, channels, and messages (the template's declared
entities) and creates them through real signup / conversations.create /
chat.postMessage calls. See Seeding.
Apps and tokens
Real Slack apps authenticate with bearer tokens (xoxb- for bots, xoxp- for
users) and are configured by an app manifest. The clone mirrors that: it has a
first-class app entity with a stored manifest, opaque bearer tokens, and
manifest scopes that are enforced on every app-token call.
Tokens you get for free
Every spin slack auto-provisions a default workspace, an admin user, and a
default app, then prints a bot token and a user token:
✓ slack-a1b2 (api) http://127.0.0.1:3001/api ready in 38.2s
db clone_slack_a1b2 · env -
bot token xoxb-…
user token xoxp-…Use either as a bearer token against the Web API — no signup/login step. The
identity check is auth.test (the real Slack method), POSTed with the token:
curl -s -X POST http://127.0.0.1:3001/api/auth.test \
-H "authorization: Bearer xoxb-…"
# → { "ok": true, "user_id": "U…", "team_id": "T…", "user": "…", … }A bot token acts as the app's bot user (messages it posts are attributed to
the bot, is_bot: true); a user token acts as the admin user. This is the
fastest way to point an agent at a clone — see
Connect your agent.
Get the tokens again later
Tokens are stored locally so you can reprint them any time:
hone env tokens slack-a1b2 # print the bot + user tokens
hone env tokens slack-a1b2 --json # machine-readable
hone env tokens slack-a1b2 --reprovision # rotate (old tokens stop working)See the tokens reference.
Create your own app
Post a Slack-shaped manifest to create additional apps (each gets its own bot
user and bot token). App management is a clone-operation, so it uses a plain REST
shape, not the { ok } envelope:
curl -s -X POST http://127.0.0.1:3001/api/apps \
-H "authorization: Bearer xoxp-…" \
-H 'content-type: application/json' \
-d '{"manifest":{"display_information":{"name":"My App"},"oauth_config":{"scopes":{"bot":["chat:write","channels:read"]}}}}'API shape — read this before pointing an agent at it
The data plane is Slack's RPC Web API, so it matches at the wire-format level, not just the capability level:
- RPC method routes —
POST /api/chat.postMessage,conversations.*,reactions.*,users.*,search.messages,auth.test— args in the JSON body (channel,ts,timestamp,users,name, …), exactly how@slack/web-apicalls them. - The
{ ok }envelope — success is{ "ok": true, … }; failures are HTTP 200{ "ok": false, "error": "<code>" }, with cursor pagination underresponse_metadata.next_cursor. - Slack-format ids — every object id is minted in Slack's grammar at the DB
layer (
T…team,U…user,C…channel,D…IM,G…mpim,A…app). No UUIDs leak into payloads. - Rate-limit tiers — each method enforces its Slack tier; a breach returns
HTTP 429+Retry-After+{ "ok": false, "error": "ratelimited" }(see below). - Byte-identical events — outgoing webhooks deliver the full Slack Events API
event_callbackenvelope with inner objects matching Slack field-for-field (message,reaction_added/removed,channel_created,channel_archive,member_joined/left_channel,user_change,presence_change).
A quick tour against a running clone:
B=http://127.0.0.1:3001/api
A="authorization: Bearer xoxb-…"
# Post a message (channel id is a Slack-format C…)
curl -s -X POST $B/chat.postMessage -H "$A" -H 'content-type: application/json' \
-d '{"channel":"C…","text":"hello from an agent"}'
# List conversations, then read history
curl -s -X POST $B/conversations.list -H "$A" -d '{"types":"public_channel"}'
curl -s -X POST $B/conversations.history -H "$A" -d '{"channel":"C…","limit":20}'
# React to a message by channel + ts (Slack's keying, not a UUID)
curl -s -X POST $B/reactions.add -H "$A" \
-d '{"channel":"C…","timestamp":"1718900000.000100","name":"thumbsup"}'
# Full-text search across visible channels
curl -s -X POST $B/search.messages -H "$A" -d '{"query":"hello"}'Implemented method surface
The data plane implements 27 Slack methods across messaging, conversations, reactions, users, search, and auth:
| Family | Methods |
|---|---|
chat | postMessage, update, delete |
conversations | list, create, info, history, replies, members, join, leave, archive, invite, kick, rename, setTopic, setPurpose, open |
reactions | add, remove |
users | list, info, profile.get, profile.set, setPresence |
search | messages |
auth | test |
Whole Slack families outside this core (files.*, pins.*, bookmarks.*,
dnd.*, views.*, usergroups.*, admin.*, …) are unimplemented by design —
this is an MVP+Beta vertical slice, not the full ~130-method Web API.
Rate-limit tiers
Each method is bound to its real Slack tier and returns 429 + Retry-After +
{ "ok": false, "error": "ratelimited" } on breach:
| Tier | Allowance | Example methods |
|---|---|---|
| Tier 1 | ~1 / min | (heaviest admin-style methods) |
| Tier 2 | ~20 / min | reactions.remove, users.setPresence |
| Tier 3 | ~50 / min | conversations.members |
| Tier 4 | ~100 / min | most reads; auth.test (a high allowance) |
| Special | ~1 / s / channel | chat.postMessage |
Control plane (clone-operations — intentionally REST)
Operations that real Slack configures via app manifests / OAuth rather than
runtime Web API methods keep plain REST shapes and are not wrapped in the
{ ok } envelope:
Auth POST /api/auth/{signup,login,refresh,logout} GET /api/auth/me
Workspaces GET/POST /api/workspaces POST /api/workspaces/:id/join GET/PUT/DELETE /api/workspaces/:id
Webhooks POST /api/webhooks/incoming/:id GET/POST /api/webhooks/{incoming,outgoing} DELETE …/:id
Events POST /api/events/subscribe GET /api/events/subscriptions DELETE /api/events/subscriptions/:id
Commands GET/POST /api/commands DELETE /api/commands/:id POST /api/commands/execute
Interactions POST /api/interactions Apps POST/GET /api/apps Admin POST /api/admin/provisionMCP server
The mcp-server exposes the eight reference Slack MCP tools with their exact
names and argument names — slack_list_channels, slack_post_message,
slack_reply_to_thread, slack_add_reaction, slack_get_channel_history,
slack_get_thread_replies, slack_get_users, slack_get_user_profile — with
tool descriptions restored character-for-character to the reference server
(modelcontextprotocol/servers-archived),
plus a few clearly-labeled slack_* extensions (slack_auth_test,
slack_search_messages, slack_open_dm). It binds to one SLACK_MCP_TOKEN
(xoxb/xoxp), resolves it to identity + workspace + scopes, and acts strictly
as that identity — gating each tool on its scope and scoping every query to the
token's workspace.
The template ships an API_PARITY.md scored by the strict /verify-api slack
workflow, which fetches Slack's live docs every run and treats identifier,
envelope, id-format, rate-limit tier, event-schema, and MCP-verbatim conformance
as blocking checks. Check apps/slack-clone/API_PARITY.md for the per-method
breakdown.
Inspect a running clone
hone env query slack-a1b2 "select id, name from channels"
hone env db slack-a1b2 # prints a psql shell command
hone env logs slack-a1b2 -f # stream the backendThe schema (channels, messages, users, workspaces, apps, …) comes straight from
001_initial_schema.sql — read it to know what's queryable.