sently 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +39 -34
  4. package/SECURITY.md +59 -0
  5. package/dist/adapters/bun.js +1 -1
  6. package/dist/adapters/cf.js +2 -2
  7. package/dist/adapters/cf.js.map +1 -1
  8. package/dist/adapters/deno.js +1 -1
  9. package/dist/adapters/node.js +1 -1
  10. package/dist/auth/oauth2.js +2 -2
  11. package/dist/auth/oauth2.js.map +1 -1
  12. package/dist/channel-result.d.ts +6 -0
  13. package/dist/channel-result.js +1 -0
  14. package/dist/{chunk-skqhj0rm.js → chunk-1ke3nmyg.js} +3 -3
  15. package/dist/{chunk-skqhj0rm.js.map → chunk-1ke3nmyg.js.map} +1 -1
  16. package/dist/{chunk-7gy2q9sh.js → chunk-5es0v1j7.js} +2 -2
  17. package/dist/{chunk-7gy2q9sh.js.map → chunk-5es0v1j7.js.map} +1 -1
  18. package/dist/{chunk-0qxws3kj.js → chunk-7wtfjx0d.js} +2 -2
  19. package/dist/{chunk-0qxws3kj.js.map → chunk-7wtfjx0d.js.map} +1 -1
  20. package/dist/chunk-8kpgbrba.js +4 -0
  21. package/dist/{chunk-93vqxxj2.js.map → chunk-8kpgbrba.js.map} +2 -2
  22. package/dist/{chunk-vjpds2pf.js → chunk-a057jzsy.js} +2 -2
  23. package/dist/{chunk-vjpds2pf.js.map → chunk-a057jzsy.js.map} +1 -1
  24. package/dist/{chunk-8njyga62.js → chunk-bhr7w3gg.js} +2 -2
  25. package/dist/{chunk-8njyga62.js.map → chunk-bhr7w3gg.js.map} +1 -1
  26. package/dist/chunk-cs1tvehn.js +4 -0
  27. package/dist/chunk-cs1tvehn.js.map +10 -0
  28. package/dist/{chunk-29z7fkzy.js → chunk-d5aw5vag.js} +3 -3
  29. package/dist/{chunk-29z7fkzy.js.map → chunk-d5aw5vag.js.map} +1 -1
  30. package/dist/chunk-f5x8vesr.js +4 -0
  31. package/dist/{chunk-b602dhck.js.map → chunk-f5x8vesr.js.map} +2 -2
  32. package/dist/chunk-fv0bemgf.js +4 -0
  33. package/dist/{chunk-yp311efr.js.map → chunk-fv0bemgf.js.map} +3 -3
  34. package/dist/{chunk-ywq6s10g.js → chunk-hnzmn4s4.js} +2 -2
  35. package/dist/{chunk-ywq6s10g.js.map → chunk-hnzmn4s4.js.map} +1 -1
  36. package/dist/chunk-htdg075t.js +4 -0
  37. package/dist/{chunk-qtkd5bak.js.map → chunk-htdg075t.js.map} +4 -3
  38. package/dist/{chunk-0gqy32pe.js → chunk-q2mahnb7.js} +3 -3
  39. package/dist/chunk-q2mahnb7.js.map +10 -0
  40. package/dist/chunk-sbgt4165.js +4 -0
  41. package/dist/chunk-sbgt4165.js.map +10 -0
  42. package/dist/{chunk-6npp3x3c.js → chunk-th4cwrpb.js} +2 -2
  43. package/dist/{chunk-6npp3x3c.js.map → chunk-th4cwrpb.js.map} +1 -1
  44. package/dist/{chunk-tamww15j.js → chunk-ttbwyxmh.js} +2 -2
  45. package/dist/{chunk-tamww15j.js.map → chunk-ttbwyxmh.js.map} +1 -1
  46. package/dist/chunk-w4c9kj70.js +13 -0
  47. package/dist/{chunk-n0qeyzqm.js.map → chunk-w4c9kj70.js.map} +2 -2
  48. package/dist/chunk-w9sabmb0.js +4 -0
  49. package/dist/chunk-w9sabmb0.js.map +10 -0
  50. package/dist/{chunk-8dty8c4p.js → chunk-yg3kj723.js} +3 -3
  51. package/dist/{chunk-8dty8c4p.js.map → chunk-yg3kj723.js.map} +2 -2
  52. package/dist/chunk-z1589fjk.js +4 -0
  53. package/dist/chunk-z1589fjk.js.map +10 -0
  54. package/dist/core/channel-result.d.ts +36 -0
  55. package/dist/core/channel-result.js +3 -0
  56. package/dist/core/channel-result.js.map +10 -0
  57. package/dist/core/decorated-transport.d.ts +30 -0
  58. package/dist/core/decorator-hooks.d.ts +17 -0
  59. package/dist/core/errors.js +2 -2
  60. package/dist/core/errors.js.map +1 -1
  61. package/dist/core/provider-label.d.ts +7 -3
  62. package/dist/core/push-endpoint.d.ts +5 -0
  63. package/dist/core/push-types.d.ts +48 -14
  64. package/dist/core/sms-types.d.ts +12 -0
  65. package/dist/core/smtp.js +2 -2
  66. package/dist/core/smtp.js.map +1 -1
  67. package/dist/core/whatsapp-types.d.ts +12 -0
  68. package/dist/detect.js +2 -2
  69. package/dist/detect.js.map +1 -1
  70. package/dist/dkim.js +2 -2
  71. package/dist/dkim.js.map +2 -2
  72. package/dist/idempotency.js +2 -2
  73. package/dist/idempotency.js.map +1 -1
  74. package/dist/index.d.ts +4 -0
  75. package/dist/index.js +5 -0
  76. package/dist/mailer.js +2 -2
  77. package/dist/mailer.js.map +1 -1
  78. package/dist/observability/console.js +1 -1
  79. package/dist/plugins/react.js +2 -2
  80. package/dist/plugins/react.js.map +1 -1
  81. package/dist/plugins/template.js +1 -1
  82. package/dist/pool/pool.js +2 -2
  83. package/dist/pool/pool.js.map +1 -1
  84. package/dist/push.d.ts +1 -0
  85. package/dist/push.js +2 -2
  86. package/dist/push.js.map +3 -3
  87. package/dist/sms.d.ts +1 -0
  88. package/dist/sms.js +2 -2
  89. package/dist/sms.js.map +3 -3
  90. package/dist/smtp-mailer.js +2 -2
  91. package/dist/smtp-mailer.js.map +1 -1
  92. package/dist/transports/brevo.js +2 -2
  93. package/dist/transports/brevo.js.map +2 -2
  94. package/dist/transports/cloudflare-email.js +2 -2
  95. package/dist/transports/cloudflare-email.js.map +2 -2
  96. package/dist/transports/fallback.d.ts +32 -6
  97. package/dist/transports/fallback.js +2 -2
  98. package/dist/transports/fallback.js.map +1 -1
  99. package/dist/transports/fcm.d.ts +55 -0
  100. package/dist/transports/fcm.js +4 -0
  101. package/dist/transports/fcm.js.map +10 -0
  102. package/dist/transports/loops.js +2 -2
  103. package/dist/transports/loops.js.map +1 -1
  104. package/dist/transports/mailersend.js +2 -2
  105. package/dist/transports/mailersend.js.map +2 -2
  106. package/dist/transports/mailgun.js +2 -2
  107. package/dist/transports/mailgun.js.map +2 -2
  108. package/dist/transports/mailpit.d.ts +166 -0
  109. package/dist/transports/mailpit.js +3 -0
  110. package/dist/transports/mailpit.js.map +10 -0
  111. package/dist/transports/mailtrap.js +2 -2
  112. package/dist/transports/mailtrap.js.map +2 -2
  113. package/dist/transports/msegat.js +2 -2
  114. package/dist/transports/msegat.js.map +1 -1
  115. package/dist/transports/plunk.js +2 -2
  116. package/dist/transports/plunk.js.map +1 -1
  117. package/dist/transports/postmark.js +2 -2
  118. package/dist/transports/postmark.js.map +2 -2
  119. package/dist/transports/preview.js +2 -2
  120. package/dist/transports/preview.js.map +1 -1
  121. package/dist/transports/resend.js +2 -2
  122. package/dist/transports/resend.js.map +2 -2
  123. package/dist/transports/retry.d.ts +28 -9
  124. package/dist/transports/retry.js +2 -2
  125. package/dist/transports/retry.js.map +3 -3
  126. package/dist/transports/sendgrid.js +2 -2
  127. package/dist/transports/sendgrid.js.map +2 -2
  128. package/dist/transports/ses.js +2 -2
  129. package/dist/transports/ses.js.map +2 -2
  130. package/dist/transports/smtp.js +2 -2
  131. package/dist/transports/smtp.js.map +1 -1
  132. package/dist/transports/sndr.js +2 -2
  133. package/dist/transports/sndr.js.map +1 -1
  134. package/dist/transports/sparkpost.js +2 -2
  135. package/dist/transports/sparkpost.js.map +2 -2
  136. package/dist/transports/taqnyat-mail.js +2 -2
  137. package/dist/transports/taqnyat-mail.js.map +1 -1
  138. package/dist/transports/taqnyat-sms.js +2 -2
  139. package/dist/transports/taqnyat-sms.js.map +1 -1
  140. package/dist/transports/taqnyat-whatsapp.js +2 -2
  141. package/dist/transports/taqnyat-whatsapp.js.map +1 -1
  142. package/dist/transports/twilio-sms.js +2 -2
  143. package/dist/transports/twilio-sms.js.map +2 -2
  144. package/dist/transports/unifonic.d.ts +81 -0
  145. package/dist/transports/unifonic.js +3 -0
  146. package/dist/transports/unifonic.js.map +10 -0
  147. package/dist/transports/webpush.js +2 -2
  148. package/dist/transports/webpush.js.map +3 -3
  149. package/dist/transports/weighted-fallback.d.ts +11 -9
  150. package/dist/transports/weighted-fallback.js +2 -2
  151. package/dist/transports/weighted-fallback.js.map +3 -3
  152. package/dist/transports/whatsapp-cloud.js +2 -2
  153. package/dist/transports/whatsapp-cloud.js.map +1 -1
  154. package/dist/webhooks/brevo.js +2 -2
  155. package/dist/webhooks/brevo.js.map +1 -1
  156. package/dist/webhooks/mailgun.js +2 -2
  157. package/dist/webhooks/mailgun.js.map +1 -1
  158. package/dist/webhooks/postmark.js +2 -2
  159. package/dist/webhooks/postmark.js.map +1 -1
  160. package/dist/webhooks/resend.js +2 -2
  161. package/dist/webhooks/resend.js.map +1 -1
  162. package/dist/webhooks/sendgrid.js +2 -2
  163. package/dist/webhooks/sendgrid.js.map +1 -1
  164. package/dist/webhooks/ses.js +2 -2
  165. package/dist/webhooks/ses.js.map +1 -1
  166. package/dist/webhooks/sndr.js +2 -2
  167. package/dist/webhooks/sndr.js.map +1 -1
  168. package/dist/webhooks/timing-safe-equal.js +1 -1
  169. package/dist/webhooks/twilio-sms.d.ts +20 -0
  170. package/dist/webhooks/twilio-sms.js +3 -0
  171. package/dist/webhooks/twilio-sms.js.map +10 -0
  172. package/dist/webhooks/types.d.ts +37 -3
  173. package/dist/webhooks/types.js +3 -0
  174. package/dist/webhooks/types.js.map +9 -0
  175. package/dist/webhooks/unifonic.d.ts +13 -0
  176. package/dist/webhooks/unifonic.js +3 -0
  177. package/dist/webhooks/unifonic.js.map +10 -0
  178. package/dist/webhooks/whatsapp-cloud.d.ts +14 -0
  179. package/dist/webhooks/whatsapp-cloud.js +3 -0
  180. package/dist/webhooks/whatsapp-cloud.js.map +10 -0
  181. package/dist/webhooks.d.ts +10 -5
  182. package/dist/webhooks.js +11 -0
  183. package/dist/whatsapp.d.ts +1 -0
  184. package/dist/whatsapp.js +2 -2
  185. package/dist/whatsapp.js.map +3 -3
  186. package/package.json +39 -2
  187. package/site/content/docs/ai/llms-txt.mdx +47 -2
  188. package/site/content/docs/channels/email.mdx +3 -0
  189. package/site/content/docs/channels/hooks.mdx +6 -5
  190. package/site/content/docs/channels/index.mdx +5 -3
  191. package/site/content/docs/channels/push.mdx +31 -21
  192. package/site/content/docs/channels/sms.mdx +4 -1
  193. package/site/content/docs/decorators/fallback.mdx +45 -13
  194. package/site/content/docs/decorators/index.mdx +31 -13
  195. package/site/content/docs/decorators/preview.mdx +52 -9
  196. package/site/content/docs/decorators/retry.mdx +32 -9
  197. package/site/content/docs/decorators/weighted-fallback.mdx +27 -6
  198. package/site/content/docs/get-started/entrypoints.mdx +11 -8
  199. package/site/content/docs/get-started/index.mdx +8 -2
  200. package/site/content/docs/get-started/installation.mdx +3 -2
  201. package/site/content/docs/get-started/introduction.mdx +7 -3
  202. package/site/content/docs/get-started/meta.json +4 -1
  203. package/site/content/docs/get-started/migrate-nodemailer.mdx +132 -5
  204. package/site/content/docs/get-started/non-goals.mdx +57 -0
  205. package/site/content/docs/get-started/stability.mdx +69 -0
  206. package/site/content/docs/get-started/support-matrix.mdx +62 -0
  207. package/site/content/docs/guides/compare.mdx +87 -0
  208. package/site/content/docs/guides/failover.mdx +121 -0
  209. package/site/content/docs/guides/index.mdx +2 -0
  210. package/site/content/docs/guides/meta.json +2 -0
  211. package/site/content/docs/guides/security.mdx +38 -8
  212. package/site/content/docs/guides/webhooks.mdx +35 -20
  213. package/site/content/docs/index.mdx +78 -4
  214. package/site/content/docs/meta.json +0 -1
  215. package/site/content/docs/reference/channel-result.mdx +82 -0
  216. package/site/content/docs/reference/exports.mdx +9 -4
  217. package/site/content/docs/reference/meta.json +1 -0
  218. package/site/content/docs/reference/push-options.mdx +58 -3
  219. package/site/content/docs/reference/sms-options.mdx +18 -2
  220. package/site/content/docs/reference/webhook-events.mdx +38 -14
  221. package/site/content/docs/reference/whatsapp-options.mdx +19 -1
  222. package/site/content/docs/transports/fcm.mdx +84 -0
  223. package/site/content/docs/transports/index.mdx +11 -3
  224. package/site/content/docs/transports/mailpit.mdx +123 -0
  225. package/site/content/docs/transports/meta.json +4 -1
  226. package/site/content/docs/transports/smtp.mdx +61 -22
  227. package/site/content/docs/transports/unifonic.mdx +85 -0
  228. package/dist/chunk-0gqy32pe.js.map +0 -10
  229. package/dist/chunk-5915vbcj.js +0 -4
  230. package/dist/chunk-5915vbcj.js.map +0 -10
  231. package/dist/chunk-93vqxxj2.js +0 -4
  232. package/dist/chunk-b602dhck.js +0 -4
  233. package/dist/chunk-gp4fwrs8.js +0 -4
  234. package/dist/chunk-gp4fwrs8.js.map +0 -10
  235. package/dist/chunk-n0qeyzqm.js +0 -13
  236. package/dist/chunk-q8k3m6qe.js +0 -4
  237. package/dist/chunk-q8k3m6qe.js.map +0 -10
  238. package/dist/chunk-qtkd5bak.js +0 -4
  239. package/dist/chunk-yp311efr.js +0 -4
  240. package/site/content/docs/quick-start/email.mdx +0 -40
  241. package/site/content/docs/quick-start/index.mdx +0 -15
  242. package/site/content/docs/quick-start/meta.json +0 -5
  243. package/site/content/docs/quick-start/push.mdx +0 -36
  244. package/site/content/docs/quick-start/sms.mdx +0 -36
  245. package/site/content/docs/quick-start/whatsapp.mdx +0 -38
