a little carrier pigeon that ferries figma events to discord
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 201 lines 6.2 kB view raw view rendered
1# pigeon 2 3a little carrier pigeon that ferries Figma `LIBRARY_PUBLISH` events to a Discord channel webhook. Cloudflare Worker. coalesces per-asset-type events into a single Discord embed inside a 60s window via a Durable Object (one DO instance per `file_key`). one retry at +5 min on Discord failure, then drop. 4 5stack: Cloudflare Workers + Durable Objects (SQLite storage + alarms) + Workers KV. no external services, no runtime deps. 6 7## prerequisites 8 9- Cloudflare account. Workers Free is sufficient. 10- Figma team admin access + personal access token with `webhooks:write` scope. 11- Discord channel incoming webhook url. 12- `bun` installed (`https://bun.sh`). swap to `npm`/`pnpm` by editing `package.json` scripts if needed. 13 14## setup 15 16all commands run from the repo root. 17 181. copy config template: 19 20 ``` 21 cp wrangler.toml.example wrangler.toml 22 ``` 23 242. install deps: 25 26 ``` 27 bun install 28 ``` 29 303. authenticate with Cloudflare (one-time): 31 32 ``` 33 bunx wrangler login 34 ``` 35 364. (optional) pin deploys to a specific Cloudflare account: 37 38 ``` 39 export CLOUDFLARE_ACCOUNT_ID=<account-id> # from `bunx wrangler whoami` 40 ``` 41 425. create KV namespace. paste the returned `id` into `wrangler.toml` under `[[kv_namespaces]]`: 43 44 ``` 45 bunx wrangler kv namespace create FIGMA_DISCORD_WEBHOOK 46 ``` 47 486. map your Figma `file_key` to the Discord webhook url: 49 50 ``` 51 bunx wrangler kv key put --binding=FIGMA_DISCORD_WEBHOOK --remote \ 52 "<FIGMA_FILE_KEY>" "<DISCORD_WEBHOOK_URL>" 53 ``` 54 557. (optional) bind a custom domain. uncomment and edit the `[[routes]]` block in `wrangler.toml`: 56 57 ``` 58 [[routes]] 59 pattern = "figma.example.com" 60 custom_domain = true 61 ``` 62 63 the parent zone must already exist on this Cloudflare account. 64 658. deploy: 66 67 ``` 68 bun run deploy 69 ``` 70 71 note the deployed url (either `<name>.<subdomain>.workers.dev` or your custom domain). 72 739. set the passcode secret (random string, used for auth between Figma and Worker): 74 75 ``` 76 openssl rand -hex 32 | bunx wrangler secret put FIGMA_PASSCODE 77 ``` 78 79 save the same value; step 10 uses it. 80 8110. register the Figma webhook: 82 83 ``` 84 curl -X POST \ 85 -H "X-FIGMA-TOKEN: <FIGMA_PAT>" \ 86 -H "Content-Type: application/json" \ 87 "https://api.figma.com/v2/webhooks" \ 88 -d '{ 89 "event_type": "LIBRARY_PUBLISH", 90 "team_id": "<FIGMA_TEAM_ID>", 91 "endpoint": "<WORKER_URL>/figma", 92 "passcode": "<SAME_PASSCODE_AS_STEP_9>", 93 "description": "Design system publish -> Discord" 94 }' 95 ``` 96 97 Figma sends `PING` immediately. Worker returns 200; no Discord message is posted for `PING`. 98 9911. test: publish a library change in Figma. Discord message arrives ~60s later. 100 101## endpoints 102 103- `POST /figma` — Figma webhook receiver. validates `passcode`. accepts `PING` and `LIBRARY_PUBLISH`. other event types return 200 and are ignored. 104- `GET /health` — returns `{"status":"healthy","lastFigmaRequestAt":<iso>|null}` as JSON with 200. `lastFigmaRequestAt` is the ISO timestamp of the most recent authenticated Figma webhook delivery, useful for detecting upstream silence. 105 106all other routes return 404. 107 108## opt-out 109 110include `#exclude` (case-insensitive) anywhere in the Figma publish description. Worker drops the event. 111 112## key / value reference 113 114- `FIGMA_FILE_KEY`: path segment in `figma.com/file/<KEY>/…` or `figma.com/design/<KEY>/…`. 115- `FIGMA_TEAM_ID`: path segment in `figma.com/files/team/<ID>/…`. 116- `CLOUDFLARE_ACCOUNT_ID`: from `bunx wrangler whoami`. 117- `DISCORD_WEBHOOK_URL`: Discord channel → edit channel → integrations → webhooks → new webhook. 118- `FIGMA_PAT`: Figma → settings → security → personal access tokens. scope: `webhooks:write`. 119 120## files 121 122- `src/index.ts` — fetch handler, passcode check, route to Batcher DO. 123- `src/batcher.ts` — Durable Object. merges items by key, schedules 60s alarm, flushes to Discord, one retry on failure. 124- `src/discord.ts` — embed builder + POST. respects Discord field limits (1024 char value, 15 items per field). 125- `src/figma.ts` — payload types, `isExcluded()`, `figmaFileUrl()`, `collectChanged()`. 126- `src/types.ts` — `Env`, `BatchState`, `BatchItems`, `LibraryItem`. 127 128## config knobs 129 130in `src/batcher.ts`: 131 132- `BATCH_WINDOW_MS = 60_000` — coalescing window from first event. 133- `RETRY_DELAY_MS = 300_000` — delay before single retry on Discord failure. 134- `MAX_FLUSH_ATTEMPTS = 2` — total attempts (initial + retry). higher values risk loops. 135 136in `src/discord.ts`: 137 138- `MAX_ITEMS_PER_FIELD = 15` 139- `FIELD_VALUE_MAX = 1024` 140 141## Figma event handling 142 143input: Figma `LIBRARY_PUBLISH` payload. separate events fire per asset type (components, styles, variables) within milliseconds of each other. all three categories are merged into one batch per `file_key` via `env.BATCHER.idFromName(file_key)`. 144 145output: one Discord embed per batch window. fields: components (N), styles (N), variables (N). each field lists up to 15 item names; overflow renders as "+ X more". deleted items are not shown. 146 147## limits 148 149- Figma: 20 webhooks per team. team-context webhooks do not fire for invite-only project files. 150- Discord: 1024 chars per field value, 6000 chars per embed total. enforced in `src/discord.ts`. 151- Workers Free: ample for typical design system publish frequency. 152 153## debugging 154 155live logs: 156 157``` 158bun run tail 159``` 160 161Figma webhook delivery history (last 7 days): 162 163``` 164curl -H "X-FIGMA-TOKEN: <PAT>" \ 165 "https://api.figma.com/v2/webhooks/<WEBHOOK_ID>/requests" 166``` 167 168list webhooks on a team: 169 170``` 171curl -H "X-FIGMA-TOKEN: <PAT>" \ 172 "https://api.figma.com/v2/teams/<TEAM_ID>/webhooks" 173``` 174 175delete a webhook: 176 177``` 178curl -X DELETE -H "X-FIGMA-TOKEN: <PAT>" \ 179 "https://api.figma.com/v2/webhooks/<WEBHOOK_ID>" 180``` 181 182typecheck source: 183 184``` 185bun run typecheck 186``` 187 188local dev (needs a tunnel to receive Figma webhooks): 189 190``` 191cp .dev.vars.example .dev.vars # fill in FIGMA_PASSCODE 192bun run dev 193``` 194 195## acknowledgements 196 197inspired by [`bryanberger/figma-discord-webhook`](https://github.com/bryanberger/figma-discord-webhook). the `#exclude` opt-out convention is borrowed from that project. no code was copied. 198 199## license 200 201MIT. see `LICENSE`.