@vercel/board 0.0.0 → 0.16.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.
- package/README.md +180 -0
- package/dist/a2-C41hD8SF.mjs +3045 -0
- package/dist/a2-D24cvwpN.d.mts +3359 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +30 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-DjMeZpnU.mjs +224 -0
- package/dist/attempts-Bxp9KgPf.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-CU56Q2Xh.d.mts +204 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-0roEmjl0.mjs +613 -0
- package/dist/backend-http-BVRno-ca.mjs +469 -0
- package/dist/backend-http-Bid7RIpC.d.mts +630 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-Bir5s8ts.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-DLiE7fLY.d.mts +4 -0
- package/dist/exports-Dvc1o52I.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-Bkg77Pxn.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-BsOOc-6_.mjs +170 -0
- package/dist/server-jwPY7_wi.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-yM5JBUgr.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-CLjsa6od.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +264 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +501 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +168 -1
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- 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).
|