Skip to content

Commit 8114fdd

Browse files
authored
docs: restructure along Diátaxis — maturity tiers, non-goals, tutorial, how-tos (#11)
* docs: restructure along Diátaxis — maturity tiers, non-goals, tutorial, how-tos Closes #9 - README: honest adapter maturity tiers (Slack supported, Linear experimental, Teams spike), Non-Goals section with ADR rationales, Diátaxis docs index, and staleness fixes (rate-limit retry, multi-tenant installs, HistoryReader, DispatchDeferred, ConcurrencyQueue, NATS state, Linear full activity surface are all implemented and now documented as such). - docs/: Diátaxis skeleton — verified Slack bot tutorial, six how-to guides, reference (pkg.go.dev pointers + capability matrix), explanation index over CONTEXT.md and the ADRs. - docs/agents/issue-tracker.md: GitHub issues are the public source of truth; .scratch/ demoted to internal working notes. * docs: address codex review findings — fix inaccurate claims in how-to guides - slash commands: respond via RespondURL (channel commands have a synthetic channel root that Thread.Post cannot post to) - deferred dispatch: build options from DefaultRuntimeOptions (WithRuntimeOptions replaces, not merges); describe Shutdown as cancel-then-drain, not graceful completion - interactive components: actor ID mention instead of unset Actor.Name; accurate sync-vs-deferred ack ordering; view_submission payloads are dropped, not observable; OpenModalForTenant in multi-tenant mode - README: accurate ack ordering for command/interaction events under DispatchSync - AGENTS.md: align issue-tracker policy with GitHub issues as public source of truth * docs: address round-2 codex review findings - slash commands: channel command lock scope is the synthetic channel root, distinct from message thread scopes — no cross-serialization claim - interactive components: document the missing public trigger_id accessor as a known gap (tracked in #12) instead of an un-followable example - linear agent sessions: check every activity error before continuing - deferred dispatch (and siblings): describe the detached tail as launched at ack time / concurrent with the response, not strictly after the 2xx * docs: address round-3 codex review findings - tutorial: Step 8 adds message.channels + channels:history (or DM path) before promising unmentioned follow-ups - linear-agent-capabilities.md: rewritten to match ADR 0008/0013 reality (GraphQL, full activity surface, signals, stop, session updates all ship); remaining gaps are proactive session creation, repo suggestions, activity history/HistoryReader parity, workflow helpers, UX examples - interactive components: chat.Text renders <@id> literally — no mention claim - multi-tenant: distinguish Slack's verified lookup from Linear's untrusted pre-verification routing lookup - deferred dispatch + README: queue coalescing is per-process; cross-replica superseded events can each run - linear agent sessions: concurrent-ack phrasing; stop check cannot preempt in-flight handlers — document app-owned cancellation * docs: address round-4 codex review findings - deferred dispatch: document loss-of-exclusivity when lock renewal fails (renewal stops, handler keeps running; overlap possible after TTL) - linear agent sessions: first activity must be a thought, not a response — a response is a completion signal that ends the session - interactive components: PostNative example compiles and checks its error; menu selected-option values share the unexported-payload gap (#12 extended) * docs: address round-5 codex review findings - linear agent sessions: session-handler example posts a genuinely final response; elicitation/error/response presented as mutually exclusive completion branches after nonterminal progress activities - ADR 0006: implementation note — Slack verifies the shared signature before tenant parse/lookup; unverified routing reads apply to per-install-signed platforms (Linear) - README non-goals: streaming framed as deferred-not-foreclosed per ADR 0011, not a permanent boundary - multi-tenant: Linear BotUserID/BotActorID documented as required for mention detection and self-filtering, with OAuth capture guidance * docs: address round-6 codex review finding - deferred dispatch: a queued follow-up's DetachTimeout starts before the lock wait, so queue time consumes its budget and an exhausted wait cancels the (already-deduped) follow-up without running it * docs: address round-7 codex review findings - slash commands: channel-wide queue coalescing caveat (one pending slot per channel scope supersedes independent commands) - linear stop signal: state plainly that Stop cannot cancel in-flight work through the adapter today; short-turn or out-of-band patterns instead - multi-tenant: per-install bot identity required on Slack too (self-filter loop risk with message events), not just Linear - capability tracker: lazy token refresh applies to client-credential installs only - README: state-module go get commands fail externally until a tagged release; document the replace workaround - CONTEXT.md: thread-lock relationship now matches implemented drop/queue strategy behavior instead of forbidding drops * docs: address round-8 codex review finding - interactive components: document that repeat activations of the same action_id on one message dedupe to a single event for DedupeTTL (identity anchors on message ts, not the activation); tracked in #43 with interim distinct-action_id guidance * docs: address round-9 codex review findings - interactive components: scope block_actions support to message-based blocks (modal-view containers are rejected before routing); attribution vs real-mention distinction - multi-tenant: thread reconstruction validates only; credential lookup happens at post time - reference: agent activities are thought/response/action/elicitation/error; plans/external URLs are session updates - linear generic comments: subscription precedence over mention routing - tutorial: qualify follow-up echoes under the default drop strategy * docs: address round-10 codex review findings; reconcile with moved main Main gained Linear HistoryReader (#37), OpenModalFromRaw (#42), and v0.1.0 tags with externally consumable submodules (#15); rebased and reconciled: - README/reference/tracker: Linear HistoryReader now supported; go get caveat removed (v0.1.0 tags exist); modal-open documented via OpenModalFromRaw / OpenModalForTenantFromRaw - capability tracker: roadmap section replaced with issue links (#47-#49); activity-history section removed (implemented); subscription precedence qualified in routing rows - reference matrix: Linear PostEphemeral marked unsupported (thoughts are a separate Linear-specific surface); block_actions scoped to messages - state backend guide: per-application namespace section (shared-backend record collisions) with Prefix/Namespace set in every production snippet - interactive components: action-value gap re-tracked as #46 (follow-up to closed #12) * docs: address round-11 codex review findings - explanation + linear guide: plans are session updates, not a sixth agent activity; the five activities are thought/response/action/elicitation/error - interactive components: queued interactions can outlive trigger_id's 3-second lifetime before the handler runs; modal opens then fail
1 parent 3163fed commit 8114fdd

17 files changed

Lines changed: 1496 additions & 331 deletions

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Issue tracker
66

7-
Issues and PRDs are tracked as local markdown files under `.scratch/`. See `docs/agents/issue-tracker.md`.
7+
GitHub issues (https://github.com/coder/chat/issues) are the public source of truth for roadmap and bugs; `.scratch/` holds internal working notes only. See `docs/agents/issue-tracker.md`.
88

99
### Triage labels
1010

‎CONTEXT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ _Avoid_: Full platform schema, strict external SDK model
417417
- **Runtime Options** TTL values must be positive.
418418
- **Runtime Options** include a **Concurrency Strategy** that defaults to drop.
419419
- The runtime implements the drop (default) and queue **Concurrency Strategy** values; burst, debounce, concurrent, lock-scope, and force/steerability remain reserved for future slices.
420-
- A **Thread Lock** must not drop distinct **Webhook Events** for the same **Thread**; it only coordinates their processing.
420+
- A **Thread Lock** coordinates processing of distinct **Webhook Events** for the same **Thread**; it never deduplicates them, and what happens to a conflicting event is decided by the **Concurrency Strategy** (drop acknowledges and drops it; queue coalesces waiters per process and runs the most recent after the lock releases).
421421
- A **Thread Lock** is represented as a **Lock Lease** with an ownership token.
422422
- Releasing or extending a **Lock Lease** must verify the ownership token so an expired holder cannot affect a newer holder.
423423
- A **Lock Conflict** is acknowledged to the platform by default and recorded as unhandled runtime contention.

‎README.md‎

Lines changed: 173 additions & 65 deletions
Large diffs are not rendered by default.

‎docs/README.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Chat SDK Go Documentation
2+
3+
User-facing documentation is organized along [Diátaxis](https://diataxis.fr/):
4+
learning-oriented tutorials, task-oriented how-to guides, information-oriented
5+
reference, and understanding-oriented explanation.
6+
7+
## Tutorials
8+
9+
Start here if you are new to the SDK.
10+
11+
- [Your first Slack bot](tutorials/slack-bot.md) — zero to a running Slack bot
12+
in under 30 minutes.
13+
14+
## How-To Guides
15+
16+
Task-oriented guides for people already running a bot.
17+
18+
- [Choose a state backend](how-to/choose-a-state-backend.md) — memory, Redis,
19+
Postgres, or NATS JetStream.
20+
- [Defer long-running work (ack-then-work)](how-to/deferred-dispatch.md) —
21+
acknowledge webhooks fast and run handlers on a detached context.
22+
- [Handle slash commands](how-to/slash-commands.md) — route Slack slash
23+
commands through `OnCommand`.
24+
- [Handle interactive components](how-to/interactive-components.md) — buttons,
25+
menus, Block Kit content, and modals.
26+
- [Install into multiple workspaces (multi-tenant)](how-to/multi-tenant-install.md) —
27+
resolve per-tenant credentials with an `InstallStore`.
28+
- [Run Linear agent sessions](how-to/linear-agent-sessions.md) — build a Linear
29+
agent with thoughts, responses, actions, elicitations, and plans.
30+
31+
## Reference
32+
33+
- [API and package reference](reference.md) — pkg.go.dev pointers, module
34+
layout, and per-adapter capability status.
35+
- [Linear agent capability gaps](linear-agent-capabilities.md) — tracked list
36+
of Linear agent APIs the adapter does not yet wrap.
37+
38+
## Explanation
39+
40+
- [Architecture and design decisions](explanation.md) — an index over
41+
[`CONTEXT.md`](../CONTEXT.md) (the ubiquitous language and architecture
42+
document) and the [ADRs](adr/) that record every significant decision.
43+
44+
## Non-User Documentation
45+
46+
- [`docs/agents/`](agents/) — instructions for coding agents working on this
47+
repository, not for SDK users.

‎docs/adr/0006-multi-tenant-install.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Specifically:
5050

5151
- **Lookup ordering.** During webhook handling the adapter: (1) parses the **Platform Tenant** out of the **Supported Platform Shape**; (2) calls `InstallStore.Lookup`; (3) verifies the signature using install-record material where the platform signs per-install, or the shared app-level signing secret where it does not (Slack); (4) normalizes the tenant-scoped **Event** and hands it to **Runtime Dispatch**. On platforms with a per-install signing secret the tenant must be read from an unverified body for routing only, then re-validated by signature verification before any side effect.
5252

53+
*Implementation note (as built):* the Slack adapter verifies the shared app-level signature **before** parsing the tenant or calling `Lookup`, so a Slack `InstallStore` only ever sees tenants from verified requests. The unverified-routing-read ordering above applies to per-install-signed platforms (Linear), where the store's tenant argument is untrusted routing input by necessity.
54+
5355
- **Not-installed is an Ignored Event.** `ErrInstallNotFound` is acknowledged to the platform without dispatch, consistent with CONTEXT.md's **Ignored Event** definition. Any other **Install Store** error is a transport failure the platform may retry.
5456

5557
- **Out-of-webhook posting uses the same resolver.** **Thread Handle** reconstruction decodes the **Platform Tenant** from the **Thread ID**, calls `InstallStore.Lookup`, and posts. A stored **Thread ID** stays postable while the app holds a valid install record.
@@ -71,7 +73,7 @@ Deliberate divergences from the upstream Chat SDK and trade-offs:
7173

7274
- The runtime owns the credential-lookup *contract* but never the store, unlike full marketplace SDKs that ship installation persistence.
7375
- The OAuth web flow is explicitly out: the app owns authorize/callback routes, matching how the runtime exposes only **Webhook Handlers** and owns no HTTP server.
74-
- On per-app-signed platforms (Slack) the tenant is parsed from an unverified body before verification. This is a routing read only, re-validated by signature verification before side effects; adapters document the ordering.
76+
- On per-install-signed platforms (Linear) the tenant is parsed from an unverified body before verification. This is a routing read only, re-validated by signature verification before side effects; adapters document the ordering. (On per-app-signed platforms — Slack — the implementation verifies the shared signature first, so lookup happens post-verification.)
7577
- The credential payload is adapter-specific (`any`), trading a normalized token model for honesty about how differently Slack and Linear authorize.
7678

7779
Costs: the app must build and secure the **Install Store**; a misimplemented store (returning a stale or wrong-tenant credential) can post to the wrong workspace, so tenant-correctness tests are required. Adapters carry two construction modes to keep tested.

‎docs/agents/issue-tracker.md‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
1-
# Issue tracker: Local Markdown
1+
# Issue tracker: GitHub Issues
22

3-
Issues and PRDs for this repo live as markdown files in `.scratch/`.
3+
The public source of truth for this repo's roadmap, feature requests, and bug
4+
reports is GitHub issues:
45

5-
## Conventions
6+
https://github.com/coder/chat/issues
7+
8+
Anything user-facing — planned work, accepted/rejected proposals, bug state —
9+
belongs there. Use `gh issue` to read and update it.
10+
11+
## `.scratch/` is internal working notes only
12+
13+
The `.scratch/` directory holds internal working notes: PRDs, implementation
14+
breakdowns, and drafts that agents produce while working. It is not a public
15+
roadmap, it makes no promises, and nothing in it should be treated as
16+
authoritative over GitHub issues. When a scratch note graduates into real
17+
planned work, file a GitHub issue for it.
18+
19+
### Conventions for `.scratch/`
620

721
- One feature per directory: `.scratch/<feature-slug>/`
822
- The PRD is `.scratch/<feature-slug>/PRD.md`
9-
- Implementation issues are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`
10-
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
23+
- Implementation notes are `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`
24+
- Triage state is recorded as a `Status:` line near the top of each file (see `triage-labels.md` for the role strings)
1125
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
1226

1327
## When a skill says "publish to the issue tracker"
1428

15-
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
29+
Create a GitHub issue with `gh issue create`. Use `.scratch/<feature-slug>/`
30+
only for supporting working notes that are not ready to be public.
1631

1732
## When a skill says "fetch the relevant ticket"
1833

19-
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
34+
If the reference is a number or URL, read the GitHub issue with
35+
`gh issue view`. If the reference is a path, read the file at that path.

‎docs/explanation.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Architecture And Design Decisions
2+
3+
Chat SDK Go's design is documented in two places, and this page is the index
4+
over both:
5+
6+
- [`CONTEXT.md`](../CONTEXT.md) — the ubiquitous language and architecture
7+
document. It defines every domain term precisely (with the synonyms to
8+
avoid), states the architectural invariants as explicit relationships, and
9+
records resolved ambiguities against upstream Vercel Chat SDK behavior.
10+
- [`docs/adr/`](adr/) — Architecture Decision Records. Every significant
11+
decision has one, including the decisions *not* to build something.
12+
13+
## Reading CONTEXT.md
14+
15+
If you want to understand the system, read `CONTEXT.md` top to bottom; it is
16+
the single most information-dense document in the repository. Its `Language`
17+
section groups the vocabulary by area — runtime lifecycle, the platform
18+
adapter boundary, Linear session lifecycle, threads and routing, the
19+
event/message model, dispatch and concurrency, observability, state and
20+
history, content and formatting, and tenancy and identity. The
21+
`Relationships` section is the closest thing to a formal specification of the
22+
runtime's invariants, and `Flagged ambiguities` explains where and why the
23+
design deliberately diverges from Vercel Chat SDK.
24+
25+
## Decision Records
26+
27+
| ADR | Decision | Status |
28+
| --- | --- | --- |
29+
| [0001](adr/0001-linear-app-actor-slice.md) | Linear app-actor slice before a full Linear adapter | Accepted |
30+
| [0002](adr/0002-async-dispatch.md) | Deferred runtime dispatch (ack-then-work) | Accepted |
31+
| [0003](adr/0003-slash-commands.md) | Command Events and slash command routing | Accepted |
32+
| [0004](adr/0004-interactive-components.md) | Interaction Events and native content instead of a card DSL | Accepted |
33+
| [0005](adr/0005-rate-limit-handling.md) | Rate-limit retry lives in adapters, with typed `RateLimited` errors | Accepted |
34+
| [0006](adr/0006-multi-tenant-install.md) | Multi-tenant installs via app-implemented `InstallStore`; OAuth flows stay app-owned | Accepted |
35+
| [0007](adr/0007-teams-adapter.md) | Microsoft Teams adapter approach (Bot Framework, direct HTTP) | Proposed — gated on a spike |
36+
| [0008](adr/0008-linear-full-adapter.md) | Full Linear agent activity surface (thought/response/action/elicitation/error) plus session updates (plans, external URLs) | Accepted |
37+
| [0009](adr/0009-message-history.md) | Message history stays application-owned; optional storage-free `HistoryReader` | Accepted |
38+
| [0010](adr/0010-observability.md) | Optional `Observer` seam; no OpenTelemetry in core | Accepted |
39+
| [0011](adr/0011-resumable-streaming.md) | Resumable streaming deferred from core, not foreclosed | Proposed |
40+
| [0012](adr/0012-concurrency-strategy.md) | Concurrency strategy expansion (`queue` implemented; `burst`/`debounce`/`concurrent` staged) | Accepted (staged) |
41+
| [0013](adr/0013-linear-generic-comments.md) | Linear generic issue/comment participation | Accepted |
42+
| [0014](adr/0014-nats-state-adapter.md) | NATS JetStream state adapter | Accepted |
43+
44+
## The Short Version
45+
46+
For readers who want the model in five paragraphs:
47+
48+
**The runtime coordinates conversations; it does not own your product.**
49+
`chat.Chat` verifies webhooks through adapters, normalizes platform payloads
50+
into events, dedupes them, serializes work per thread with token-owned lock
51+
leases, and routes to your single-slot handlers. Everything your product
52+
stores — transcripts, user records, workflow state — lives in your database,
53+
keyed by the opaque `ThreadID`.
54+
55+
**Adapters own the platform boundary.** Signature verification, payload
56+
normalization, outbound rendering, rate-limit retries, and platform quirks
57+
live inside the adapter. Platform-specific power is reached deliberately via
58+
typed adapter access (`chat.AdapterAs`), never by making raw platform structs
59+
the normal API.
60+
61+
**State is required and small.** Subscriptions, dedupe marks, and locks —
62+
that is all. Memory for development; Redis, Postgres, or NATS for production.
63+
64+
**Events are broader than messages.** A slash command and a button click are
65+
normalized events with their own hooks, not messages. All events ride the
66+
same dispatch spine.
67+
68+
**Semantic compatibility, not feature parity.** Vercel Chat SDK's
69+
conversation model is the precedent; its TypeScript API shapes are not. Where
70+
Go idioms or operational safety argue otherwise, this SDK deliberately
71+
diverges and documents the divergence.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# How To Choose A State Backend
2+
3+
Runtime state is required: the runtime stores subscribed-thread membership,
4+
event dedupe marks, and thread lock leases in a `chat.State`. It is
5+
coordination state, not product state — keep your application's own data in
6+
your own database keyed by `ThreadID`.
7+
8+
Four implementations ship today:
9+
10+
| Backend | Module | Use for |
11+
| --- | --- | --- |
12+
| Memory | `github.com/coder/chat/state/memory` (in the core module) | Tests and local demos. Lost on restart. |
13+
| Redis | `github.com/coder/chat/state/redis` | Production, horizontally scaled deployments. |
14+
| Postgres | `github.com/coder/chat/state/postgres` | Production, when Postgres is already your coordination store. |
15+
| NATS JetStream | `github.com/coder/chat/state/nats` | Production, when you already run NATS with JetStream. |
16+
17+
Redis, Postgres, and NATS live in separate Go modules so applications that only
18+
need core, Slack, or memory state do not pull their dependencies.
19+
20+
## Memory
21+
22+
```go
23+
import "github.com/coder/chat/state/memory"
24+
25+
bot, err := chat.New(ctx,
26+
chat.WithState(memory.New()),
27+
chat.WithAdapter(adapter),
28+
)
29+
```
30+
31+
Memory state is for tests and local development only. Subscriptions and dedupe
32+
data vanish when the process exits, so a restarted bot forgets which threads it
33+
was in and may re-handle redelivered events.
34+
35+
## Redis
36+
37+
```sh
38+
go get github.com/coder/chat/state/redis
39+
```
40+
41+
```go
42+
import (
43+
"github.com/redis/go-redis/v9"
44+
45+
chatredis "github.com/coder/chat/state/redis"
46+
)
47+
48+
redisState, err := chatredis.New(ctx, chatredis.Options{
49+
Client: redis.NewClient(&redis.Options{Addr: os.Getenv("REDIS_ADDR")}),
50+
Prefix: "mybot", // see "One namespace per bot application" below
51+
})
52+
```
53+
54+
The runnable example, including a `compose.yaml` for a local Redis, is
55+
[`examples/slack-redis-state`](../../examples/slack-redis-state/README.md).
56+
57+
## Postgres
58+
59+
```sh
60+
go get github.com/coder/chat/state/postgres
61+
```
62+
63+
```go
64+
import (
65+
"github.com/jackc/pgx/v5/pgxpool"
66+
67+
chatpostgres "github.com/coder/chat/state/postgres"
68+
)
69+
70+
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
71+
if err != nil {
72+
return err
73+
}
74+
75+
pgState, err := chatpostgres.New(ctx, chatpostgres.Options{
76+
Pool: pool,
77+
Namespace: "mybot", // see "One namespace per bot application" below
78+
})
79+
```
80+
81+
The Postgres state initializes its own schema (subscription, event, and lock
82+
tables) on startup. The runnable example is
83+
[`examples/slack-postgres-state`](../../examples/slack-postgres-state/README.md).
84+
85+
## NATS JetStream
86+
87+
```sh
88+
go get github.com/coder/chat/state/nats
89+
```
90+
91+
```go
92+
import (
93+
natsgo "github.com/nats-io/nats.go"
94+
95+
chatnats "github.com/coder/chat/state/nats"
96+
)
97+
98+
conn, err := natsgo.Connect(os.Getenv("NATS_URL"))
99+
if err != nil {
100+
return err
101+
}
102+
103+
natsState, err := chatnats.New(ctx, chatnats.Options{
104+
Conn: conn,
105+
Prefix: "mybot", // see "One namespace per bot application" below
106+
// DedupeTTL and ThreadLockTTL default to the runtime defaults (24h and
107+
// 2m) and must match your RuntimeOptions.
108+
})
109+
```
110+
111+
NATS state stores subscriptions, dedupe marks, and locks in three JetStream
112+
Key-Value buckets with bucket-level TTLs (see
113+
[ADR 0014](../adr/0014-nats-state-adapter.md)). Because JetStream TTLs are
114+
per-bucket, the dedupe and lock TTLs are fixed at construction time. The
115+
runnable example is
116+
[`examples/slack-nats-state`](../../examples/slack-nats-state/README.md).
117+
118+
## One Namespace Per Bot Application
119+
120+
The `Prefix`/`Namespace` options default to `chat`. If two *independent* bot
121+
applications share one Redis, Postgres, or NATS service with the default,
122+
their subscription, dedupe, and lock records collide — thread IDs carry
123+
platform tenant/channel identity but no application identity, so app A
124+
subscribing a thread can route that thread's follow-ups into app B's
125+
`OnSubscribedMessage`, and one app's locks can suppress the other's events.
126+
Give every bot application its own stable namespace, shared only by that
127+
app's replicas (replicas must share the namespace — that is what makes
128+
dedupe and locking work across them).
129+
130+
## How To Decide
131+
132+
- Writing tests or following the tutorial: use memory.
133+
- Already running Redis: use Redis. Same for Postgres and NATS — the backends
134+
are contract-equivalent, so pick the one you already operate.
135+
- Running more than one bot replica: any of the durable backends works; all
136+
three implement the same token-owned lock lease and dedupe contract, which is
137+
what makes horizontal scaling safe.
138+
139+
All backends are exercised by the same conformance suite; Redis and Postgres
140+
integration tests run against real backends via Testcontainers, and NATS tests
141+
run against an embedded JetStream server.

0 commit comments

Comments
 (0)