@@ -7,6 +7,9 @@
7
7
  "installation",
8
8
  "entrypoints",
9
9
  "runtimes",
10
- "migrate-nodemailer"
10
+ "migrate-nodemailer",
11
+ "stability",
12
+ "support-matrix",
13
+ "non-goals"
11
14
  ]
12
15
  }
@@ -2,22 +2,149 @@
2
2
  title: Migrate from Nodemailer
3
3
  description: Move SMTP configuration and message fields to sently incrementally.
4
4
  icon: ArrowRightLeft
5
- source: "README.md"
5
+ source: "src/smtp-mailer.ts"
6
6
  ---
7
7
 
8
- Keep your message object, replace transporter construction, and await the sently factory.
8
+ Keep your welcome email, receipt, or password-reset fields (`from`, `to`, `subject`, `text` / `html`).
9
+ Replace `createTransport` with `createSMTPMailer`, rename `sendMail` to `send`, and `await` the factory before the first send.
10
+
11
+ <Callout title="The one rule">
12
+ Use `createSMTPMailer` for relay host/port/auth. Use `createMailer` only when you already have an explicit transport.
13
+ </Callout>
14
+
15
+ ## Quick start
16
+
17
+ <Steps>
18
+ <Step title="Create the mailer">
9
19
 
10
20
  ```ts
11
21
  import { createSMTPMailer } from "sently/smtp";
12
22
 
13
23
  const mailer = await createSMTPMailer({
14
24
  host: "smtp.example.com",
25
+ port: 587,
15
26
  auth: { user: "user@example.com", pass: process.env.SMTP_PASSWORD! },
16
27
  });
17
28
  ```
