Skip to main content
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.
asymmetric spin slack
Spinning 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

PieceNotes
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

ModeServicesUse it for
api (default)backendAgents that talk to the Web API or MCP server.
fullbackend + frontendWhen you also want the UI.
asymmetric spin slack --mode full

Seeding

asymmetric spin slack --seed acme-corp     # deterministic fixture on create
asymmetric seed slack-a1b2 --ai            # realistic data via the Web API
AI 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:
asymmetric tokens slack-a1b2            # print the bot + user tokens
asymmetric tokens slack-a1b2 --json     # machine-readable
asymmetric 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"]}}}}'
App-token scopes are enforced. Each Web API method declares the Slack scope it needs (chat:write, reactions:write, channels:history, search:read, …); a token missing it gets 403 { "ok": false, "error": "missing_scope" }. JWT / web-UI humans are exempt. OAuth install and auth.revoke are not implemented yet. Clones bind to localhost by default, so a token is not reachable off your machine unless you --expose.

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 routesPOST /api/chat.postMessage, conversations.*, reactions.*, users.*, search.messages, auth.test — args in the JSON body (channel, ts, timestamp, users, name, …), exactly how @slack/web-api calls them.
  • The { ok } envelope — success is { "ok": true, … }; failures are HTTP 200 { "ok": false, "error": "<code>" }, with cursor pagination under response_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_callback envelope 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:
FamilyMethods
chatpostMessage, update, delete
conversationslist, create, info, history, replies, members, join, leave, archive, invite, kick, rename, setTopic, setPurpose, open
reactionsadd, remove
userslist, info, profile.get, profile.set, setPresence
searchmessages
authtest
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:
TierAllowanceExample methods
Tier 1~1 / min(heaviest admin-style methods)
Tier 2~20 / minreactions.remove, users.setPresence
Tier 3~50 / minconversations.members
Tier 4~100 / minmost reads; auth.test (a high allowance)
Special~1 / s / channelchat.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/provision

MCP 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.
The verify command will fold this kind of live fidelity scoring into the CLI. Until then, API_PARITY.md and the /verify-api slack workflow are how fidelity is tracked.

Inspect a running clone

asymmetric query slack-a1b2 "select id, name from channels"
asymmetric db slack-a1b2          # prints a psql shell command
asymmetric logs slack-a1b2 -f     # stream the backend
The schema (channels, messages, users, workspaces, apps, …) comes straight from 001_initial_schema.sql — read it to know what’s queryable.