@terreno/mcp 56.0.0-beta.0 → 57.0.0-beta.1

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +10 -11
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/docs/templates/bootstrap/backend/types/models/userTypes.template.md +5 -4
  6. package/dist/docs/templates/tools/model.template.md +4 -2
  7. package/dist/docs/upgrades/0.31.0.md +60 -0
  8. package/dist/docs/upgrades/56.0.0-beta.2.md +59 -0
  9. package/dist/docs/upgrades/57.0.0-beta.1.md +41 -0
  10. package/dist/docs/versioned/next/README.md +1 -0
  11. package/dist/docs/versioned/next/explanation/README.md +7 -1
  12. package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +65 -0
  13. package/dist/docs/versioned/next/explanation/deployment-baseline.md +78 -0
  14. package/dist/docs/versioned/next/explanation/positioning.md +164 -0
  15. package/dist/docs/versioned/next/explanation/repository-settings.md +44 -0
  16. package/dist/docs/versioned/next/explanation/roadmap-process.md +330 -0
  17. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +887 -0
  18. package/dist/docs/versioned/next/how-to/README.md +8 -0
  19. package/dist/docs/versioned/next/how-to/build-for-web.md +69 -0
  20. package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +115 -0
  21. package/dist/docs/versioned/next/how-to/deploy-to-gcp.md +20 -228
  22. package/dist/docs/versioned/next/how-to/deploy-web-to-gcs-cdn.md +183 -0
  23. package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +31 -0
  24. package/dist/docs/versioned/next/implementationPlans/README.md +56 -30
  25. package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +2 -1
  26. package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +1 -0
  27. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +115 -0
  28. package/dist/docs/versioned/next/implementationPlans/billing-stripe.md +158 -0
  29. package/dist/docs/versioned/next/implementationPlans/build-terreno-app-validation.md +1 -0
  30. package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +323 -0
  31. package/dist/docs/versioned/next/implementationPlans/comms-adapter-expo-push.md +107 -0
  32. package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +157 -0
  33. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +155 -0
  34. package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +99 -0
  35. package/dist/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  36. package/dist/docs/versioned/next/implementationPlans/deploy-to-gcp.md +1 -0
  37. package/dist/docs/versioned/next/implementationPlans/deploy-to-vercel.md +1 -0
  38. package/dist/docs/versioned/next/implementationPlans/deployment-foundation.md +1 -0
  39. package/dist/docs/versioned/next/implementationPlans/docs-reference-coverage.md +1 -0
  40. package/dist/docs/versioned/next/implementationPlans/docs-tutorials-ai-first.md +1 -0
  41. package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +1 -0
  42. package/dist/docs/versioned/next/implementationPlans/infra-mcp-research.md +188 -0
  43. package/dist/docs/versioned/next/implementationPlans/infra-mcp.md +269 -0
  44. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  45. package/dist/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  46. package/dist/docs/versioned/next/implementationPlans/native-module-baseline.md +105 -0
  47. package/dist/docs/versioned/next/implementationPlans/orgs-and-teams.md +158 -0
  48. package/dist/docs/versioned/next/implementationPlans/oss-governance-baseline.md +1 -0
  49. package/dist/docs/versioned/next/implementationPlans/password-reset-and-email-verification.md +147 -0
  50. package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -0
  51. package/dist/docs/versioned/next/implementationPlans/public-roadmap-github.md +1 -0
  52. package/dist/docs/versioned/next/implementationPlans/upgrade-guides-and-skill.md +1 -0
  53. package/dist/docs/versioned/next/implementationPlans/web-ssr-and-admin-spa.md +1 -0
  54. package/dist/docs/versioned/next/reference/README.md +5 -0
  55. package/dist/docs/versioned/next/reference/admin-spa.md +130 -0
  56. package/dist/docs/versioned/next/reference/ai.md +519 -0
  57. package/dist/docs/versioned/next/reference/comms.md +132 -0
  58. package/dist/docs/versioned/next/reference/environment-variables.md +229 -382
  59. package/dist/docs/versioned/next/reference/test.md +108 -0
  60. package/dist/docs/versioned/next/roadmap.md +9 -0
  61. package/dist/docs/versioned/next/tasks/README.md +42 -0
  62. package/dist/docs/versioned/next/tasks/agentic-sdlc-plugin.md +5 -5
  63. package/dist/docs/versioned/next/tasks/billing-stripe.md +75 -0
  64. package/dist/docs/versioned/next/tasks/comms-abstraction.md +90 -0
  65. package/dist/docs/versioned/next/tasks/comms-adapter-expo-push.md +29 -0
  66. package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +39 -0
  67. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +39 -0
  68. package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +19 -0
  69. package/dist/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  70. package/dist/docs/versioned/next/tasks/deploy-to-gcp.md +11 -11
  71. package/dist/docs/versioned/next/tasks/deployment-foundation.md +12 -12
  72. package/dist/docs/versioned/next/tasks/docs-reference-coverage.md +4 -4
  73. package/dist/docs/versioned/next/tasks/infra-mcp.md +43 -0
  74. package/dist/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  75. package/dist/docs/versioned/next/tasks/native-module-baseline.md +42 -0
  76. package/dist/docs/versioned/next/tasks/orgs-and-teams.md +70 -0
  77. package/dist/docs/versioned/next/tasks/oss-governance-baseline.md +18 -18
  78. package/dist/docs/versioned/next/tasks/password-reset-and-email-verification.md +60 -0
  79. package/dist/docs/versioned/next/tasks/positioning-django-rails-universal.md +4 -4
  80. package/dist/docs/versioned/next/tasks/public-roadmap-github.md +15 -15
  81. package/package.json +4 -3
  82. package/src/docs/templates/bootstrap/backend/types/models/userTypes.template.md +5 -4
  83. package/src/docs/templates/tools/model.template.md +4 -2
  84. package/src/docs/upgrades/0.31.0.md +60 -0
  85. package/src/docs/upgrades/56.0.0-beta.2.md +59 -0
  86. package/src/docs/upgrades/57.0.0-beta.1.md +41 -0
  87. package/src/docs/versioned/next/README.md +1 -0
  88. package/src/docs/versioned/next/explanation/README.md +7 -1
  89. package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +65 -0
  90. package/src/docs/versioned/next/explanation/deployment-baseline.md +78 -0
  91. package/src/docs/versioned/next/explanation/positioning.md +164 -0
  92. package/src/docs/versioned/next/explanation/repository-settings.md +44 -0
  93. package/src/docs/versioned/next/explanation/roadmap-process.md +330 -0
  94. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +887 -0
  95. package/src/docs/versioned/next/how-to/README.md +8 -0
  96. package/src/docs/versioned/next/how-to/build-for-web.md +69 -0
  97. package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +115 -0
  98. package/src/docs/versioned/next/how-to/deploy-to-gcp.md +20 -228
  99. package/src/docs/versioned/next/how-to/deploy-web-to-gcs-cdn.md +183 -0
  100. package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +31 -0
  101. package/src/docs/versioned/next/implementationPlans/README.md +56 -30
  102. package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +2 -1
  103. package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +1 -0
  104. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +115 -0
  105. package/src/docs/versioned/next/implementationPlans/billing-stripe.md +158 -0
  106. package/src/docs/versioned/next/implementationPlans/build-terreno-app-validation.md +1 -0
  107. package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +323 -0
  108. package/src/docs/versioned/next/implementationPlans/comms-adapter-expo-push.md +107 -0
  109. package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +157 -0
  110. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +155 -0
  111. package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +99 -0
  112. package/src/docs/versioned/next/implementationPlans/comms-admin-dashboard.md +180 -0
  113. package/src/docs/versioned/next/implementationPlans/deploy-to-gcp.md +1 -0
  114. package/src/docs/versioned/next/implementationPlans/deploy-to-vercel.md +1 -0
  115. package/src/docs/versioned/next/implementationPlans/deployment-foundation.md +1 -0
  116. package/src/docs/versioned/next/implementationPlans/docs-reference-coverage.md +1 -0
  117. package/src/docs/versioned/next/implementationPlans/docs-tutorials-ai-first.md +1 -0
  118. package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +1 -0
  119. package/src/docs/versioned/next/implementationPlans/infra-mcp-research.md +188 -0
  120. package/src/docs/versioned/next/implementationPlans/infra-mcp.md +269 -0
  121. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci-research.md +126 -0
  122. package/src/docs/versioned/next/implementationPlans/migrate-cicd-to-circleci.md +269 -0
  123. package/src/docs/versioned/next/implementationPlans/native-module-baseline.md +105 -0
  124. package/src/docs/versioned/next/implementationPlans/orgs-and-teams.md +158 -0
  125. package/src/docs/versioned/next/implementationPlans/oss-governance-baseline.md +1 -0
  126. package/src/docs/versioned/next/implementationPlans/password-reset-and-email-verification.md +147 -0
  127. package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +1 -0
  128. package/src/docs/versioned/next/implementationPlans/public-roadmap-github.md +1 -0
  129. package/src/docs/versioned/next/implementationPlans/upgrade-guides-and-skill.md +1 -0
  130. package/src/docs/versioned/next/implementationPlans/web-ssr-and-admin-spa.md +1 -0
  131. package/src/docs/versioned/next/reference/README.md +5 -0
  132. package/src/docs/versioned/next/reference/admin-spa.md +130 -0
  133. package/src/docs/versioned/next/reference/ai.md +519 -0
  134. package/src/docs/versioned/next/reference/comms.md +132 -0
  135. package/src/docs/versioned/next/reference/environment-variables.md +229 -382
  136. package/src/docs/versioned/next/reference/test.md +108 -0
  137. package/src/docs/versioned/next/roadmap.md +9 -0
  138. package/src/docs/versioned/next/tasks/README.md +42 -0
  139. package/src/docs/versioned/next/tasks/agentic-sdlc-plugin.md +5 -5
  140. package/src/docs/versioned/next/tasks/billing-stripe.md +75 -0
  141. package/src/docs/versioned/next/tasks/comms-abstraction.md +90 -0
  142. package/src/docs/versioned/next/tasks/comms-adapter-expo-push.md +29 -0
  143. package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +39 -0
  144. package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +39 -0
  145. package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +19 -0
  146. package/src/docs/versioned/next/tasks/comms-admin-dashboard.md +52 -0
  147. package/src/docs/versioned/next/tasks/deploy-to-gcp.md +11 -11
  148. package/src/docs/versioned/next/tasks/deployment-foundation.md +12 -12
  149. package/src/docs/versioned/next/tasks/docs-reference-coverage.md +4 -4
  150. package/src/docs/versioned/next/tasks/infra-mcp.md +43 -0
  151. package/src/docs/versioned/next/tasks/migrate-cicd-to-circleci.md +215 -0
  152. package/src/docs/versioned/next/tasks/native-module-baseline.md +42 -0
  153. package/src/docs/versioned/next/tasks/orgs-and-teams.md +70 -0
  154. package/src/docs/versioned/next/tasks/oss-governance-baseline.md +18 -18
  155. package/src/docs/versioned/next/tasks/password-reset-and-email-verification.md +60 -0
  156. package/src/docs/versioned/next/tasks/positioning-django-rails-universal.md +4 -4
  157. package/src/docs/versioned/next/tasks/public-roadmap-github.md +15 -15
