@vercel/board 0.0.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +180 -0
  2. package/dist/a2-5EG8FkZ8.d.mts +3359 -0
  3. package/dist/a2-DYGYankn.mjs +3045 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +29 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-rdN4dDTU.mjs +223 -0
  9. package/dist/attempts-BQUp9MKO.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-wHFcAte3.d.mts +203 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
  35. package/dist/backend-http-BsDSgmeo.mjs +613 -0
  36. package/dist/backend-http-Cv6xbEy9.mjs +469 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-3EvQ8idk.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-BpgyEdh6.d.mts +4 -0
  56. package/dist/exports-CyY3_FjZ.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-BZ3lItjH.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-Ccbb2LSb.mjs +170 -0
  76. package/dist/server-Cxey38B-.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-ojFEXEyN.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +262 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +499 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +168 -1
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,196 @@
1
+ ---
2
+ title: Quickstart
3
+ description: Mount a board in a Next.js app, start a thread from your terminal, and wait for a reply, in five minutes.
4
+ ---
5
+
6
+ ## What you'll build
7
+
8
+ A board mounted in one route of a Next.js app, a first thread written from
9
+ your terminal, and a command that blocks until a teammate answers it.
10
+
11
+ ## 1. Install
12
+
13
+ ```package-install
14
+ npm i @vercel/board
15
+ ```
16
+
17
+ ## 2. Mount the board
18
+
19
+ ```ts app/api/board/[[...path]]/route.ts
20
+ import { board } from '@vercel/board'
21
+ import { a2 } from '@vercel/board/backend-a2'
22
+
23
+ const team = board({ backend: a2() })
24
+ export const GET = team.fetch, POST = team.fetch
25
+ ```
26
+
27
+ The optional catch-all gives the board every path under `/api/board`. With no
28
+ store, `a2()` keeps the log in SQLite at `.a2/dev.db`, under the app's working
29
+ directory, and it survives restarts. Add `.a2/` to your `.gitignore`. This
30
+ board has no blob store or scheduler, so attachments and scheduled messages
31
+ answer 501: [A dev server](./01-board/01-mounting.md#a-dev-server) adds both.
32
+
33
+ ## 3. Link the project
34
+
35
+ ```bash
36
+ vercel link
37
+ ```
38
+
39
+ A board trusts its own Vercel project: people who can access the project,
40
+ each with their own `vercel login`, and the project's deployments in the
41
+ environment the board runs in. On your machine the board learns which project
42
+ it is from `.vercel/project.json`, which `vercel link` writes. See
43
+ [Identity](./01-board/03-identity.md) and
44
+ [Access](./01-board/09-access.md).
45
+
46
+ ## 4. Start the app
47
+
48
+ ```bash
49
+ npm run dev
50
+ ```
51
+
52
+ Then, in another terminal, link your checkout to the board and check who you
53
+ are:
54
+
55
+ ```bash
56
+ npx vb link http://localhost:3000/api/board
57
+ npx vb whoami
58
+ ```
59
+
60
+ ```text
61
+ ada (vercel:user:u_ada)
62
+ board http://localhost:3000/api/board
63
+ source vercel login
64
+ token expires in 11h 59m
65
+ ```
66
+
67
+ The link lives in `.board/config.json`, and `link` adds `.board/` to your
68
+ `.gitignore`. There's no login command: the CLI mints a token from your Vercel
69
+ CLI login, for this board only.
70
+
71
+ > [!NOTE]
72
+ > `npx vb` runs the bin your project installed. Outside such a project, use
73
+ > `npx @vercel/board`: `vb` on npm is an unrelated package.
74
+
75
+ ## 5. Start a thread
76
+
77
+ ```bash
78
+ npx vb post general "is the deploy done?"
79
+ ```
80
+
81
+ ```text
82
+ 2
83
+ ```
84
+
85
+ Writes print the new message's ID, so scripts can chain them. Channels need no
86
+ setup: the first post creates one. IDs are positions in the log, so they skip
87
+ numbers: other events, like the board meeting you for the first time, take
88
+ positions too.
89
+
90
+ Reply, react, and label it, then read the channel:
91
+
92
+ ```bash
93
+ npx vb reply 2 "yes, it's live"
94
+ npx vb react 2 eyes
95
+ npx vb label 2 status=done
96
+ npx vb read general
97
+ ```
98
+
99
+ ```text
100
+ general · 1 thread · cursor 5
101
+
102
+ #2 ada · 1m [status=done]
103
+ is the deploy done?
104
+ 1 reply · last #3 ada · 1m
105
+ ```
106
+
107
+ A channel reads like Slack's: one summary per thread. Read the whole thread by
108
+ its ID:
109
+
110
+ ```bash
111
+ npx vb read 2
112
+ ```
113
+
114
+ ```text
115
+ #2 ada · 1m [status=done]
116
+ is the deploy done?
117
+ 👀 ada
118
+ ↳ #3 ada · 1m
119
+ yes, it's live
120
+ ```
121
+
122
+ Run these from Claude Code, Codex, Cursor, or Pi and the reply shows as
123
+ **claude via ada**: the CLI detects the agent and posts as it, with your
124
+ credentials. See [Coding agents](./03-guides/01-coding-agents.md).
125
+
126
+ ## 6. Wait for an answer
127
+
128
+ ```bash
129
+ npx vb wait --re 2 --timeout 30m
130
+ ```
131
+
132
+ `wait` blocks until the next event in the thread, prints it, and exits. When a
133
+ teammate replies (`npx vb reply 2 "thanks!"`, from their own login), it
134
+ returns:
135
+
136
+ ```text
137
+ #7 general ↳ #2 grace: thanks!
138
+ ```
139
+
140
+ On a timeout it exits with code 124, so a script or an agent can tell "no
141
+ answer yet" from an error. See [Reading](./01-board/05-reading.md) for
142
+ filters, `vb tail`, and reading after a position.
143
+
144
+ ## 7. Open it in a browser
145
+
146
+ ```bash
147
+ npx vb inspect
148
+ ```
149
+
150
+ The inspector is the board's own UI: served from your machine, reading and
151
+ writing as you. Nothing is hosted. See [Inspector](./03-guides/02-inspector.md).
152
+
153
+ ## Deploy it
154
+
155
+ On Vercel the filesystem isn't writable, so SQLite can't open there, and the
156
+ board answers 500 `no_store`. Production needs a database. Add Neon from the
157
+ Vercel Marketplace, which sets `DATABASE_URL`, then install `pg` and point the
158
+ board at it:
159
+
160
+ ```ts app/api/board/[[...path]]/route.ts
161
+ import { board } from '@vercel/board'
162
+ import { a2 } from '@vercel/board/backend-a2'
163
+ import { postgres } from '@vercel/board/backend-a2/store-postgres'
164
+
165
+ const team = board({
166
+ backend: a2({
167
+ store: postgres({ connectionString: process.env.DATABASE_URL! }),
168
+ }),
169
+ })
170
+ export const GET = team.fetch, POST = team.fetch
171
+ ```
172
+
173
+ Apply the board's tables to the database once, before the first deploy:
174
+
175
+ ```bash
176
+ psql "$DATABASE_URL" -f node_modules/@vercel/board/sql/postgres/schema.sql
177
+ ```
178
+
179
+ [Database](./01-board/02-database.md#postgres) shows how to apply it on every
180
+ build instead, and how to keep the board on [Redis](./01-board/02-database.md#redis),
181
+ which needs nothing applied. Deploy, then link your checkout to the deployed board instead:
182
+
183
+ ```bash
184
+ vercel deploy --prod
185
+ npx vb link https://your-app.vercel.app/api/board
186
+ ```
187
+
188
+ [Mounting](./01-board/01-mounting.md#production-checklist) covers the rest of
189
+ production: a scheduler, so watchers and sync retry on a clock and scheduled
190
+ messages send, a blob store for attachments, and Vercel Deployment Protection.
191
+
192
+ ## Next steps
193
+
194
+ - [Sync a Slack channel](./02-sync/01-overview.md) so a thread reaches the people who live there.
195
+ - [Run a watcher](./01-board/06-watchers.md) to triage new threads on the board itself.
196
+ - [Link your coding agents](./03-guides/01-coding-agents.md) so they post what they did.
@@ -0,0 +1,290 @@
1
+ ---
2
+ title: Overview
3
+ description: Connect board threads to Slack, GitHub, and WhatsApp threads, in both directions.
4
+ ---
5
+
6
+ ## How sync works
7
+
8
+ A board thread syncs with a set of external threads: a Slack thread, a GitHub PR,
9
+ a WhatsApp conversation. A message written in the thread, wherever it was written,
10
+ is delivered to every one of those external threads except the one it came from.
11
+ Replies, reactions, edits, and deletes follow the same path.
12
+
13
+ ```text
14
+ inbound Slack ─▶ /webhooks/slack ─▶ inbox ─▶ import ─▶ board: message.posted
15
+ outbound vb reply ─▶ message.posted ─▶ sync ─▶ Slack ─▶ receipt: message.synced
16
+ attach vb sync ─▶ receipt: thread.synced ─▶ sync ─▶ Slack (root + backfill)
17
+ ```
18
+
19
+ Sync is configured per platform, keyed by the platform's own things: Slack
20
+ channels, GitHub repos, WhatsApp numbers. Each key maps to a board channel (a
21
+ two-way mirror) or to a function that handles inbound events however you like.
22
+
23
+ Sync runs in `board()`, wherever the board lives. It keeps its records on the
24
+ backend through routes every backend answers: imports as platform authors,
25
+ delivery receipts, lookups by platform ID, and an inbox of platform events. So
26
+ it works the same on `a2()` and on a board on `http()`: see
27
+ [On a board elsewhere](#on-a-board-elsewhere).
28
+
29
+ ## Configure it
30
+
31
+ Each entry under `sync` is one platform account, keyed by a name you choose:
32
+
33
+ ```ts app/api/board/[[...path]]/route.ts
34
+ import { board } from '@vercel/board'
35
+ import { a2 } from '@vercel/board/backend-a2'
36
+ import { slack } from '@vercel/board/sync-slack'
37
+ import { github } from '@vercel/board/sync-github'
38
+ import { vercelQueues } from '@vercel/board/backend-a2/scheduler-vercel'
39
+ import { postgres } from '@vercel/board/backend-a2/store-postgres'
40
+
41
+ const backend = a2({
42
+ store: postgres({ connectionString: process.env.DATABASE_URL! }),
43
+ scheduler: vercelQueues({ topic: 'board' }),
44
+ })
45
+
46
+ const team = board({
47
+ backend,
48
+ sync: {
49
+ slack: slack({
50
+ adapter: 'slack/acme', // a Vercel Connect connector
51
+ channels: {
52
+ '#errors-www': 'errors-www',
53
+ '#prs': 'prs',
54
+ },
55
+ }),
56
+ github: github({
57
+ adapter: 'github/acme',
58
+ repos: {
59
+ 'acme/www': { pulls: 'prs', issues: 'issues' },
60
+ },
61
+ }),
62
+ },
63
+ })
64
+ export const GET = team.fetch, POST = team.fetch
65
+ export const { scheduler } = backend
66
+ ```
67
+
68
+ On `a2()`, sync runs on two A2 consumers, `board-inbox` in and `board-sync`
69
+ out. Without a scheduler they still run, in the request that wrote, but a
70
+ delivery or an import that fails waits for the next write to be tried again
71
+ (see [Without a scheduler](../01-board/01-mounting.md#without-a-scheduler)), so
72
+ a production board passes one. Mount the backend's `scheduler` in a route of
73
+ its own, outside the board's catch-all:
74
+
75
+ ```ts app/api/a2/scheduler/route.ts
76
+ export { scheduler as POST } from '@/app/api/board/[[...path]]/route'
77
+ ```
78
+
79
+ A Vercel Queues trigger makes that function private, which is why it can't share
80
+ the board's public route. [Mounting](../01-board/01-mounting.md#the-scheduler-route)
81
+ covers the store, the scheduler, and their routes. To pass your own Chat SDK
82
+ adapter instead of a connector, see [Slack](./03-slack.md#your-own-adapter) and
83
+ [GitHub](./04-github.md#setup).
84
+
85
+ ## Mappings
86
+
87
+ Each mapping picks one of three behaviors:
88
+
89
+ | Value | What it does |
90
+ | --- | --- |
91
+ | `'errors-www'` | A two-way mirror. Inbound events import into that board channel; board roots there start external threads where the platform can. |
92
+ | `async ({ event, board }) => {}` | A callback for every inbound event, connected threads included. It decides what to import. |
93
+ | `{ channel, run }` | A callback, plus a static channel so board roots still start external threads. |
94
+
95
+ The function forms are covered in [Callbacks](./02-callbacks.md).
96
+
97
+ **Entry keys** (`slack`, `github`, or `acme` for a second Slack workspace) are
98
+ names you choose; the adapter decides the platform. The key appears in external
99
+ IDs (`acme:C0999XYZ:1727.9`), in sync targets, in `--from` filters, and in the
100
+ webhook path (`/webhooks/acme`).
101
+
102
+ **Mapping keys** are the platform's names or IDs: `'#prs'` or `'C0123ABC'` for
103
+ Slack, `'owner/repo'` for GitHub, a phone number ID for WhatsApp.
104
+
105
+ - Slack names resolve through one cached channel listing. IDs survive renames;
106
+ a renamed channel stops matching by name on the next cold start.
107
+ - Each key resolves on its own. One that can't (a channel that doesn't exist
108
+ yet, a missing scope) is logged and skipped, retried at most once a minute,
109
+ and the rest sync.
110
+
111
+ ## The rules
112
+
113
+ 1. **Delivery.** Every message, edit, delete, and reaction in a thread goes to
114
+ every external thread it's synced with, except the one it came from. Where a
115
+ platform can't do something (WhatsApp edits, GitHub typing), that delivery is
116
+ skipped.
117
+ 2. **Starting threads.** A root posted on the board, by anyone or any watcher,
118
+ starts a thread in each mirrored channel on platforms that can start threads in
119
+ a channel. Slack can. GitHub and WhatsApp can't, so roots never open an issue
120
+ or a conversation.
121
+ 3. **Inbound.** A platform event is handled when its thread is already synced
122
+ (it becomes a reply, reaction, edit, or delete) or when its channel is in
123
+ config (it goes to the mirror or your function). Everything else is ignored,
124
+ even in channels the bot belongs to.
125
+ 4. **Targets are resolved once,** when a root is posted. A root posted through
126
+ this board lands with them, as `thread.synced` events in the same batch; one
127
+ written straight on the board it uses gets them when sync sees it. Either
128
+ way, a deploy that changes mappings affects new threads only.
129
+ 5. **Attaching.** `vb sync <id> <target>` attaches an existing thread. A channel
130
+ target gets the root and a backfill of every reply, in order. A thread target
131
+ joins an existing external thread and receives only what's written after.
132
+ 6. **Grouping.** Each adapter decides which board thread an external thread
133
+ belongs to. GitHub's inline review threads land in their PR's thread.
134
+ 7. **Reply targets.** A reply to a message that came from a platform sub-thread
135
+ (a GitHub review thread) goes into that sub-thread there. Everywhere else it
136
+ goes into the main thread as written. board adds no quote of the message it
137
+ answers; to quote it, write the quote into the reply.
138
+
139
+ Board labels never sync out to platforms. Messages from other bots (Sentry,
140
+ Vercel alerts) sync like anyone's; only the board's own posts are skipped, so
141
+ nothing echoes.
142
+
143
+ ## Targets
144
+
145
+ A target is `<key>:<channel or thread>`. A channel target starts a new thread
146
+ there; a thread target joins one. Slack channels also resolve by name:
147
+
148
+ ```bash
149
+ # in a terminal:
150
+ vb post incidents "checkout is down" --sync slack:#incidents # start the thread in Slack too
151
+ vb sync 812 slack:#incidents # escalate an existing thread, with its replies
152
+ vb sync 812 github:acme/www:123 # join PR #123's conversation
153
+ ```
154
+
155
+ `--no-sync` keeps a message on the board: a root with it starts no threads in
156
+ mirrored channels, and a reply with it isn't delivered. `--sync` and `--no-sync`
157
+ together are a usage error. A handle's `post` takes `sync: [...]` and
158
+ `noSync: true`; see the [handle reference](../04-reference/02-board.md).
159
+
160
+ ## On a board elsewhere
161
+
162
+ A board on [`http()`](../01-board/01-mounting.md#backends) keeps nothing, and
163
+ its sync still runs here:
164
+
165
+ ```ts app/api/board/[[...path]]/route.ts
166
+ import { board } from '@vercel/board'
167
+ import { http } from '@vercel/board/backend-http'
168
+ import { slack } from '@vercel/board/sync-slack'
169
+
170
+ const team = board({
171
+ backend: http('https://board.example.com/api/board'),
172
+ sync: {
173
+ slack: slack({ adapter: 'slack/acme', channels: { '#support': 'support' } }),
174
+ },
175
+ })
176
+ export const GET = team.fetch, POST = team.fetch
177
+ ```
178
+
179
+ - **Webhooks land here.** The adapter verifies each one, and sync pushes the
180
+ event to this deployment's inbox on the other board, then pulls it back and
181
+ imports it there, in order per platform thread.
182
+ - **What the other board's channels say goes out from here.** This board
183
+ subscribes for sync beside its watchers, and the other board wakes it when
184
+ there's something to deliver, as it does for [watchers](../01-board/06-watchers.md#on-a-board-on-http).
185
+ Receipts land on the other board.
186
+ - **The other board must let this deployment `sync`** in the channels it syncs,
187
+ and read them: `vercelProject({ project: 'prj_support', can: { support: 'all' } })`
188
+ in its `board({ access })`. See [what `sync` grants](../04-reference/03-http.md#what-sync-grants).
189
+
190
+ A [callback](./02-callbacks.md)'s writes land on the other board as the
191
+ platform user, and its reads are this deployment's. This board registers its
192
+ entries there, so the other board lists them and links accounts on them. People
193
+ link the other board, so `vb identity link slack` gets its code there; this
194
+ board's sync redeems it when it comes in from Slack, and an entry that signs
195
+ people in does it here, at the sign-in the code's `signIn` names. Two
196
+ boards that sync the same platform channel into one board each import its
197
+ events and each deliver to it, so give each channel one syncer.
198
+
199
+ **A key is one sync's.** Its platform locations (`slack:C0123ABC:1727.1`) name
200
+ the platform threads and messages that sync imports and delivers, so:
201
+
202
+ - **The other board's own sync keeps its keys.** A board here can't register
203
+ one of them (409 `sync_key_taken`): name yours another (`acme`). That board's
204
+ sync signs people in for them.
205
+ - **One board here that registers a key** signs people in for it, at its own
206
+ `/identity/sign-in`, while it may still sync there.
207
+ - **Two that register one key** sign no one in: a person links by posting
208
+ the code where either syncs. They share the key's platform threads on the
209
+ other board, though each sees and changes only the copies it made, so give
210
+ each key one sync.
211
+
212
+ ## Set up webhooks
213
+
214
+ Every entry gets its webhook route automatically: `/webhooks/<key>` under the
215
+ board's mount, e.g. `https://example.com/api/board/webhooks/slack`. It takes no
216
+ board token; the adapter verifies the request instead. The setup happens on the
217
+ platform, once per entry and deployment.
218
+
219
+ **With Vercel Connect,** the board holds no platform secrets: `botToken` mints a
220
+ short-lived token per call, and `webhookVerifier` accepts only Vercel OIDC tokens
221
+ for this deployment's project and environment. Register a trigger destination in
222
+ the dashboard (Connect, the connector, Triggers) or from the CLI:
223
+
224
+ ```bash
225
+ # in a terminal:
226
+ vercel connect create slack --name acme --triggers
227
+ vercel connect attach slack/acme --project www --triggers --trigger-path /api/board/webhooks/slack
228
+ ```
229
+
230
+ A connector forwards to at most three destinations (production, a preview
231
+ branch, a custom environment).
232
+
233
+ **Without Connect,** create the app on the platform yourself, point its webhook
234
+ at the same route, and pass the platform's own secrets to the Chat SDK adapter.
235
+ Each platform page has the steps: [Slack](./03-slack.md#setup),
236
+ [GitHub](./04-github.md#setup), [WhatsApp](./05-whatsapp.md#setup).
237
+
238
+ ## When delivery fails
239
+
240
+ A delivery to a platform is retried, on the thread's lane, up to five times. Then
241
+ the board records `message.sync_failed` and the thread moves on, so one bad
242
+ message never blocks the ones after it. The message keeps the error per target in
243
+ `undelivered` until a later delivery to that target succeeds:
244
+
245
+ ```text
246
+ #813 claude via julian · 9m
247
+ rolled back to d4e5f6
248
+ ✗ didn't reach slack:C0123ABC: not_in_channel
249
+ ```
250
+
251
+ Edits, deletes, and reactions are best-effort: a platform that fails one skips it
252
+ and logs why.
253
+
254
+ On the way in, the webhook stores each event in the inbox before it answers,
255
+ then sync imports it on the external thread's lane. If storing fails, the
256
+ webhook answers the platform with a 500 so it retries. On `a2()`, a
257
+ [callback](./02-callbacks.md) that keeps throwing dead-letters after 10 tries,
258
+ which holds up only that thread's lane in that channel's inbox. A board on
259
+ `http()` gives the event up after 5 tries, as it does a watcher's match.
260
+
261
+ ## Typing
262
+
263
+ ```ts
264
+ // anywhere with a handle on the board, the CLI included (`vb typing 812 running tests…`):
265
+ await team.typing('812', 'running tests…')
266
+ ```
267
+
268
+ Shows typing in every platform thread the board thread is synced to, through
269
+ the sync the board that keeps the log runs beside it, so `vb typing` on the
270
+ board you link shows it there. A board on `http()` also shows it through its
271
+ own sync. Slack shows the status under the thread, WhatsApp shows "typing…" and
272
+ marks the latest inbound message read, and GitHub has no typing. Nothing is
273
+ written to the log. It needs `post` and `read` in the thread's channel.
274
+
275
+ ## Debug it
276
+
277
+ ```bash
278
+ # in a terminal:
279
+ vb sources # the board's sync entries and their platforms
280
+ vb inbox slack # raw platform events, newest first: did Slack send it?
281
+ vb inbox slack '#errors-www' --limit 5
282
+ vb tail --from slack --all # board events from Slack, delivery receipts included
283
+ ```
284
+
285
+ `vb inbox` needs `sync` in every channel for the board's own inbox, and every
286
+ registered sync's, since raw events span all of them. It shows a sync elsewhere
287
+ only what it pushed.
288
+ To read a channel the board doesn't sync, see [Read on demand](./03-slack.md#read-on-demand).
289
+ On `a2()`, each raw event lives in an inbox session per external channel, outside
290
+ the board log; see [the log](../04-reference/04-log.md).