18
29
 
19
- <Callout title="The one rule">Use `createSMTPMailer` for relay configuration; use `createMailer` only with an explicit transport.</Callout>
30
+ </Step>
31
+ <Step title="Send with the same fields">
32
+
33
+ ```diff
34
+ - const info = await transporter.sendMail({
35
+ + const result = await mailer.send({
36
+ from: "Acme <hello@example.com>",
37
+ to: "person@example.com",
38
+ subject: "Welcome",
39
+ text: "Thanks for joining.",
40
+ });
41
+ ```
42
+
43
+ </Step>
44
+ <Step title="Read the result">
45
+
46
+ ```ts
47
+ console.log(result.messageId, result.accepted, result.rejected);
48
+ ```
49
+
50
+ </Step>
51
+ </Steps>
52
+
53
+ ## API map
54
+
55
+ | Nodemailer | sently |
56
+ | --- | --- |
57
+ | `createTransport({ host, port, auth })` | `await createSMTPMailer({ host, port, auth })` |
58
+ | `transporter.sendMail(msg)` | `mailer.send(msg)` |
59
+ | `transporter.verify()` | `mailer.verify()` |
60
+ | `transporter.close()` | `mailer.close()` |
61
+ | `info.messageId` / `accepted` / `rejected` / `response` / `envelope` | Same fields on `SendResult` |
62
+ | HTTP providers via plugins / custom | `createMailer` + `sently/transports/<provider>` |
63
+
64
+ ## Message fields
65
+
66
+ | Nodemailer | sently |
67
+ | --- | --- |
68
+ | `from` / `to` / `cc` / `bcc` / `replyTo` | Same names on `MailOptions` |
69
+ | `subject`, `text`, `html`, `headers`, `messageId`, `date` | Same names |
70
+ | `priority` | `"high" \| "normal" \| "low"` |
71
+ | `attachments[].filename` / `content` / `path` / `contentType` | Same names |
72
+ | `attachments[].cid` | `attachments[].contentId` |
73
+ | `attachments[].contentDisposition: "inline"` | `attachments[].inline: true` |
74
+ | `icalEvent`, SOCKS proxy options | Not supported — see [Non-goals](/docs/get-started/non-goals) |
75
+
76
+ ## SMTP options
77
+
78
+ | Option | Type | Default | Meaning |
79
+ | --- | --- | --- | --- |
80
+ | `host` | `string` | required | Relay hostname |
81
+ | `port` | `number` | `587` (`465` if `secure`) | SMTP port |
82
+ | `secure` | `boolean` | `false` | Implicit TLS on connect |
83
+ | `auth` | `SMTPAuth` | — | `{ user, pass?, type?, oauth2? }` |
84
+ | `pool` | `boolean` | `false` | Connection pooling |
85
+ | `requireTLS` | `boolean` | `true` when `auth` is set | Refuse AUTH on a cleartext connection |
86
+ | `tls` | `TLSOptions` | — | `rejectUnauthorized`, `servername`, `minVersion` |
87
+ | `connectionTimeout` | `number` | — | Socket connect timeout (ms) |
88
+ | `greetingTimeout` | `number` | — | Wait for SMTP greeting (ms) |
89
+ | `socketTimeout` | `number` | — | Idle socket timeout (ms) |
90
+
91
+ ## Switch to an HTTP provider later
92
+
93
+ Keep `mailer.send` and change only construction:
94
+
95
+ ```diff
96
+ - import { createSMTPMailer } from "sently/smtp";
97
+ - const mailer = await createSMTPMailer({ host, port, auth });
98
+ + import { createMailer } from "sently/mailer";
99
+ + import { ResendTransport } from "sently/transports/resend";
100
+ + const mailer = await createMailer({
101
+ + transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
102
+ + });
103
+ ```
104
+
105
+ **Consequence:** message fields stay the same; only the factory and transport change.
106
+
107
+ ## Troubleshooting
108
+
109
+ <Accordions>
110
+ <Accordion title='Error: "SMTP config passed to transport-only createMailer"'>
111
+ `createMailer` accepts only `{ transport, plugins?, hooks? }`. Move host/port/auth to `createSMTPMailer` from `sently/smtp`.
112
+ </Accordion>
113
+ <Accordion title="Do I need to rewrite every message object?">
114
+ Usually no. Keep `from`, `to`, `subject`, and body fields; change construction and `sendMail` → `send`. Rename attachment `cid` to `contentId` when you use inline images.
115
+ </Accordion>
116
+ <Accordion title="Why is createSMTPMailer async?">
117
+ The factory prepares the runtime SMTP connection path (and optionally the pool) before returning. Always `await` it before `send`.
118
+ </Accordion>
119
+ <Accordion title="Is sently a drop-in for every Nodemailer plugin?">
120
+ No. SOCKS and iCal are intentional non-goals. See [Non-goals](/docs/get-started/non-goals) and [Compare](/docs/guides/compare).
121
+ </Accordion>
122
+ </Accordions>
123
+
124
+ ## Learn more
125
+
126
+ - [Email channel](/docs/channels/email) — mailer + transport model after you migrate
127
+ - [Mail options](/docs/reference/mail-options) — full `MailOptions` field list
128
+ - [Attachments](/docs/guides/attachments) — `content`, `path`, and `contentId`
129
+ - [Entrypoints](/docs/get-started/entrypoints) — `sently/smtp` vs `sently/mailer`
130
+ - [Support matrix](/docs/get-started/support-matrix) — which runtimes and exports are supported
20
131
 