@@ -0,0 +1,323 @@
1
+ # Implementation Plan: Pluggable communications layer (@terreno/comms)
2
+
3
+ **Status:** Draft
4
+ **Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1018
5
+ **Priority:** High
6
+ **Effort:** Big batch
7
+ **Owner:** unassigned
8
+ **Created:** 2026-08-09
9
+ **Program:** [B2B platform](b2b-platform-program.md)
10
+ **Depends on:** none (inbound-webhooks is needed only by adapter delivery-callback phases)
11
+ **RTK deprecation flag:** None — backend-only package
12
+
13
+ ## Goal
14
+
15
+ Terreno backends cannot send email, SMS, or push notifications: there is no mail
16
+ integration at all, `expo-server-sdk` sits unused in `@terreno/api`, and the RTK client's
17
+ `resetPassword` call has no backend to talk to. This IP creates `@terreno/comms`, a new
18
+ workspace package that defines **provider interfaces** for the four outbound channels
19
+ (mail, SMS, push, OTP verification), a **`CommsApp`** TerrenoPlugin that registers
20
+ configured providers and exposes send services to routes and other plugins, **push-token
21
+ registration** routes and model, a **delivery log**, and **console adapters** so every
22
+ channel works in development with zero external accounts.
23
+
24
+ Concrete providers (Twilio, SendGrid, Expo push) are separate IPs — one per adapter — so
25
+ consumer apps install only the SDKs they use.
26
+
27
+ ## Non-Goals
28
+
29
+ - Any concrete provider SDK integration (see `comms-adapter-*` IPs).
30
+ - In-app notification center, inbox UI, or user notification preferences
31
+ (`notification-center` item).
32
+ - Password reset / email verification flows (`password-reset-and-email-verification` item
33
+ — a consumer of this layer).
34
+ - Inbound webhook handling (`inbound-webhooks` item); this IP only defines the
35
+ `DeliveryEvent` type adapters will feed from callbacks.
36
+ - Broadcast/bulk campaign tooling.
37
+
38
+ ## Decisions
39
+
40
+ | Question | Decision |
41
+ |----------|----------|
42
+ | New package or `@terreno/api` module? | New package `@terreno/comms`, mirroring `@terreno/feature-flags` — keeps provider SDKs out of the core dependency tree |
43
+ | How do provider SDKs stay optional? | Adapters live in subpath exports (`@terreno/comms/adapters/<name>`) with their SDKs as `peerDependencies` marked optional; core package has zero provider SDKs |
44
+ | Precedent to follow | `secretProviders.ts` (env + GCP behind one interface) and the OpenFeature provider in `@terreno/feature-flags` |
45
+ | Templates | Minimal: `{subject, text, html}` render helper with variable interpolation; no template builder |
46
+ | Where do send calls log? | `CommsMessage` Mongoose model, mirroring the `AIRequest.logRequest` pattern — logging failures never break the send |
47
+ | Dev experience | `ConsoleMailProvider` / `ConsoleSmsProvider` / `ConsolePushProvider` / `ConsoleVerificationProvider` print to logger and record to `CommsMessage`; default when no provider configured in non-production |
48
+ | Consumer lifecycle hooks | `CommsApp` options expose `beforeSend` / `onSend` / `onError` / `onRetry` / `onOptOut` / `onDeliveryEvent`. Hooks are awaited but wrapped: a throwing hook is logged (`logger.error`) and never breaks the send. `beforeSend` may mutate or cancel |
49
+ | Error taxonomy | Every `SendResult` failure carries `errorCode` (provider-native code as a string) and `errorClass` (`"permanent"` \| `"transient"` \| `"config"`). Adapters own the mapping; the facade uses `errorClass` to decide its single inline retry (transient only) and the admin dashboard uses it to gate manual retries |
50
+ | Payload retention | `retainPayloadDays` option (default 30) keeps the rendered message on the `CommsMessage` row (post `redactPayload` hook) so the admin dashboard can retry; a TTL-style cleanup clears expired payloads |
51
+
52
+ ## Architecture
53
+
54
+ ```
55
+ @terreno/comms
56
+ src/
57
+ types.ts # Provider interfaces + message/result types
58
+ commsApp.ts # CommsApp TerrenoPlugin
59
+ commsService.ts # sendMail/sendSms/sendPush/startVerification/checkVerification
60
+ models/
61
+ commsMessage.ts # Delivery log
62
+ pushToken.ts # Device push token registry
63
+ routes/
64
+ pushTokens.ts # modelRouter for token registration
65
+ commsExplorer.ts # Admin-only delivery log explorer
66
+ adapters/
67
+ console.ts # Dev adapters for all four channels
68
+ templates.ts # renderTemplate({subject, text, html}, data)
69
+ ```
70
+
71
+ ### Provider interfaces (types.ts)
72
+
73
+ ```typescript
74
+ export interface SendResult {
75
+ providerMessageId?: string;
76
+ accepted: boolean;
77
+ error?: string;
78
+ errorCode?: string; // provider-native code, e.g. "21610", "sendgrid-400"
79
+ errorClass?: "permanent" | "transient" | "config"; // drives retry eligibility
80
+ }
81
+
82
+ export interface MailMessage {
83
+ to: string | string[];
84
+ from?: string; // default from CommsApp options
85
+ subject: string;
86
+ text?: string;
87
+ html?: string;
88
+ replyTo?: string;
89
+ metadata?: Record<string, string>;
90
+ }
91
+
92
+ export interface MailProvider {
93
+ readonly id: string; // "console" | "sendgrid" | ...
94
+ sendMail(message: MailMessage): Promise<SendResult>;
95
+ }
96
+
97
+ export interface SmsProvider {
98
+ readonly id: string;
99
+ sendSms(message: {to: string; body: string}): Promise<SendResult>;
100
+ }
101
+
102
+ export interface PushMessage {
103
+ tokens: string[];
104
+ title: string;
105
+ body: string;
106
+ data?: Record<string, unknown>;
107
+ badge?: number;
108
+ sound?: string | null;
109
+ }
110
+
111
+ export interface PushProvider {
112
+ readonly id: string;
113
+ sendPush(message: PushMessage): Promise<SendResult[]>; // one per token
114
+ }
115
+
116
+ export interface VerificationProvider {
117
+ readonly id: string;
118
+ startVerification(options: {to: string; channel: "sms" | "email"}): Promise<SendResult>;
119
+ checkVerification(options: {to: string; code: string}): Promise<{
120
+ valid: boolean;
121
+ error?: string;
122
+ }>;
123
+ }
124
+
125
+ export interface DeliveryEvent {
126
+ channel: "mail" | "sms" | "push";
127
+ providerMessageId: string;
128
+ status: "delivered" | "bounced" | "failed" | "opened";
129
+ errorCode?: string; // provider failure code from the callback, when present
130
+ raw?: unknown;
131
+ }
132
+
133
+ export interface CommsHookContext {
134
+ channel: "mail" | "sms" | "push" | "verification";
135
+ provider: string;
136
+ message: MailMessage | {to: string; body: string} | PushMessage;
137
+ userId?: string;
138
+ isRetry: boolean; // true for facade inline retries and admin-dashboard retries
139
+ messageId?: string; // CommsMessage._id once the log row exists
140
+ }
141
+
142
+ export interface OptOutEvent {
143
+ channel: "mail" | "sms";
144
+ to: string;
145
+ provider: string;
146
+ reason: string; // e.g. "sms-stop", "unsubscribe", "spam-report"
147
+ raw?: unknown;
148
+ }
149
+ ```
150
+
151
+ Verification start attempts store `metadata.verificationChannel` while recipient values remain
152
+ redacted. Verification codes are never persisted.
153
+
154
+ ### CommsApp (TerrenoPlugin)
155
+
156
+ ```typescript
157
+ export interface CommsOptions {
158
+ mail?: MailProvider;
159
+ sms?: SmsProvider;
160
+ push?: PushProvider;
161
+ verification?: VerificationProvider;
162
+ defaultFrom?: string; // mail from fallback
163
+ logMessages?: boolean; // default true → CommsMessage
164
+ retainPayloadDays?: number; // default 30; 0 disables retention (and admin retry)
165
+ redactPayload?: (context: CommsHookContext, payload: unknown) => unknown; // before storage
166
+
167
+ // Lifecycle hooks — all optional, all awaited, all wrapped so a throwing
168
+ // hook is logged and never breaks (or blocks) the send itself.
169
+ beforeSend?: (context: CommsHookContext) =>
170
+ Promise<{cancel?: boolean; message?: CommsHookContext["message"]} | void>;
171
+ onSend?: (context: CommsHookContext, result: SendResult) => Promise<void>;
172
+ onError?: (context: CommsHookContext, result: SendResult) => Promise<void>;
173
+ onRetry?: (context: CommsHookContext, attempt: number) => Promise<void>;
174
+ onOptOut?: (event: OptOutEvent) => Promise<void>;
175
+ onDeliveryEvent?: (event: DeliveryEvent) => Promise<void>;
176
+ }
177
+
178
+ new TerrenoApp({userModel: User})
179
+ .register(new CommsApp({mail: new ConsoleMailProvider(), push: new ExpoPushProvider()}))
180
+ .start();
181
+ ```
182
+
183
+ `CommsApp.register()` mounts the push-token routes and admin explorer;
184
+ `getCommsService()` (module-level accessor, mirroring how `FeatureFlagsApp` exposes
185
+ evaluation) gives routes and other plugins `sendMail` / `sendSms` / `sendPushToUser` /
186
+ `startVerification` / `checkVerification`. `sendPushToUser(userId, message)` resolves the
187
+ user's active `PushToken`s and prunes tokens the provider reports dead.
188
+
189
+ Unconfigured channel behavior: in production, throw `APIError({status: 501, title:
190
+ "Comms channel not configured"})`; in development, fall back to the console adapter with a
191
+ `logger.warn`.
192
+
193
+ ### Hook order and semantics
194
+
195
+ For every send (including retries) the facade runs:
196
+
197
+ 1. `beforeSend` — may replace the message (`{message}`) or cancel (`{cancel: true}`,
198
+ logged as a `CommsMessage` with status `cancelled`). The consuming app uses this for
199
+ quiet hours, per-user notification preferences, and suppression lists.
200
+ 2. Provider `send*` → on transient failure (`errorClass: "transient"`), `onRetry` fires
201
+ and the facade retries once inline.
202
+ 3. `onSend` (accepted) or `onError` (final failure) — `onError` receives the full
203
+ `SendResult` (`errorCode`, `errorClass`) so apps can alert, escalate, or mark domain
204
+ objects without polling the delivery log.
205
+ 4. Asynchronously, adapters feed `onDeliveryEvent` (delivery callbacks) and `onOptOut`
206
+ (STOP replies, unsubscribes, spam reports) once their webhook phases land.
207
+
208
+ Hooks are observation and shaping points, not error channels: exceptions thrown inside a
209
+ hook are caught, logged with `logger.error`, and recorded in `CommsMessage.metadata` —
210
+ they never change the send outcome.
211
+
212
+ ## Models
213
+
214
+ All fields carry `description`; both models use `createdUpdatedPlugin` + `isDeletedPlugin`
215
+ and live per the five-type pattern (`CommsMessageDocument`, etc.).
216
+
217
+ **PushToken** — `userId` (ref User, required, indexed), `token` (string, required, unique),
218
+ `platform` ("ios" | "android" | "web"), `deviceId` (string, optional), `active` (boolean,
219
+ default true), `lastSeenAt` (date).
220
+
221
+ **CommsMessage** — `channel` ("mail" | "sms" | "push" | "verification"), `provider`
222
+ (string), `to` (string, redacted-at-rest for sms/mail per option), `subject` (string,
223
+ optional), `userId` (ref User, optional), `status` ("sent" | "failed" | "delivered" |
224
+ "bounced" | "cancelled"), `providerMessageId` (string, indexed), `error` (string,
225
+ optional), `errorCode` (string, indexed), `errorClass` ("permanent" | "transient" |
226
+ "config"), `templateId` (string, optional), `attempts` (array of `{at, provider,
227
+ providerMessageId, errorCode, error}` — one entry per facade attempt including the inline
228
+ retry), `attemptCount` (number), `lastAttemptAt` (date), `retriedFromId` / `retriedById`
229
+ (ref CommsMessage — links between an original and its admin-dashboard retries), `payload`
230
+ (Mixed, retained per `retainPayloadDays` after `redactPayload`, cleared on expiry),
231
+ `metadata` (Mixed). Static `logSend()` mirrors `AIRequest.logRequest`.
232
+
233
+ These fields exist so the [comms-admin-dashboard](comms-admin-dashboard.md) IP can filter
234
+ on error codes/classes, show per-attempt history, and re-send from the retained payload
235
+ without any further schema changes.
236
+
237
+ ## APIs
238
+
239
+ | Method | Path | Permissions | Notes |
240
+ |---|---|---|---|
241
+ | POST | `/comms/pushTokens` | IsAuthenticated | Upsert by `token`; sets `userId`, `platform`, `lastSeenAt` |
242
+ | GET | `/comms/pushTokens` | IsOwner via queryFilter | List own device tokens |
243
+ | DELETE | `/comms/pushTokens/:id` | IsOwner | Deactivate on logout/uninstall |
244
+ | GET | `/comms/messages` | IsAdmin | Paginated delivery log explorer; filters: `channel`, `status`, `userId`, date range |
245
+
246
+ Push token registration, owner-scoped list, and deactivation use dedicated handlers because
247
+ `modelRouter` create cannot provide idempotent upsert semantics and `OwnerQueryFilter` targets a
248
+ persisted `ownerId` while `PushToken` persists `userId`. Owner read remains on `modelRouter`.
249
+ The explorer uses `createOpenApiBuilder`, modeled on `addAiRequestsExplorerRoutes`, and is
250
+ deliberately minimal here — the [comms-admin-dashboard](comms-admin-dashboard.md) IP upgrades it
251
+ with error-code/class filtering, free-text search, a detail route, stats, and retry endpoints.
252
+
253
+ ## Notifications
254
+
255
+ None beyond the feature itself.
256
+
257
+ ## UI
258
+
259
+ None in this IP. example-frontend gains a settings row that registers the Expo push token
260
+ (client helper already exists in `store/utils.ts`) once the Expo adapter lands.
261
+
262
+ ## Phases
263
+
264
+ 1. **Package + contracts:** `@terreno/comms` package scaffold, `types.ts`, console
265
+ adapters, `CommsMessage` + logging, `commsService`, unit tests.
266
+ 2. **CommsApp + routes:** plugin registration, `PushToken` model + routes, admin explorer,
267
+ OpenAPI, supertest coverage.
268
+ 3. **Integration:** example-backend registers `CommsApp` (console adapters), seed docs,
269
+ `docs/reference/comms.md`, catalog entries, publish wiring in `publish-on-tag.yml`.
270
+
271
+ ## Feature Flags & Migrations
272
+
273
+ None. New collections only; no existing data touched.
274
+
275
+ ## Activity Log & User Updates
276
+
277
+ Every send attempt is a `CommsMessage` row (that is the audit surface). No user-facing
278
+ updates.
279
+
280
+ ## Not Included / Future Work
281
+
282
+ - Adapter IPs: `comms-adapter-expo-push`, `comms-adapter-twilio-sms`,
283
+ `comms-adapter-twilio-verify`, `comms-adapter-sendgrid`.
284
+ - Admin operations dashboard (rich filtering, log digging, manual retries) —
285
+ [comms-admin-dashboard](comms-admin-dashboard.md); this IP only ships the fields and
286
+ hooks it needs.
287
+ - Notification center, user notification preferences (`beforeSend` is the extension
288
+ point they will plug into).
289
+ - Queue-backed sending with automatic scheduled retries — arrives with `job-queues`;
290
+ until then sends are inline with one transient-error retry plus manual admin retries.
291
+
292
+ ## Files to Create / Modify
293
+
294
+ - `comms/` (new workspace package: `package.json`, `tsconfig.json`, `biome.jsonc`, `src/*`
295
+ as in Architecture)
296
+ - Root `package.json` — workspace entry + scripts (`comms:compile|lint|test`)
297
+ - `example-backend/src/server.ts` — register `CommsApp`
298
+ - `docs/reference/comms.md`, `.cursor/rules/comms/00-comms.mdc` (via rulesync)
299
+ - `.github/workflows/publish-on-tag.yml`, new `comms-ci` workflow
300
+
301
+ ## Task List
302
+
303
+ See [docs/tasks/comms-abstraction.md](../tasks/comms-abstraction.md).
304
+
305
+ ## Acceptance Criteria
306
+
307
+ - [x] A Terreno app with only console adapters can call `sendMail`, `sendSms`,
308
+ `sendPushToUser`, and `startVerification`/`checkVerification` in development, each
309
+ producing a logger line and a `CommsMessage` row.
310
+ - [ ] `beforeSend` can cancel and mutate; `onSend`/`onError` fire with the final
311
+ `SendResult`; `onRetry` fires on the inline transient retry — and a hook that throws
312
+ is logged without changing the send outcome (test per hook).
313
+ - [ ] Failed sends record `errorCode` and `errorClass` on both the `SendResult` and the
314
+ `CommsMessage` row; only `transient` failures trigger the inline retry.
315
+ - [ ] `CommsMessage.attempts` holds one entry per attempt with per-attempt error data;
316
+ `payload` is retained post-redaction and cleared after `retainPayloadDays`.
317
+ - [x] `POST /comms/pushTokens` registers a device token for the authenticated user;
318
+ re-posting the same token updates rather than duplicates; other users cannot list or
319
+ delete it.
320
+ - [x] `GET /comms/messages` is admin-only, paginated, and filterable by channel and status.
321
+ - [x] Production apps with an unconfigured channel get a 501 `APIError`, not a silent no-op.
322
+ - [x] `@terreno/comms` core has zero provider SDKs in `dependencies`.
323
+ - [x] All routes appear in `/openapi.json` and the generated SDK compiles.
@@ -0,0 +1,107 @@
1
+ # Implementation Plan: Comms adapter — Expo push notifications
2
+
3
+ **Status:** Draft
4
+ **Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1019
5
+ **Priority:** High
6
+ **Effort:** Small batch
7
+ **Owner:** unassigned
8
+ **Created:** 2026-08-09
9
+ **Program:** [B2B platform](b2b-platform-program.md)
10
+ **Depends on:** [comms-abstraction](comms-abstraction.md)
11
+ **RTK deprecation flag:** None — backend adapter; client token registration already exists
12
+
13
+ ## Goal
14
+
15
+ Implement `PushProvider` on Expo's push service so `sendPushToUser` delivers real
16
+ notifications to iOS/Android devices. `expo-server-sdk` is already a dependency of
17
+ `@terreno/api` (currently unused — it moves to this adapter's peer deps); the client-side
18
+ token fetch (`getExpoPushTokenAsync`) already exists in `example-frontend/store/utils.ts`.
19
+
20
+ ## Non-Goals
21
+
22
+ - Direct FCM/APNs integration (future adapter if demand appears).
23
+ - Notification center / inbox UI.
24
+ - Rich notification content (images, actions) beyond title/body/data/badge/sound.
25
+
26
+ ## Decisions
27
+
28
+ | Question | Decision |
29
+ |----------|----------|
30
+ | SDK | `expo-server-sdk` as optional peer of `@terreno/comms` (subpath `adapters/expoPush`); removed from `@terreno/api` deps |
31
+ | Receipts | Poll receipts after send (Expo's required pattern); surface as `DeliveryEvent`s to `onDeliveryEvent` |
32
+ | Dead tokens | `DeviceNotRegistered` receipt → `SendResult.accepted: false` with permanent-failure marker so core pruning deactivates the `PushToken` |
33
+ | Chunking | Use the SDK's `chunkPushNotifications`; one `SendResult` per token preserved through chunks |
34
+
35
+ ## Architecture
36
+
37
+ ```typescript
38
+ // @terreno/comms/adapters/expoPush
39
+ export class ExpoPushProvider implements PushProvider {
40
+ readonly id = "expo";
41
+ constructor(options?: {accessToken?: string; receiptPollDelayMs?: number});
42
+ sendPush(message: PushMessage): Promise<SendResult[]>;
43
+ }
44
+ ```
45
+
46
+ Send flow: validate tokens with `Expo.isExpoPushToken` → chunk → send → collect tickets →
47
+ schedule one receipt poll (default 15 min, configurable; inline timer until `job-queues`
48
+ lands) → map receipt errors to `DeliveryEvent`s and permanent-failure results.
49
+
50
+ ## Models
51
+
52
+ None new — uses `PushToken` and `CommsMessage` from the abstraction.
53
+
54
+ ## APIs
55
+
56
+ None new.
57
+
58
+ ## Notifications
59
+
60
+ This is the notification plumbing itself.
61
+
62
+ ## UI
63
+
64
+ example-frontend: register the push token on login via `POST /comms/pushTokens` (guarded by
65
+ `expo-device` physical-device check once the native baseline lands) and a profile-screen
66
+ test-send row in dev builds.
67
+
68
+ ## Phases
69
+
70
+ Single phase.
71
+
72
+ ## Feature Flags & Migrations
73
+
74
+ None.
75
+
76
+ ## Activity Log & User Updates
77
+
78
+ Sends and receipt failures logged to `CommsMessage`.
79
+
80
+ ## Not Included / Future Work
81
+
82
+ - Queue-backed receipt polling (moves to `job-queues` when available).
83
+ - Web push (Expo tokens cover native only; web is a future adapter).
84
+
85
+ ## Files to Create / Modify
86
+
87
+ - `comms/src/adapters/expoPush.ts` + tests (mock `Expo` client)
88
+ - `comms/package.json` — optional peer `expo-server-sdk`
89
+ - `api/package.json` — remove unused `expo-server-sdk`
90
+ - `example-frontend/app/(tabs)/profile.tsx`, `store/utils.ts` — token registration wiring
91
+ - `docs/reference/comms.md` — adapter section
92
+
93
+ ## Task List
94
+
95
+ See [docs/tasks/comms-adapter-expo-push.md](../tasks/comms-adapter-expo-push.md).
96
+
97
+ ## Acceptance Criteria
98
+
99
+ - [ ] With a mocked Expo client, `sendPush` returns one `SendResult` per token across chunk
100
+ boundaries.
101
+ - [ ] A `DeviceNotRegistered` receipt deactivates the corresponding `PushToken` via core
102
+ pruning.
103
+ - [ ] Invalid tokens are rejected before hitting the SDK and logged as failed
104
+ `CommsMessage` rows.
105
+ - [ ] `@terreno/api` no longer lists `expo-server-sdk`; apps not using the adapter install
106
+ nothing new.
107
+ - [ ] example-frontend registers its token after login on a physical device build.
@@ -0,0 +1,157 @@
1
+ # Implementation Plan: Comms adapter — transactional email (SendGrid)
2
+
3
+ **Status:** Draft
4
+ **Roadmap issue:** https://github.com/FlourishHealth/terreno/issues/1022
5
+ **Priority:** High
6
+ **Effort:** Small batch
7
+ **Owner:** unassigned
8
+ **Created:** 2026-08-09
9
+ **Program:** [B2B platform](b2b-platform-program.md)
10
+ **Depends on:** [comms-abstraction](comms-abstraction.md); inbound-webhooks (Phase 2 only)
11
+ **RTK deprecation flag:** None — backend only
12
+
13
+ ## Goal
14
+
15
+ Implement `MailProvider` on Twilio SendGrid (decision D2 — first mail adapter, sharing the
16
+ Twilio account story with the SMS and Verify adapters) so `sendMail` delivers real
17
+ transactional email. This unblocks `password-reset-and-email-verification` and
18
+ `invitations-and-seats`.
19
+
20
+ ## Non-Goals
21
+
22
+ - Other mail providers (Resend/SES/SMTP — items when demand appears).
23
+ - Marketing campaigns, contact lists, SendGrid dynamic template management.
24
+ - Inbound email parsing.
25
+
26
+ ## Decisions
27
+
28
+ | Question | Decision |
29
+ |----------|----------|
30
+ | SDK | `@sendgrid/mail` as optional peer, subpath `@terreno/comms/adapters/sendgrid` |
31
+ | Auth | `SENDGRID_API_KEY` env or constructor option; fail fast at registration when missing |
32
+ | From address | Constructor `fromEmail`/`fromName` falling back to `CommsApp.defaultFrom`; sender verification is a documented SendGrid prerequisite, not automated |
33
+ | Templates | Terreno-side rendering (`templates.ts` from the abstraction) sent as `text`/`html`; SendGrid dynamic templates supported pass-through via `templateId` + `dynamicTemplateData` but not required |
34
+ | Sandbox | SendGrid `mail_settings.sandbox_mode` toggle for CI/e2e so tests never send |
35
+ | Delivery events | Phase 2: SendGrid Event Webhook (delivered/deferred/bounce/dropped/spamreport/unsubscribe/open) with ECDSA signature verification via the inbound-webhooks framework, mapped to `DeliveryEvent`s / `OptOutEvent`s |
36
+ | Error classification | Adapter owns the HTTP-status + event-type → `errorClass` map (table below); 429/5xx are `transient`, other 4xx `permanent`, 401/403 `config` |
37
+ | Suppression / opt-outs | Bounces, spam reports, and unsubscribes emit `OptOutEvent` → consumer `onOptOut`. SendGrid maintains its own suppression lists; a send to a suppressed address comes back as a `dropped` event with the suppression reason, classified `permanent` |
38
+ | Admin dashboard support | Every `CommsMessage` row stores `errorCode`/`errorClass`, the `x-message-id`, and a `metadata.consoleUrl` deep link to SendGrid Email Activity for log digging |
39
+
40
+ ## Architecture
41
+
42
+ ```typescript
43
+ // @terreno/comms/adapters/sendgrid
44
+ export class SendGridMailProvider implements MailProvider {
45
+ readonly id = "sendgrid";
46
+ constructor(options?: {
47
+ apiKey?: string; // default env SENDGRID_API_KEY
48
+ fromEmail?: string;
49
+ fromName?: string;
50
+ sandboxMode?: boolean; // default true when NODE_ENV === "test"
51
+ });
52
+ sendMail(message: MailMessage): Promise<SendResult>;
53
+ }
54
+ ```
55
+
56
+ Error mapping: SendGrid 4xx (bad address, unverified sender) → `accepted: false` with the
57
+ response body's first error message in `SendResult.error` and full body in
58
+ `CommsMessage.metadata`; 429/5xx classified `transient` and retried once by the core
59
+ facade. The provider never throws through `sendMail`.
60
+
61
+ ### Error classification
62
+
63
+ Send-time failures classify by HTTP status; post-acceptance failures arrive as events
64
+ (Phase 2) and reclassify the `CommsMessage`.
65
+
66
+ | Signal | Meaning | `errorCode` | `errorClass` |
67
+ |---|---|---|---|
68
+ | 400 (bad address, missing field), 413 | Request is wrong | `sendgrid-<status>` + first body error | `permanent` |
69
+ | 401, 403 (bad key, unverified sender) | App misconfiguration | `sendgrid-<status>` | `config` (also `logger.error` at send time) |
70
+ | 429 | Rate limited | `sendgrid-429` | `transient` |
71
+ | 5xx / network errors | SendGrid-side or transport failure | `sendgrid-<status>` | `transient` |
72
+ | Event: `bounce` (hard) | Mailbox permanently rejects | `bounce` + reason | `permanent` |
73
+ | Event: `bounce` (soft) / `deferred` | Temporary mailbox/receiver issue | `deferred` | `transient` |
74
+ | Event: `dropped` | SendGrid suppressed (prior bounce, spam report, unsubscribe, invalid) | `dropped` + reason | `permanent` |
75
+ | Event: `spamreport`, `unsubscribe`, `group_unsubscribe` | Recipient opted out | event type | `permanent` (also emits `OptOutEvent`) |
76
+
77
+ Permanent-class failures are never auto-retried and show as non-retryable in the admin
78
+ dashboard with the reason (retrying a suppressed or unsubscribed address damages sender
79
+ reputation); transient-class failures retry once inline and stay manually retryable.
80
+
81
+ ### Event webhook mapping (Phase 2)
82
+
83
+ | SendGrid event | `DeliveryEvent.status` | `CommsMessage.status` |
84
+ |---|---|---|
85
+ | `processed` | — | `sent` (unchanged) |
86
+ | `delivered` | `delivered` | `delivered` |
87
+ | `deferred` | — (attempt noted in metadata) | `sent` (SendGrid keeps retrying) |
88
+ | `bounce`, `dropped` | `bounced` / `failed` (+ reason as `errorCode`) | `bounced` / `failed` |
89
+ | `open` | `opened` | unchanged (recorded in metadata) |
90
+ | `spamreport`, `unsubscribe`, `group_unsubscribe` | — | unchanged; emits `OptOutEvent` → `onOptOut` |
91
+
92
+ Events correlate to rows via the `x-message-id` response header captured at send time
93
+ (SendGrid appends per-recipient suffixes; match on the prefix).
94
+
95
+ ## Models / APIs / Notifications / UI
96
+
97
+ None new. Phase 2 adds one webhook route via the inbound-webhooks framework.
98
+
99
+ ## Phases
100
+
101
+ 1. **Send path:** provider, config resolution, sandbox mode, error classification,
102
+ `x-message-id` capture + Email Activity deep link, mocked-client tests;
103
+ example-backend env-gated registration; docs (including the sender-verification
104
+ prerequisite checklist).
105
+ 2. **Delivery events + opt-outs** *(gated on inbound-webhooks IP)*: Event Webhook route
106
+ with ECDSA signature verification; event mapping per the table →
107
+ `DeliveryEvent`/`OptOutEvent` → `CommsMessage.status` + consumer hooks.
108
+
109
+ ## Feature Flags & Migrations
110
+
111
+ None.
112
+
113
+ ## Activity Log & User Updates
114
+
115
+ All sends logged to `CommsMessage` with provider `sendgrid`.
116
+
117
+ ## Not Included / Future Work
118
+
119
+ - Suppression-list *management* (viewing/clearing SendGrid suppressions from admin) — the
120
+ adapter only reports suppression outcomes; a management surface can extend the
121
+ [comms-admin-dashboard](comms-admin-dashboard.md) later.
122
+ - Unsubscribe-group configuration.
123
+ - Batch sending beyond the multiple-recipient `to` array.
124
+
125
+ ## Files to Create / Modify
126
+
127
+ - `comms/src/adapters/sendgrid.ts` + tests
128
+ - `comms/package.json` — optional peer `@sendgrid/mail`
129
+ - `example-backend/src/server.ts` — env-gated registration
130
+ - `docs/reference/comms.md`, `docs/reference/environment-variables.md`
131
+
132
+ ## Task List
133
+
134
+ See [docs/tasks/comms-adapter-sendgrid.md](../tasks/comms-adapter-sendgrid.md).
135
+
136
+ ## Acceptance Criteria
137
+
138
+ - [ ] `sendMail` delivers text/html mail with from-address fallback chain (provider option
139
+ → `CommsApp.defaultFrom`), and passes `templateId`/`dynamicTemplateData` through when
140
+ given.
141
+ - [ ] Sandbox mode is on by default under test and produces accepted results without real
142
+ sends.
143
+ - [ ] SendGrid errors surface as `accepted: false` with the SendGrid error message and a
144
+ correct `errorCode`/`errorClass` per the classification table; nothing throws
145
+ through the facade.
146
+ - [ ] 401/403 classify as `config` and `logger.error` at send time; 429/5xx classify as
147
+ `transient` and retry once; other 4xx are `permanent` and never retried.
148
+ - [ ] Each accepted send captures the `x-message-id` and stores a `metadata.consoleUrl`
149
+ Email Activity deep link.
150
+ - [ ] Consumer `onError` fires with the classified `SendResult` on every failed send
151
+ (mocked-client test).
152
+ - [ ] Missing API key fails fast at `CommsApp` registration with a clear error.
153
+ - [ ] Apps not using the adapter do not install `@sendgrid/mail`.
154
+ - [ ] (Phase 2) Event webhook updates `CommsMessage.status` per the mapping table only
155
+ for signature-verified payloads; bounce/dropped reasons land as `errorCode`.
156
+ - [ ] (Phase 2) `spamreport`/`unsubscribe` events fire `onOptOut`; later sends to the
157
+ suppressed address record `dropped` as `permanent`.