21
- <Accordions><Accordion title="Why does transport-only createMailer reject host?">It only accepts a `transport`. Move relay configuration to `createSMTPMailer`.</Accordion></Accordions>
132
+ ## Next
22
133
 
23
- <Cards><Card title="Email channel" href="/docs/channels/email" /></Cards>
134
+ <Cards>
135
+ <Card
136
+ title="Email channel"
137
+ description="Send with createMailer or createSMTPMailer."
138
+ href="/docs/channels/email"
139
+ />
140
+ <Card
141
+ title="SMTP transport"
142
+ description="Relay options when you wire SMTPTransport yourself."
143
+ href="/docs/transports/smtp"
144
+ />
145
+ <Card
146
+ title="Compare"
147
+ description="Nodemailer, vendor SDKs, and orchestration platforms."
148
+ href="/docs/guides/compare"
149
+ />
150
+ </Cards>
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Non-goals
3
+ description: What sently intentionally does not ship — and what belongs on top.
4
+ icon: Ban
5
+ source: "README.md"
6
+ ---
7
+
8
+ sently is a **channel-delivery library**. Keeping the surface focused is part of the 1.x promise.
9
+
10
+ <Callout title="The one rule">
11
+ Do not expect dashboards, preference centers, digests, or workflow builders inside sently. Put that product logic — or Novu / Knock / Courier — on top of the delivery layer.
12
+ </Callout>
13
+
14
+ ## Out of scope
15
+
16
+ | Non-goal | Notes |
17
+ | --- | --- |
18
+ | Orchestration platforms | Preference centers, digests, journey builders, in-app inboxes |
19
+ | Hosted SaaS / per-send billing | MIT library in your process |
20
+ | APNs as a native push transport | Use FCM and/or Web Push for 1.x |
21
+ | Voice channels | Not a sently channel |
22
+ | Inbound email productization | Outbound + webhook parsers only |
23
+ | Nodemailer SOCKS / iCal parity | Intentionally omitted |
24
+ | Equal support for every export | See [Support matrix](./support-matrix) |
25
+ | Cross-channel preview / idempotency | Email-only today |
26
+
27
+ ## Complementary stack
28
+
29
+ | Need | Place it… |
30
+ | --- | --- |
31
+ | Send email / SMS / WhatsApp / push | sently channel senders |
32
+ | Swap providers without rewriting call sites | sently transports |
33
+ | Preferences, digests, workflows, in-app | Your app or Novu / Knock / Courier **on top of** sently |
34
+
35
+ ## Troubleshooting
36
+
37
+ <Accordions>
38
+ <Accordion title="Will sently add a preference center?">
39
+ No. That is platform territory and conflicts with the library-not-platform position.
40
+ </Accordion>
41
+ <Accordion title="Where should I read the positioning story?">
42
+ See [Compare](/docs/guides/compare).
43
+ </Accordion>
44
+ </Accordions>
45
+
46
+ ## Learn more
47
+
48
+ - [Compare](/docs/guides/compare)
49
+ - [Support matrix](./support-matrix)
50
+ - [Stability policy](./stability)
51
+
52
+ ## Next
53
+
54
+ <Cards>
55
+ <Card title="Compare" href="/docs/guides/compare" />
56
+ <Card title="Introduction" href="/docs/get-started/introduction" />
57
+ </Cards>
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Stability policy
3
+ description: What is frozen at 1.0 and what may grow under semver.
4
+ icon: Shield
5
+ source: "package.json"
6
+ ---
7
+
8
+ From **1.0.0**, sently follows semver for the public channel-delivery surface.
9
+ Apps should depend on channel senders and contracts — not on vendor SDK shapes.
10
+
11
+ <Callout title="The one rule">
12
+ Treat channel factories, transport contracts, hooks, `SentlyError` codes, and
13
+ published subpath entrypoints as stable. New transports and optional fields may
14
+ appear in minor releases; renames or narrowed types require a major.
15
+ </Callout>
16
+
17
+ ## Frozen at 1.x (Tier A)
18
+
19
+ | Surface | Stable APIs |
20
+ | --- | --- |
21
+ | Factories | `createMailer`, `createSMTPMailer`, `createSmsSender`, `createWhatsAppSender`, `createPushSender` |
22
+ | Contracts | `Transport`, `SmsTransport`, `WhatsAppTransport`, `PushTransport` option and result shapes |
23
+ | Shared | Lifecycle hooks, `SentlyError` / `sentlyCode`, `ChannelSendResult` / `toChannelSendResult` |
24
+ | Decorators | `RetryTransport`, `FallbackTransport` on all channels |
25
+ | Email-only decorators | `IdempotencyTransport`, `PreviewTransport` (documented as email-only; not removed) |
26
+ | Entrypoints | Subpaths listed in `package.json` `exports` — no silent moves |
27
+
28
+ ## Intentional shapes (stable, not transitional)
29
+
30
+ | Shape | Meaning |
31
+ | --- | --- |
32
+ | `PushOptions` | Discriminated union: Web Push `subscription` **or** FCM `token` |
33
+ | `providerIndex` | Optional on send results when a fallback decorator handled the send |
34
+ | Email-only preview / idempotency | Stay email-scoped; not a signal they will move to other channels soon |
35
+
36
+ ## Allowed without a major
37
+
38
+ - New transport classes and webhook parsers
39
+ - New optional fields on options/results
40
+ - New subpath exports
41
+ - Bug fixes and security patches on the current **1.x** line
42
+
43
+ ## Security patches
44
+
45
+ See [`SECURITY.md`](https://github.com/alialnaghmoush/sently/blob/main/SECURITY.md) for reporting and which versions receive patches.
46
+
47
+ ## Troubleshooting
48
+
49
+ <Accordions>
50
+ <Accordion title="Can I pin 0.x in production after 1.0?">
51
+ Prefer upgrading to 1.x. Pre-1.0 may still receive best-effort fixes but is not the supported security line.
52
+ </Accordion>
53
+ <Accordion title="Where is the production support promise?">
54
+ See [Support matrix](./support-matrix) for Supported vs Available transports.
55
+ </Accordion>
56
+ </Accordions>
57
+
58
+ ## Learn more
59
+
60
+ - [Support matrix](./support-matrix)
61
+ - [Non-goals](./non-goals)
62
+ - [Compare](/docs/guides/compare)
63
+
64
+ ## Next
65
+
66
+ <Cards>
67
+ <Card title="Support matrix" href="/docs/get-started/support-matrix" />
68
+ <Card title="Non-goals" href="/docs/get-started/non-goals" />
69
+ </Cards>
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: Support matrix
3
+ description: Supported vs Available transports and runtimes for production use.
4
+ icon: LayoutGrid
5
+ source: "package.json"
6
+ ---
7
+
8
+ Not every exported transport is part of the **1.x production promise**.
9
+ **Supported** paths are documented, tested, and operable (verify, retry/fallback, and webhooks where listed).
10
+ **Available** paths ship and have unit coverage but are not the support bar.
11
+
12
+ <Callout title="The one rule">
13
+ Build production apps on Supported transports. Treat Available exports as optional extras you can adopt knowing the promise is thinner.
14
+ </Callout>
15
+
16
+ ## Supported (v1 promise)
17
+
18
+ | Channel | Supported transports | Operability |
19
+ | --- | --- | --- |
20
+ | Email | SMTP, Resend, SES, SendGrid, Postmark | Attachments / HTML / text, `verify`, retry / fallback, webhooks for the HTTP majors |
21
+ | SMS | Twilio SMS, Unifonic | Webhooks for Twilio and Unifonic |
22
+ | WhatsApp | WhatsApp Cloud | Signature verify + delivery parse |
23
+ | Push | Web Push, FCM | Option union: `subscription` (Web Push) or `token` (FCM) |
24
+ | Runtimes | Node ≥18, Bun, Deno, Cloudflare Workers | Adapters and smoke scripts |
25
+
26
+ FCM uses the current Firebase HTTP API (service-account JWT, no Google SDK). That is baseline push support, not a special feature tier.
27
+
28
+ ## Available (exported, not the v1 bar)
29
+
30
+ | Channel | Available examples |
31
+ | --- | --- |
32
+ | Email | Mailgun, Brevo, MailerSend, Plunk, SparkPost, Mailtrap, Mailpit (dev), Loops, SNDR, Taqnyat Mail, Cloudflare Email, … |
33
+ | SMS | Taqnyat SMS, Msegat |
34
+ | WhatsApp | Taqnyat WhatsApp |
35
+ | Decorators | `WeightedFallbackTransport` (advanced); preview / idempotency remain email-only |
36
+ | Local email | [Mailpit](/docs/transports/mailpit) (SMTP catcher), [Preview](/docs/decorators/preview) (disk) |
37
+
38
+ Available transports stay in the package. They are promoted to Supported when docs, operability, and smoke coverage meet the bar above.
39
+
40
+ ## Troubleshooting
41
+
42
+ <Accordions>
43
+ <Accordion title="Can I use an Available transport in production?">
44
+ Yes, at your own risk. Prefer Supported paths when you need the documented operability promise.
45
+ </Accordion>
46
+ <Accordion title="Why is Mailgun Available but Postmark Supported?">
47
+ The Supported set is a deliberate short list for the 1.x promise — not a popularity ranking of every working export.
48
+ </Accordion>
49
+ </Accordions>
50
+
51
+ ## Learn more
52
+
53
+ - [Stability policy](./stability)
54
+ - [Non-goals](./non-goals)
55
+ - [Transports](/docs/transports)
56
+
57
+ ## Next
58
+
59
+ <Cards>
60
+ <Card title="Stability policy" href="/docs/get-started/stability" />
61
+ <Card title="Transports" href="/docs/transports" />
62
+ </Cards>
@@ -0,0 +1,87 @@
1
+ ---
2
+ title: Compare
3
+ description: How sently fits next to vendor SDKs, Nodemailer, and orchestration platforms.
4
+ icon: Scale
5
+ source: "README.md"
6
+ ---
7
+
8
+ Teams often start with one channel — usually email via SendGrid or Resend — then add SMS and push.
9
+ Each new vendor SDK brings its own auth, retries, and error shapes into app code.
10
+
11
+ <Callout title="The one rule">
12
+ Treat sently as the channel-delivery layer. Put preference centers, digests, workflow builders, and in-app inboxes on top of it — custom code or a tool like Novu, Knock, or Courier — not instead of it.
13
+ </Callout>
14
+
15
+ ## What sently replaces
16
+
17
+ | Instead of… | Use sently for… |
18
+ | --- | --- |
19
+ | A growing pile of vendor SDKs in call sites | Channel senders + pluggable transports |
20
+ | Per-channel retry and error glue | Shared `SentlyError` codes and retry/fallback decorators |
21
+ | Nodemailer when you need multi-channel or non-Node runtimes | `createMailer` / `createSMTPMailer` plus SMS, WhatsApp, and push |
22
+
23
+ ## What sently does not replace
24
+
25
+ | Need | Where it lives |
26
+ | --- | --- |
27
+ | Preference centers and unsubscribe UX | Your product, or Novu / Knock / Courier |
28
+ | Digest / batching product logic | Your product, or an orchestration platform |
29
+ | Workflow builders and multi-step journeys | Your product, or Novu / Knock / Courier |
30
+ | In-app notification inboxes | Your product, or an orchestration platform |
31
+
32
+ Those tools (or your own routing) call sently — or any transport — to deliver. sently does not ship dashboards or per-send SaaS.
33
+
34
+ ## Versus a vendor SDK pile
35
+
36
+ | Concern | Typical stack | sently |
37
+ | --- | --- | --- |
38
+ | As you add channels | New SDK, new auth, new retries | Same sender factories |
39
+ | Failures | Per-vendor exceptions | Stable `SentlyError` codes |
40
+ | Reliability | Ad-hoc loops per client | `RetryTransport` + `FallbackTransport` |
41
+ | Provider swap | Rewrite call sites | Swap the transport |
42
+
43
+ ## Versus Nodemailer
44
+
45
+ | Concern | Nodemailer | sently |
46
+ | --- | --- | --- |
47
+ | Channels | Email | Email · SMS · WhatsApp · Push |
48
+ | Runtimes | Node.js | Node · Bun · Deno · CF Workers |
49
+ | Module format | CommonJS | ESM only |
50
+ | Edge / Workers weight | Full mail stack on import | Tree-shakeable subpaths (~6.3 KB HTTP) |
51
+
52
+ Bundle size matters most on Workers and cold starts. It is supporting evidence, not the primary reason to adopt.
53
+
54
+ ## Versus Novu, Knock, and Courier
55
+
56
+ | Concern | Orchestration platforms | sently |
57
+ | --- | --- | --- |
58
+ | Role | Workflows, preferences, digests, in-app | Channel delivery / transports |
59
+ | Hosting | Usually hosted SaaS | Library in your process |
60
+ | Pricing model | Often per-send or seat | MIT package; you pay providers |
61
+
62
+ Use both when you need product orchestration **and** a typed, multi-runtime delivery layer underneath.
63
+
64
+ ## Troubleshooting
65
+
66
+ <Accordions>
67
+ <Accordion title="Do I need sently if I already use Novu or Knock?">
68
+ Only if you want local, typed transports under your own routing — or to send outside that platform. Many teams use an orchestration tool for preferences and journeys, and a library like sently for direct transactional sends.
69
+ </Accordion>
70
+ <Accordion title="Is sently a Nodemailer drop-in?">
71
+ No. Migrate email with [Migrate from Nodemailer](/docs/get-started/migrate-nodemailer), then add other channels with the same sender pattern.
72
+ </Accordion>
73
+ </Accordions>
74
+
75
+ ## Learn more
76
+
77
+ - [Introduction](/docs/get-started/introduction)
78
+ - [Stability policy](/docs/get-started/stability)
79
+ - [Support matrix](/docs/get-started/support-matrix)
80
+ - [Migrate from Nodemailer](/docs/get-started/migrate-nodemailer)
81
+
82
+ ## Next
83
+
84
+ <Cards>
85
+ <Card title="Introduction" href="/docs/get-started/introduction" />
86
+ <Card title="Support matrix" href="/docs/get-started/support-matrix" />
87
+ </Cards>
@@ -0,0 +1,121 @@
1
+ ---
2
+ title: Failover
3
+ description: Retry inside a provider, then fail over across providers on any channel.
4
+ icon: GitBranch
5
+ source: "src/transports/fallback.ts"
6
+ ---
7
+
8
+ When a provider blips, retry that transport first, then fail over to the next.
9
+ The same decorator pattern works for email, SMS, WhatsApp, and push.
10
+
11
+ <Callout title="The one rule">
12
+ Wrap each provider in `RetryTransport`, then pass the list to `FallbackTransport`. Use the channel sender that matches the transport contract.
13
+ </Callout>
14
+
15
+ ## Quick start
16
+
17
+ <Steps>
18
+ <Step title="Email — Resend then SES">
19
+
20
+ ```ts
21
+ import { createMailer } from "sently/mailer";
22
+ import { FallbackTransport } from "sently/transports/fallback";
23
+ import { RetryTransport } from "sently/transports/retry";
24
+ import { ResendTransport } from "sently/transports/resend";
25
+ import { SESTransport } from "sently/transports/ses";
26
+
27
+ const mailer = await createMailer({
28
+ transport: new FallbackTransport(
29
+ [
30
+ new RetryTransport(new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })),
31
+ new RetryTransport(
32
+ new SESTransport({
33
+ region: process.env.AWS_REGION!,
34
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
35
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
36
+ }),
37
+ ),
38
+ ],
39
+ { cooldownMs: 300_000 },
40
+ ),
41
+ });
42
+ ```
43
+
44
+ </Step>
45
+ <Step title="SMS — Twilio then Unifonic">
46
+
47
+ ```ts
48
+ import { createSmsSender } from "sently/sms";
49
+ import { FallbackTransport } from "sently/transports/fallback";
50
+ import { RetryTransport } from "sently/transports/retry";
51
+ import { TwilioSmsTransport } from "sently/transports/twilio-sms";
52
+ import { UnifonicTransport } from "sently/transports/unifonic";
53
+
54
+ const sms = createSmsSender({
55
+ transport: new FallbackTransport(
56
+ [
57
+ new RetryTransport(
58
+ new TwilioSmsTransport({
59
+ accountSid: process.env.TWILIO_ACCOUNT_SID!,
60
+ authToken: process.env.TWILIO_AUTH_TOKEN!,
61
+ }),
62
+ ),
63
+ new RetryTransport(
64
+ new UnifonicTransport({
65
+ appSid: process.env.UNIFONIC_APPSID!,
66
+ senderId: "MyBrand",
67
+ }),
68
+ ),
69
+ ],
70
+ { cooldownMs: 300_000 },
71
+ ),
72
+ });
73
+ ```
74
+
75
+ </Step>
76
+ <Step title="Inspect which provider won">
77
+
78
+ ```ts
79
+ const result = await sms.send({
80
+ to: "+15551234567",
81
+ body: "Hello",
82
+ from: "+15557654321",
83
+ });
84
+ console.log(result.provider, result.providerIndex);
85
+ ```
86
+
87
+ </Step>
88
+ </Steps>
89
+
90
+ ## Behavior
91
+
92
+ | Behavior | Default |
93
+ | --- | --- |
94
+ | Fail over on 5xx / network | Yes |
95
+ | Fail over on 400 / 401 / 403 | No (same client error on every provider) |
96
+ | `cooldownMs` | Skip a failed provider until the cooldown ends |
97
+ | Hooks | `onRetry` / `onFallback` on the channel sender when wired |
98
+
99
+ ## Troubleshooting
100
+
101
+ <Accordions>
102
+ <Accordion title="Why did both providers reject the same message?">
103
+ Permanent client errors do not fail over by default. Fix the payload or credentials before expecting a second provider to succeed.
104
+ </Accordion>
105
+ <Accordion title="Can I mix email and SMS in one FallbackTransport?">
106
+ No. Every entry must implement the same channel contract as the sender.
107
+ </Accordion>
108
+ </Accordions>
109
+
110
+ ## Learn more
111
+
112
+ - [Retry](/docs/decorators/retry)
113
+ - [Fallback](/docs/decorators/fallback)
114
+ - [Support matrix](/docs/get-started/support-matrix)
115
+
116
+ ## Next
117
+
118
+ <Cards>
119
+ <Card title="Fallback decorator" href="/docs/decorators/fallback" />
120
+ <Card title="Retry decorator" href="/docs/decorators/retry" />
121
+ </Cards>
@@ -8,6 +8,8 @@ source: "README.md"
8
8
  Configure sently features around your delivery workflow.
9
9
 
10
10
  <Cards>
11
+ <Card title="Compare" href="/docs/guides/compare" />
12
+ <Card title="Failover" href="/docs/guides/failover" />
11
13
  <Card title="Adapters" href="/docs/guides/adapters" />
12
14
  <Card title="Webhooks" href="/docs/guides/webhooks" />
13
15
  <Card title="Security" href="/docs/guides/security" />
@@ -3,6 +3,8 @@
3
3
  "icon": "Map",
4
4
  "pages": [
5
5
  "index",
6
+ "compare",
7
+ "failover",
6
8
  "adapters",
7
9
  "dkim",
8
10
  "oauth2",
@@ -1,17 +1,47 @@
1
1
  ---
2
2
  title: Security
3
- description: Protect credentials, delivery endpoints, and webhook integrity.
3
+ description: Protect credentials, delivery endpoints, webhook integrity, and publish trust.
4
4
  icon: ShieldCheck
5
5
  source: "src/transports/webpush.ts"
6
6
  ---
7
7
 
8
- <Callout title="The one rule">Keep provider keys and VAPID private keys in a secrets manager or environment variables.</Callout>
8
+ Provider API keys, VAPID private keys, and push endpoints are long-lived credentials.
9
+ Keep them out of source control and out of unstructured logs.
9
10
 
10
- - Enable TLS for SMTP authentication; `requireTLS` defaults to true when auth is set.
11
- - Verify provider webhook signatures before parsing trusted events.
12
- - Do not log email bodies, SMS bodies, or full push subscription endpoints.
13
- - Treat a push endpoint as a delivery token.
11
+ <Callout title="The one rule">
12
+ Keep provider keys and VAPID private keys in a secrets manager or environment variables.
13
+ </Callout>
14
14
 
15
- <Accordions><Accordion title="Why is a push endpoint redacted in hooks?">Its path can contain a long-lived delivery token.</Accordion></Accordions>
15
+ ## Checklist
16
16
 
17
- <Cards><Card title="Webhooks" href="/docs/guides/webhooks" /></Cards>
17
+ | Practice | Detail |
18
+ | --- | --- |
19
+ | SMTP TLS | `requireTLS` defaults to true when auth is set |
20
+ | Webhooks | Verify signatures before trusting parsed events |
21
+ | Logging | Do not log email/SMS bodies or full push endpoints |
22
+ | Push | Treat subscription endpoints and FCM tokens as delivery tokens |
23
+ | Reporting | Use private advisories — see root `SECURITY.md` |
24
+
25
+ ## Troubleshooting
26
+
27
+ <Accordions>
28
+ <Accordion title="Why is a push endpoint redacted in hooks?">
29
+ Its path can contain a long-lived delivery token. Hook context keeps a short fingerprint.
30
+ </Accordion>
31
+ <Accordion title="Where do I report a vulnerability?">
32
+ Open a private GitHub Security Advisory for this repository. Do not file a public issue. Details and response targets are in [`SECURITY.md`](https://github.com/alialnaghmoush/sently/blob/main/SECURITY.md).
33
+ </Accordion>
34
+ </Accordions>
35
+
36
+ ## Learn more
37
+
38
+ - [Stability policy](/docs/get-started/stability)
39
+ - [Webhooks](/docs/guides/webhooks)
40
+ - [Web Push](/docs/transports/webpush)
41
+
42
+ ## Next
43
+
44
+ <Cards>
45
+ <Card title="Webhooks" href="/docs/guides/webhooks" />
46
+ <Card title="Stability policy" href="/docs/get-started/stability" />
47
+ </Cards>