@consilioweb/payload-support 2.0.1 → 3.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 (215) hide show
  1. package/README.md +636 -402
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/index.d.ts +4 -0
  29. package/dist/components/TicketConversation/index.js +2 -2
  30. package/dist/components/TicketConversation/types.d.ts +56 -0
  31. package/dist/components/TicketConversation/utils.d.ts +2 -0
  32. package/dist/index.cjs +392 -320
  33. package/dist/index.d.cts +100 -3
  34. package/dist/index.d.ts +100 -3
  35. package/dist/index.js +388 -321
  36. package/dist/utils/features.d.ts +123 -0
  37. package/dist/utils/features.js +147 -0
  38. package/dist/views/BillingView/client.d.ts +2 -0
  39. package/dist/views/BillingView/index.d.ts +4 -0
  40. package/dist/views/ChatView/client.d.ts +2 -0
  41. package/dist/views/ChatView/client.js +9 -9
  42. package/dist/views/ChatView/index.d.ts +4 -0
  43. package/dist/views/CrmView/client.d.ts +2 -0
  44. package/dist/views/CrmView/client.js +6 -9
  45. package/dist/views/CrmView/index.d.ts +4 -0
  46. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  47. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  48. package/dist/views/ImportConversationView/client.d.ts +2 -0
  49. package/dist/views/ImportConversationView/index.d.ts +4 -0
  50. package/dist/views/LogsView/client.d.ts +2 -0
  51. package/dist/views/LogsView/index.d.ts +4 -0
  52. package/dist/views/NewTicketView/client.d.ts +2 -0
  53. package/dist/views/NewTicketView/index.d.ts +4 -0
  54. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  55. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  56. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  57. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  58. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  59. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  60. package/dist/views/TicketDetailView/client.d.ts +2 -0
  61. package/dist/views/TicketDetailView/client.js +2 -2
  62. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  63. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  64. package/dist/views/TicketDetailView/index.d.ts +4 -0
  65. package/dist/views/TicketDetailView/types.d.ts +40 -0
  66. package/dist/views/TicketInboxView/client.d.ts +2 -0
  67. package/dist/views/TicketInboxView/client.js +5 -5
  68. package/dist/views/TicketInboxView/index.d.ts +4 -0
  69. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  70. package/dist/views/TicketingSettingsView/client.js +5 -4
  71. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  72. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  73. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  74. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  75. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  76. package/dist/views/shared/Skeleton.d.ts +21 -0
  77. package/dist/views/shared/StatusPill.d.ts +8 -0
  78. package/dist/views/shared/adminTokens.d.ts +19 -0
  79. package/dist/views/shared/config.d.ts +8 -0
  80. package/dist/views/shared/config.js +1 -40
  81. package/dist/views/shared/dateLocale.d.ts +5 -0
  82. package/dist/views/shared/index.d.ts +12 -0
  83. package/dist/views/shared/index.js +2 -1
  84. package/dist/views/shared/sla.d.ts +14 -0
  85. package/dist/views.d.ts +13 -13
  86. package/package.json +16 -31
  87. package/src/collections/SupportClients.ts +29 -0
  88. package/src/collections/TicketCollaborators.ts +6 -1
  89. package/src/collections/TicketMessages.ts +46 -24
  90. package/src/collections/Tickets.ts +117 -79
  91. package/src/collections/TimeEntries.ts +57 -32
  92. package/src/components/TicketConversation/config.ts +16 -80
  93. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  94. package/src/components/TicketConversation/index.tsx +2 -2
  95. package/src/endpoints/ai.ts +2 -1
  96. package/src/endpoints/client-intelligence.ts +2 -1
  97. package/src/endpoints/process-scheduled.ts +13 -8
  98. package/src/endpoints/round-robin-config.ts +30 -20
  99. package/src/endpoints/settings.ts +28 -77
  100. package/src/index.ts +6 -2
  101. package/src/portal/LiveChat.tsx +33 -22
  102. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  103. package/src/types/lucide-react.d.ts +10 -2
  104. package/src/utils/aiAgent.ts +2 -1
  105. package/src/utils/aiProvider.ts +21 -0
  106. package/src/utils/features.ts +297 -0
  107. package/src/utils/fireWebhooks.ts +6 -0
  108. package/src/utils/generateTicketSynthesis.ts +2 -1
  109. package/src/utils/index.ts +4 -2
  110. package/src/utils/readSettings.ts +82 -9
  111. package/src/utils/webhookDispatcher.ts +19 -7
  112. package/src/views/ChatView/client.tsx +13 -9
  113. package/src/views/CrmView/client.tsx +10 -11
  114. package/src/views/TicketDetailView/client.tsx +2 -2
  115. package/src/views/TicketInboxView/client.tsx +10 -5
  116. package/src/views/TicketingSettingsView/client.tsx +8 -6
  117. package/src/views/shared/config.ts +14 -80
  118. package/src/views/shared/index.ts +3 -1
  119. package/dist/components/RichTextEditor/index.cjs +0 -279
  120. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  121. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  122. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  123. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  124. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  125. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  126. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  127. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  128. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  129. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  130. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  131. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  132. package/dist/components/TicketConversation/config.cjs +0 -44
  133. package/dist/components/TicketConversation/constants.cjs +0 -127
  134. package/dist/components/TicketConversation/context.cjs +0 -13
  135. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  136. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  137. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  138. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  139. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  140. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  141. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -101
  142. package/dist/components/TicketConversation/index.cjs +0 -1114
  143. package/dist/components/TicketConversation/types.cjs +0 -2
  144. package/dist/components/TicketConversation/utils.cjs +0 -27
  145. package/dist/views/BillingView/client.cjs +0 -469
  146. package/dist/views/BillingView/index.cjs +0 -34
  147. package/dist/views/ChatView/client.cjs +0 -301
  148. package/dist/views/ChatView/index.cjs +0 -34
  149. package/dist/views/CrmView/client.cjs +0 -458
  150. package/dist/views/CrmView/index.cjs +0 -34
  151. package/dist/views/EmailTrackingView/client.cjs +0 -171
  152. package/dist/views/EmailTrackingView/index.cjs +0 -34
  153. package/dist/views/ImportConversationView/client.cjs +0 -238
  154. package/dist/views/ImportConversationView/index.cjs +0 -34
  155. package/dist/views/LogsView/client.cjs +0 -148
  156. package/dist/views/LogsView/index.cjs +0 -32
  157. package/dist/views/NewTicketView/client.cjs +0 -213
  158. package/dist/views/NewTicketView/index.cjs +0 -32
  159. package/dist/views/PendingEmailsView/client.cjs +0 -471
  160. package/dist/views/PendingEmailsView/index.cjs +0 -34
  161. package/dist/views/SupportDashboardView/client.cjs +0 -437
  162. package/dist/views/SupportDashboardView/index.cjs +0 -34
  163. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  164. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  165. package/dist/views/TicketDetailView/client.cjs +0 -1412
  166. package/dist/views/TicketDetailView/constants.cjs +0 -20
  167. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  168. package/dist/views/TicketDetailView/index.cjs +0 -34
  169. package/dist/views/TicketDetailView/types.cjs +0 -2
  170. package/dist/views/TicketInboxView/client.cjs +0 -359
  171. package/dist/views/TicketInboxView/index.cjs +0 -32
  172. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  173. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  174. package/dist/views/TimeDashboardView/client.cjs +0 -180
  175. package/dist/views/TimeDashboardView/index.cjs +0 -34
  176. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  177. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  178. package/dist/views/shared/Skeleton.cjs +0 -77
  179. package/dist/views/shared/StatusPill.cjs +0 -47
  180. package/dist/views/shared/adminTokens.cjs +0 -34
  181. package/dist/views/shared/config.cjs +0 -44
  182. package/dist/views/shared/dateLocale.cjs +0 -5
  183. package/dist/views/shared/index.cjs +0 -72
  184. package/dist/views/shared/sla.cjs +0 -37
  185. package/dist/views.cjs +0 -86
  186. package/dist/views.d.cts +0 -13
  187. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  188. package/src/__tests__/authResponses.test.ts +0 -69
  189. package/src/__tests__/capabilities.test.ts +0 -71
  190. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  191. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  192. package/src/__tests__/integration/auth.test.ts +0 -43
  193. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  194. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  195. package/src/__tests__/integration/channels.test.ts +0 -52
  196. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  197. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  198. package/src/__tests__/integration/digest.test.ts +0 -48
  199. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  200. package/src/__tests__/integration/isolation.test.ts +0 -81
  201. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  202. package/src/__tests__/integration/nps.test.ts +0 -37
  203. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  204. package/src/__tests__/integration/push.test.ts +0 -75
  205. package/src/__tests__/integration/sanitization.test.ts +0 -35
  206. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  207. package/src/__tests__/integration/smoke.test.ts +0 -26
  208. package/src/__tests__/integration/snooze.test.ts +0 -68
  209. package/src/__tests__/integration/teams.test.ts +0 -38
  210. package/src/__tests__/rateLimiter.test.ts +0 -83
  211. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  212. package/src/__tests__/sla.test.ts +0 -258
  213. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  214. package/src/__tests__/translations.test.ts +0 -67
  215. package/src/__tests__/webhookSecurity.test.ts +0 -58
package/README.md CHANGED
@@ -1,98 +1,115 @@
1
- <div align="center" style="background: linear-gradient(135deg, #1f8a5b 0%, #16a34a 50%, #0d9488 100%); padding: 50px 40px; border-radius: 12px; color: white; margin-bottom: 40px;">
2
- <h1 style="margin: 0 0 15px 0; font-size: 42px; font-weight: 700; letter-spacing: -0.5px;">payload-support</h1>
3
- <p style="margin: 0 auto; font-size: 18px; opacity: 0.95; max-width: 640px; line-height: 1.6;">A complete, self-hosted support &amp; ticketing system for Payload CMS 3 + Next.js tickets, SLA, AI assists and an autonomous AI agent, live chat, a full client portal, time tracking &amp; invoicing, and a visual automation-rules engine. No third-party support SaaS required.</p>
4
- </div>
1
+ # @consilioweb/payload-support
2
+
3
+ > A self-hosted support and ticketing system for Payload CMS 3 + Next.js: tickets, SLA, live chat, a client portal, time tracking and invoicing, AI assists and an automation-rules engine.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@consilioweb/payload-support.svg)](https://www.npmjs.com/package/@consilioweb/payload-support)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![payload](https://img.shields.io/badge/payload-%5E3.37-blue.svg)](https://payloadcms.com)
8
+
9
+ ## About
10
+
11
+ `supportPlugin()` turns a Payload 3 admin into a helpdesk: it injects 14 to 25 collections
12
+ (depending on the feature flags), 13 admin views and around 60 REST endpoints covering the whole
13
+ ticket lifecycle — intake, assignment, SLA, replies by email or live chat, resolution, CSAT and
14
+ billing. It is aimed at agencies and product teams who already run Payload and do not want to pay
15
+ for, or export their customers' data to, a third-party support SaaS.
16
+
17
+ Two things it deliberately does *not* do: it does not mount the client portal for you (the portal
18
+ ships as a template you copy into your own `app/` directory — see
19
+ [Mounting the client portal](#mounting-the-client-portal)), and it does not bundle an LLM SDK
20
+ (install `@anthropic-ai/sdk` yourself if you enable the AI features).
21
+
22
+ ## Table of Contents
23
+
24
+ - [Features](#features)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Mounting the client portal](#mounting-the-client-portal)
28
+ - [Configuration](#configuration)
29
+ - [Deployment adapters](#deployment-adapters)
30
+ - [API Endpoints](#api-endpoints)
31
+ - [Webhooks](#webhooks)
32
+ - [Collections](#collections)
33
+ - [Package Exports](#package-exports)
34
+ - [Requirements](#requirements)
35
+ - [Security](#security)
36
+ - [Performance](#performance)
37
+ - [Troubleshooting](#troubleshooting)
38
+ - [FAQ](#faq)
39
+ - [Upgrading](#upgrading)
40
+ - [Contributing](#contributing)
41
+ - [Changelog](#changelog)
42
+ - [Support](#support)
43
+ - [License](#license)
44
+
45
+ ## Features
46
+
47
+ - **Ticketing** — statuses, priorities, categories, tags, merge, split, snooze, scheduled replies,
48
+ internal notes, collaborators, and an append-only activity log (`create` and `update` are denied
49
+ to everyone; only hooks write to it).
50
+ - **Agent inbox** — a keyboard-driven inbox view, bulk actions, macros, canned responses, and typing
51
+ and presence indicators.
52
+ - **SLA** — per-team or default policies, business-hours deadlines, pause-on-hold, escalation, and a
53
+ `sla-check` endpoint that lists what is breaching.
54
+ - **Automation** — an `automation-rules` collection (event → conditions → actions) and optional
55
+ round-robin assignment.
56
+ - **AI assists** — sentiment, reply suggestion, multi-style rewrite, per-ticket cached synthesis, a
57
+ knowledge-base chatbot, a client-intelligence summary, and an autonomous agent that either answers
58
+ from the knowledge base or escalates. Anthropic, or `provider: 'ollama'` to reach a self-hosted
59
+ gateway through the same SDK pointed at your own `OLLAMA_API_URL`.
60
+ - **Omnichannel** — inbound and outbound email with open tracking, live chat (SSE in the portal
61
+ widget, polling in the shipped agent console; the portal widget stays hidden until
62
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`), and a normalized inbound webhook for WhatsApp /
63
+ Messenger.
64
+ - **Client portal** — password login, email 2FA, Google OAuth, ticket list and detail, FAQ, profile,
65
+ GDPR data export and account deletion. Shipped as a template you copy (see below).
66
+ - **Time tracking and billing** — timer and manual entries, a per-ticket rollup, a time dashboard,
67
+ per-project pre-billing, and an invoice rendered as HTML or PDF.
68
+ - **Reporting** — dashboard KPIs, CSAT and NPS, email statistics, CSV export.
69
+ - **Notifications** — admin notifications, Web Push (VAPID), and daily/weekly digest emails.
70
+ - **Webhooks** — HMAC-SHA256 signed outbound deliveries on ticket events.
71
+ - **i18n** — French and English catalogs across the admin views, selectable per user.
72
+
73
+ ## Installation
5
74
 
6
- <div align="center">
7
-
8
- [![npm version](https://img.shields.io/npm/v/@consilioweb/payload-support?color=1f8a5b&label=npm)](https://www.npmjs.com/package/@consilioweb/payload-support)
9
- [![MIT License](https://img.shields.io/badge/license-MIT-1f8a5b)](LICENSE)
10
- [![Node.js](https://img.shields.io/badge/node-18+-1f8a5b)](https://nodejs.org)
11
- [![Payload](https://img.shields.io/badge/payload-3.x-1f8a5b)](https://payloadcms.com)
12
- [![Tests](https://img.shields.io/badge/tests-132%20passing-1f8a5b)](src/__tests__)
13
- [![TypeScript](https://img.shields.io/badge/typescript-strict-1f8a5b)](https://www.typescriptlang.org)
14
-
15
- </div>
16
-
17
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
18
-
19
- ## 📑 Table of Contents
20
-
21
- - [Features](#-features)
22
- - [Quick Start](#-quick-start)
23
- - [Installation](#-installation)
24
- - [Usage](#-usage)
25
- - [API Reference](#-api-reference)
26
- - [Configuration](#-configuration)
27
- - [Performance](#-performance)
28
- - [Examples](#-examples)
29
- - [FAQ](#-faq)
30
- - [Troubleshooting](#-troubleshooting)
31
- - [Security](#-security)
32
- - [Contributing](#-contributing)
33
- - [Changelog](#-changelog)
34
- - [Roadmap](#-roadmap)
35
- - [Support](#-support)
36
- - [License](#-license)
37
-
38
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
39
-
40
- ## ✨ Features
41
-
42
- <table>
43
- <tr>
44
- <td width="50%">
45
-
46
- **🎫 Complete ticketing**
47
-
48
- Statuses, priorities, categories, tags, merge/split, snooze, scheduled replies, internal notes, an immutable activity log and a keyboard-driven Superhuman-style inbox.
49
-
50
- </td>
51
- <td width="50%">
52
-
53
- **⏱️ SLA & automation**
54
-
55
- SLA policies (business hours + pause-on-hold), escalation, macros, round-robin, and a **visual automation-rules engine** (conditions → actions).
56
-
57
- </td>
58
- </tr>
59
- <tr>
60
- <td width="50%">
61
-
62
- **🤖 Built-in AI**
63
-
64
- Sentiment, reply suggestions, multi-style rewriting, cached synthesis, a KB chatbot, and an **autonomous AI agent** that answers or escalates. Anthropic, OpenAI or self-hosted Ollama.
65
-
66
- </td>
67
- <td width="50%">
68
-
69
- **💬 Omnichannel & portal**
70
-
71
- Inbound/outbound email, live chat (SSE), widget, a full **client portal** (auth, 2FA, Google OAuth), knowledge base and deflection.
72
-
73
- </td>
74
- </tr>
75
- <tr>
76
- <td width="50%">
77
-
78
- **💶 Time tracking & billing**
75
+ ```bash
76
+ pnpm add @consilioweb/payload-support
77
+ # or: npm install @consilioweb/payload-support
78
+ # or: yarn add @consilioweb/payload-support
79
+ ```
79
80
 
80
- Timer, manual entries, time dashboard, per-project pre-billing and a **print-ready invoice** (HTML → PDF).
81
+ **Peer dependencies** all six are required, none is optional:
81
82
 
82
- </td>
83
- <td width="50%">
83
+ ```bash
84
+ pnpm add payload@^3.37.0 @payloadcms/next@^3.37.0 next@^15.2.9 react@^19 react-dom@^19 lucide-react@">=0.300.0"
85
+ ```
84
86
 
85
- **📊 Reporting & privacy**
87
+ - `payload@^3.37.0` the Google OAuth endpoint imports `jwtSign` from the `payload` barrel, which
88
+ only exists from 3.37.0 on. Below that, the missing named export fails the whole
89
+ `payload.config.ts`, not just that endpoint.
90
+ - `@payloadcms/next` and `next` — the 13 admin views are registered unless you pass
91
+ `skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
92
+ `next/navigation` / `next/link` statically. Both are already present in any Payload 3 admin app.
93
+ - `lucide-react` — imported statically by `PendingEmailsView` and `TicketingSettingsView`. (The
94
+ third importer, `src/portal/LiveChat.tsx`, is a leftover: nothing in the shipped portal mounts
95
+ it.)
96
+
97
+ **AI runtime dependency** — the AI features load `@anthropic-ai/sdk` through a runtime
98
+ `require`/dynamic `import` and it is marked `external` at build time. It is intentionally not a
99
+ dependency of this package, so install it yourself if you use `features.ai`, the chatbot, the
100
+ autonomous agent, ticket synthesis, client intelligence or the AI conversation import:
86
101
 
87
- Real-time dashboard, **CSAT + NPS**, email tracking, HMAC-signed webhooks, notification digests, CSV exports. 100% self-hostable.
102
+ ```bash
103
+ pnpm add @anthropic-ai/sdk
104
+ ```
88
105
 
89
- </td>
90
- </tr>
91
- </table>
106
+ After adding admin components, regenerate the import map:
92
107
 
93
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
108
+ ```bash
109
+ pnpm payload generate:importmap
110
+ ```
94
111
 
95
- ## 🚀 Quick Start
112
+ ## Quick Start
96
113
 
97
114
  ```ts
98
115
  // payload.config.ts
@@ -103,8 +120,7 @@ export default buildConfig({
103
120
  plugins: [
104
121
  supportPlugin({
105
122
  features: { ai: true, sla: true, timeTracking: true, chat: true },
106
- ai: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
107
- locale: 'fr',
123
+ ticketNumber: { prefix: 'TK-', padding: 6 },
108
124
  }),
109
125
  ],
110
126
  })
@@ -115,61 +131,234 @@ pnpm payload generate:importmap
115
131
  pnpm dev
116
132
  ```
117
133
 
118
- Open `/admin/support/inbox` for the agent inbox, or `/support` for the client portal. The plugin injects collections, API endpoints and admin views automatically — no external SaaS.
119
-
120
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
121
-
122
- ## 📦 Installation
123
-
124
- ### npm
125
- ```bash
126
- npm install @consilioweb/payload-support
134
+ Open `/admin/support/inbox` for the agent inbox and `/admin/support/settings` to pick the AI
135
+ provider, the sender addresses, the SLA targets and the runtime feature flags.
136
+
137
+ > **The client portal is not a route the plugin mounts.** `/support` returns 404 until you copy the
138
+ > portal template into your own `app/` directory — see
139
+ > [Mounting the client portal](#mounting-the-client-portal). Everything the portal talks to (the REST
140
+ > endpoints, the auth/2FA/OAuth endpoints, the collections) *is* installed by the plugin; only the
141
+ > Next.js pages have to live in your app.
142
+
143
+ ## Mounting the client portal
144
+
145
+ The portal ships as a **template you copy**, not as an importable subpath — and that is a
146
+ constraint, not an oversight: `src/portal/auth/layout.tsx` and three of its pages import
147
+ `@payload-config`, an alias that only exists inside your application, and `src/portal/layout.tsx`
148
+ renders its own `<html>`/`<body>` as a Next.js **root layout**. Neither can be resolved from
149
+ `node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next route.
150
+
151
+ The sources are published in the tarball (`files` includes `src`), so copy them out of
152
+ `node_modules` into a route group of your own `app/` directory. Two directories have to be
153
+ renamed on the way in — the shipped names are plain folders, not Next.js route syntax — and
154
+ `layout.tsx` has to sit at the **group** level, because it renders its own `<html>`/`<body>`
155
+ and only one root layout per group may do that (the same convention `create-payload-app`
156
+ uses for its `(payload)` / `(frontend)` groups):
157
+
158
+ ```text
159
+ src/app/
160
+ └── (support)/
161
+ ├── layout.tsx ← src/portal/layout.tsx (root layout of the group)
162
+ └── support/
163
+ ├── page.tsx ← src/portal/page.tsx → /support
164
+ ├── login/ register/ forgot-password/ reset-password/
165
+ └── (auth)/ ← src/portal/auth/ (route group: no URL segment)
166
+ ├── layout.tsx (auth guard + header + chatbot and chat widgets)
167
+ ├── dashboard/ faq/ profile/
168
+ └── tickets/
169
+ ├── new/
170
+ └── [id]/ ← src/portal/auth/tickets/detail/ (the page reads `params.id`)
127
171
  ```
128
172
 
129
- ### yarn
130
173
  ```bash
131
- yarn add @consilioweb/payload-support
174
+ mkdir -p 'src/app/(support)/support'
175
+ cp -R node_modules/@consilioweb/payload-support/src/portal/. 'src/app/(support)/support/'
176
+ cd 'src/app/(support)'
177
+ mv support/layout.tsx layout.tsx
178
+ mv support/auth 'support/(auth)'
179
+ mv 'support/(auth)/tickets/detail' 'support/(auth)/tickets/[id]'
132
180
  ```
133
181
 
134
- ### pnpm
135
- ```bash
136
- pnpm add @consilioweb/payload-support
137
- ```
182
+ That yields `/support`, `/support/login`, `/support/register`, `/support/forgot-password`,
183
+ `/support/reset-password` and, behind the auth layout, `/support/dashboard`, `/support/faq`,
184
+ `/support/profile`, `/support/tickets/new` and `/support/tickets/<id>` — exactly the paths the
185
+ plugin's notification emails and the portal's own links point to, so the renames are
186
+ mandatory, not cosmetic.
138
187
 
139
- **Peer dependencies:** `payload@^3`, `react@^18 || ^19`, `react-dom@^18 || ^19`, `next@^14 || ^15 || ^16`. `lucide-react` and the `@payloadcms/*` packages are optional depending on the features you enable. After adding admin components, run `pnpm payload generate:importmap`.
188
+ What your app must provide for those pages to work:
140
189
 
141
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
190
+ | Requirement | Why |
191
+ |---|---|
192
+ | The `@payload-config` alias | The authenticated pages call `getPayload({ config })` directly. Standard in a `create-payload-app` project. |
193
+ | **Tailwind CSS** | 28 of the 35 portal files are styled with Tailwind utility classes only. Without Tailwind the portal renders unstyled. |
194
+ | `@payloadcms/richtext-lexical` | `auth/faq/page.tsx` renders knowledge-base entries with `RichText` from `@payloadcms/richtext-lexical/react`. It is **not** a peer dependency of this plugin — add it yourself if you mount the FAQ page. |
195
+ | `NEXT_PUBLIC_SERVER_URL` | Email and portal links are built from it. |
196
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | The ticket detail page prints a support phone number; without this variable it prints the placeholder `01 23 45 67 89` to your clients. |
142
197
 
143
- ## 💻 Usage
198
+ `lucide-react` is **not** needed by the pages above: the only portal file importing it is
199
+ `src/portal/LiveChat.tsx`, which the shipped `(auth)` layout never mounts. It stays a required peer
200
+ for the admin views.
144
201
 
145
- ### Basic
202
+ The `(auth)` layout mounts two widgets. `ChatbotWidget` is always rendered; `ChatWidget` — the
203
+ floating live-chat FAB — returns `null` unless the host app sets
204
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`, whatever `features.chat` says.
146
205
 
147
- ```ts
148
- // All features are on by default turn off what you don't need:
149
- supportPlugin({ features: { chat: false, pendingEmails: false }, locale: 'en' })
150
- ```
206
+ Because it is a copy, the portal is yours to restyle and re-brand; the trade-off is that
207
+ plugin upgrades do not update it. Diff it against
208
+ `node_modules/@consilioweb/payload-support/src/portal` after a version bump.
151
209
 
152
- ### Advanced
210
+ ## Configuration
153
211
 
154
- ```ts
155
- supportPlugin({
156
- features: { ai: true, sla: true, roundRobin: true, webhooks: true, snooze: true },
157
- rateLimitStore: 'payload', // shared, persistent limits for multi-instance deployments
158
- ticketNumber: { prefix: 'TK-', padding: 6 },
159
- ai: { provider: 'ollama', model: 'qwen2.5', baseUrl: process.env.OLLAMA_API_URL },
160
- email: { fromName: 'Support ACME', fromAddress: 'support@acme.com', replyTo: 'support@acme.com' },
161
- allowedEmailDomains: ['acme.com'], // restrict OAuth auto-registration
162
- collectionSlugs: { tickets: 'support-tickets' }, // slug overrides
163
- navGroup: 'Support',
164
- basePath: '/support',
165
- })
212
+ ### Plugin options
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `features` | `SupportFeatures` | see below | Build-time feature flags — decide which collections, endpoints and views exist. |
217
+ | `rateLimitStore` | `RateLimitStore \| 'payload'` | in-memory | Shared storage for the endpoint rate limits. `'payload'` adds the `support-rate-limits` collection. |
218
+ | `ticketNumber` | `{ prefix?, padding? }` | `{ prefix: 'TK-', padding: 4 }` | Sequential ticket-number formatting, backed by an atomic counter. |
219
+ | `capabilities` | `SupportCapabilities` | — | Host adapters for SMS, inbound email, digests, AI titles/summaries, detailed billing, volunteering, thread cleanup and project suggestions. See [Deployment adapters](#deployment-adapters). |
220
+ | `basePath` | `string` | `'/support'` | Prefix of the admin view routes (`/admin<basePath>/inbox`, …). The conversation-import view is the exception: it is registered at the fixed `/admin/import-conversation`. |
221
+ | `userCollectionSlug` | `string` | `'users'` | The staff/agent auth collection. |
222
+ | `allowedEmailDomains` | `string[]` | — | Restricts Google OAuth **auto-registration** to these domains. Existing accounts are unaffected. |
223
+ | `collectionSlugs` | `object` | see [Collections](#collections) | Per-collection slug overrides. |
224
+ | `notificationSlug` | `string` | `'admin-notifications'` | Collection the plugin writes admin notifications to. |
225
+ | `conversationComponent` | `string` | `'@consilioweb/payload-support/components/TicketConversation'` | Import path of the conversation UI field rendered on a ticket. |
226
+ | `projectCollectionSlug` | `string` | — | When set, adds a `project` relationship to tickets (and enables per-project billing). |
227
+ | `documentsCollectionSlug` | `string` | — | When set, adds quote/invoice upload fields to tickets. |
228
+ | `skipCollections` | `boolean` | `false` | Do not inject the collections. |
229
+ | `skipViews` | `boolean` | `false` | Do not inject the admin views. |
230
+ | `skipEndpoints` | `boolean` | `false` | Do not inject the REST endpoints. |
231
+
232
+ ### Options accepted but not read
233
+
234
+ Four keys are part of `SupportPluginConfig` but no code path reads them. They are listed here so you
235
+ do not configure them and wonder why nothing changes:
236
+
237
+ | Option | What actually decides | Where to set it |
238
+ |---|---|---|
239
+ | `ai` (`AIProviderConfig`) | `settings.ai.provider` / `settings.ai.model` | Admin → Support → Settings, or `POST /api/support/settings`. Defaults to `anthropic` / `claude-haiku-4-5-20251001`. |
240
+ | `email` (`EmailConfig`) | `settings.email.*`, falling back to `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | Same settings view, or the environment. |
241
+ | `locale` | The per-user preference | `GET`/`POST /api/support/user-prefs` (defaults to `fr`). |
242
+ | `navGroup` | — | Nowhere: each collection hard-codes its own `admin.group`. 20 sit under `Support`, three under `Gestion` (`canned-responses`, `ticket-activity-log`, `time-entries`), and `support-counters` / `support-rate-limits` declare none — they are `hidden` anyway. |
243
+
244
+ ### Build-time feature flags
245
+
246
+ `supportPlugin({ features })` accepts `SupportFeatures`. Every flag defaults to `true` except
247
+ `roundRobin` and `customStatuses`; `autoCloseDays` defaults to `7`.
248
+
249
+ | Flag | Default | Turning it off removes |
250
+ |---|---|---|
251
+ | `ai` | `true` | `client-summaries`, `/support/ai`, `/support/ai-agent`, `/support/client-intelligence`, `/support/ticket-synthesis` |
252
+ | `timeTracking` | `true` | `time-entries`, `/support/billing`, `/support/billing/invoice`, the Time dashboard view |
253
+ | `chat` | `true` | `chat-messages`, the four chat endpoints, the Chat view |
254
+ | `emailTracking` | `true` | `email-logs`, `/support/email-stats`, `/support/track-open`, the Tracking view |
255
+ | `pendingEmails` | `true` | `pending-emails`, `/support/pending-emails/:id/process`, the Pending emails view |
256
+ | `sla` | `true` | `sla-policies`, `/support/sla-check` |
257
+ | `webhooks` | `true` | `webhook-endpoints` (and therefore every outbound delivery) |
258
+ | `macros` | `true` | `macros`, `/support/apply-macro` |
259
+ | `authLogs` | `true` | `auth-logs` |
260
+ | `customStatuses` | `false` | *(off by default)* `ticket-statuses`, `/support/statuses` |
261
+ | `autoClose` | `true` | `/support/auto-close`, `/support/send-reminder` |
262
+ | `autoCloseDays` | `7` | — (projection of `settings.autoClose.daysBeforeClose`) |
263
+ | `snooze` | `true` | `/support/process-snooze` |
264
+ | `scheduledReplies` | `true` | `/support/process-scheduled` |
265
+ | `satisfaction` | `true` | `/support/satisfaction` |
266
+ | `merge` | `true` | `/support/merge-tickets` |
267
+ | `splitTicket` | `true` | `/support/split-ticket` |
268
+ | `bulkActions` | `true` | `/support/bulk-action` |
269
+ | `collisionDetection` | `true` | `/support/typing`, `/support/presence` |
270
+ | `signatures` | `true` | `/support/signature` |
271
+ | `chatbot` | `true` | `/support/chatbot` |
272
+ | `roundRobin` | `false` | *(off by default)* `/support/round-robin-config` |
273
+ | `canned`, `externalMessages`, `clientHistory`, `activityLog` | `true` | Nothing at build time — they exist as runtime flags (below) and only hide UI sections. |
274
+ | `commandPalette`, `knowledgeBase` | `true` | Nothing — no code reads either flag, at build time or at runtime. |
275
+
276
+ ### Build-time vs runtime feature flags
277
+
278
+ Two distinct things share the word "features", and they act at different moments:
279
+
280
+ | | `supportPlugin({ features })` | Runtime flags (Settings view) |
281
+ |---|---|---|
282
+ | Type | `SupportFeatures` (28 keys) | `TicketingFeatures` (16 keys) |
283
+ | Decides | which collections and endpoints **exist** | which UI sections are **shown**, plus round-robin and auto-close |
284
+ | Changed by | editing `payload.config.ts` + redeploy | an admin, in the ticketing settings view |
285
+ | Stored in | code | the `features` block of the `support-settings` preference row |
286
+
287
+ Turning a feature off at build time removes its collection and endpoints, so the matching runtime
288
+ flag has nothing left to show. Runtime flags only ever narrow what a build-time-enabled feature
289
+ exposes.
290
+
291
+ Runtime flags are read through `GET /api/support/settings` and written through
292
+ `POST /api/support/settings` (admin-only). They apply to every browser and to server-side code — the
293
+ auto-assign hook and the auto-close cron read the same values. `localStorage` is still written, but
294
+ only as a cache so an admin screen stays usable when the settings call fails; it is never
295
+ authoritative.
296
+
297
+ Two flags are projections, deliberately not stored twice: `features.autoClose` mirrors
298
+ `settings.autoClose.enabled`, and `features.autoCloseDays` mirrors
299
+ `settings.autoClose.daysBeforeClose`.
300
+
301
+ > **Upgrading from ≤ 2.0.1** — flags used to live in each browser's `localStorage`
302
+ > (`ticketing_features`). The first admin view loaded after the upgrade pushes that browser's values
303
+ > to the server, once, if the server has none yet; from then on the server wins. Nothing to run by
304
+ > hand, nothing is lost. If several machines disagree, the first one to load seeds the server and the
305
+ > others adopt its copy.
306
+
307
+ ### Environment variables
308
+
309
+ | Variable | Required | Description |
310
+ |---|---|---|
311
+ | `PAYLOAD_SECRET` | yes | Payload secret. Also signs the 2FA tokens and the email tracking pixels; the 2FA endpoint refuses to operate without it. |
312
+ | `NEXT_PUBLIC_SERVER_URL` | yes | Public URL used to build every email and portal link. |
313
+ | `CRON_SECRET` | for the cron endpoints | Expected value of the `x-cron-secret` header. |
314
+ | `ANTHROPIC_API_KEY` | with the `anthropic` provider | Anthropic API key. |
315
+ | `OLLAMA_API_URL` | with the `ollama` provider | Base URL of your own AI gateway, used as the SDK `baseURL`. **Mandatory** — there is no default host, and the AI paths throw `500 "provider 'ollama' requires OLLAMA_API_URL"` rather than sending ticket content anywhere you did not choose. |
316
+ | `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | for portal OAuth | Google sign-in on the portal. |
317
+ | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | for Web Push | Generate with `npx web-push generate-vapid-keys`. |
318
+ | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL). Defaults to `mailto:support@example.com`. |
319
+ | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | Fallback sender / reply-to addresses when the settings row leaves them empty. |
320
+ | `SUPPORT_CLIENT_BCC` | optional | Archive mailbox blind-copied on client-facing emails. Use a dedicated address, not `support@`, or the inbound anti-loop guard will re-create tickets from the copies. |
321
+ | `SUPPORT_TEAM_SCOPING` | optional | `1` or `true` to scope agents to their own team's tickets. |
322
+ | `SUPPORT_WEBHOOK_SECRET` | for machine imports | Expected value of the `x-webhook-secret` header on `POST /api/support/import-conversation`. |
323
+ | `CHANNELS_WEBHOOK_SECRET` | for social channels | Expected value of the `x-channel-secret` header on `POST /api/support/channels/webhook`. |
324
+ | `WHATSAPP_TOKEN` / `WHATSAPP_PHONE_ID` | for WhatsApp replies | Outbound WhatsApp credentials. |
325
+ | `MESSENGER_PAGE_TOKEN` | for Messenger replies | Outbound Messenger page token. |
326
+ | `CONTACT_EMAIL` | optional | Address treated as "the admin" when importing an existing email conversation. |
327
+ | `NEXT_PUBLIC_ENABLE_CHAT_WIDGET` | to show the portal chat widget | `'true'` mounts the floating live-chat widget in the portal `(auth)` layout. Anything else — including leaving it unset — makes `ChatWidget` return `null`, so `features.chat: true` on its own displays nothing to clients. |
328
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | for the portal ticket page | Support phone number shown on the portal ticket detail page. Set it: the fallback is the placeholder `01 23 45 67 89`. |
329
+ | `NEXT_PUBLIC_SUPPORT_SEND_ALIASES` | optional | Comma-separated "send as" identities offered in the agent composer. Empty by default, and then the composer offers only the agent themselves. |
330
+ | `NEXT_PUBLIC_SMTP_HOST` / `NEXT_PUBLIC_SMTP_PORT` | optional | Shown read-only in the Settings view. Informational only — mail is sent through `payload.sendEmail`, so these change nothing about delivery. |
331
+
332
+ ### Cron jobs
333
+
334
+ Four endpoints are meant to be called by a scheduler and are guarded by the `x-cron-secret` header.
335
+ Note that `auto-close` is a **GET**:
336
+
337
+ ```bash
338
+ # Hourly
339
+ curl -X GET https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
340
+ curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
341
+ curl -X POST https://your-app/api/support/process-scheduled -H "x-cron-secret: $CRON_SECRET"
342
+
343
+ # Daily / weekly
344
+ curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" \
345
+ -H "Content-Type: application/json" -d '{"frequency":"daily"}'
166
346
  ```
167
347
 
168
- ### Deployment adapters
348
+ ### Automation rules
349
+
350
+ `automation-rules` is edited in the admin, no code required. A rule has a trigger
351
+ (`ticket_created`, `ticket_updated` or `ticket_status_changed`), a match mode (`all` / `any`), a list
352
+ of conditions (field, `equals` / `not_equals` / `contains`, value) and a list of actions
353
+ (`set_status`, `set_priority`, `set_category`, `assign`, `add_tag`). For example: trigger
354
+ `ticket_created`, condition `category equals bug`, action `set_priority = urgent`.
169
355
 
170
- Version 2 keeps provider-specific code in the host application while the plugin owns the
171
- generic support workflow. Optional capabilities include SMS, inbound email, digests, AI
172
- titles and summaries, detailed billing, volunteering, thread cleanup and project suggestions.
356
+ ## Deployment adapters
357
+
358
+ The plugin owns the generic support workflow; anything provider-specific stays in your application
359
+ and is injected through `capabilities`. Every key is optional and a missing adapter simply disables
360
+ the corresponding behaviour (`inboundEmail`, `projectSuggestions` and `aiTitles` also gate the
361
+ endpoints they need).
173
362
 
174
363
  ```ts
175
364
  supportPlugin({
@@ -192,24 +381,69 @@ supportPlugin({
192
381
  })
193
382
  ```
194
383
 
195
- ### Upgrading from 1.x
196
-
197
- Version 2.0 is a security and ownership release with intentional breaking changes:
198
-
199
- 1. Generate an additive Payload migration for the new `support-counters` collection and,
200
- when `rateLimitStore: 'payload'` is enabled, `support-rate-limits`. Run it before restart.
201
- 2. Regenerate Payload types and the admin import map.
202
- 3. Remove duplicated host routes and enable the plugin endpoints (`skipEndpoints: false`).
203
- 4. Read portal authentication exclusively from the `HttpOnly` cookie. Login, OAuth and 2FA
204
- responses no longer expose the JWT in JSON.
205
- 5. Send cron and webhook secrets only through their configured headers. Query-string secrets
206
- are rejected.
207
- 6. Custom rate-limit stores must implement the asynchronous `RateLimitStore` interface.
208
-
209
- The process-local memory store remains the default for development and single-instance use.
210
- Use `rateLimitStore: 'payload'` or a shared custom store in production with multiple instances.
211
-
212
- ### Run the AI agent on a ticket
384
+ | Capability | Shape | Effect |
385
+ |---|---|---|
386
+ | `sms` | `{ adapter, buildMessage? }` | Sends an SMS alongside the client notification. |
387
+ | `digests` | `boolean` | **Inert** — no code reads it. `notification-queue` and `POST /api/support/process-digests` are registered unconditionally. |
388
+ | `inboundEmail` | `{ handle, secret?, secretHeader? }` | Registers `POST /api/support-webhook/inbound-email`, validated and rate-limited by the plugin. |
389
+ | `aiTitles` | `{ generate }` | Registers `POST /api/support/ticket-title` and `POST /api/support/generate-missing-titles`, and adds the title fields on clients. |
390
+ | `aiSummaries` | `{ generate }` | Overrides the default resolved-ticket synthesis generator. |
391
+ | `detailedBilling` | `boolean` | Adds the billing-line fields and their invoicing state. |
392
+ | `volunteering` | `boolean` | Adds pro-bono tracking and its estimated value. |
393
+ | `threadCleanup` | `{ clean }` | Best-effort cleanup of noisy messages after an inbound client reply. |
394
+ | `projectSuggestions` | `{ suggest }` | Registers `POST /api/support/suggest-projects`. |
395
+
396
+ ## API Endpoints
397
+
398
+ All paths are relative to `/api`. "Staff" means a user of `userCollectionSlug` (default `users`);
399
+ "client" means a `support-clients` user. The *Flag* column names the build-time feature flag that has
400
+ to stay enabled for the endpoint to be registered.
401
+
402
+ ### Tickets and agent tools
403
+
404
+ | Method | Path | Access | Flag | Purpose |
405
+ |---|---|---|---|---|
406
+ | `GET` | `/support/search` | staff | — | Global search over tickets, messages, clients and the knowledge base. |
407
+ | `GET` | `/support/kb/search` | public | — | Knowledge-base search (used by the portal and the chatbot). |
408
+ | `GET` | `/support/statuses` | authenticated | `customStatuses` | Ticket statuses, sorted. |
409
+ | `POST` | `/support/bulk-action` | staff | `bulkActions` | Apply one action to many tickets. |
410
+ | `POST` | `/support/merge-tickets` | staff | `merge` | Merge a source ticket into a target. |
411
+ | `POST` | `/support/split-ticket` | staff | `splitTicket` | Extract a message into a new ticket. |
412
+ | `POST` | `/support/apply-macro` | staff | `macros` | Run a multi-action macro on a ticket. |
413
+ | `POST` | `/support/tickets/:id/escalate` | staff | — | Move a ticket to `escalated`. |
414
+ | `POST` | `/support/tickets/:id/transfer` | staff or ticket owner | — | Email a conversation recap to an external address. |
415
+ | `POST` | `/support/tickets/:id/invite` | staff or ticket owner | — | Invite a collaborator on the ticket. |
416
+ | `POST` | `/support/tickets/:id/feedback` | client | — | Submit a rating (1-5) and an optional comment. |
417
+ | `POST` | `/support/satisfaction` | client | `satisfaction` | Submit the CSAT survey of a resolved ticket. |
418
+ | `POST` | `/support/send-reminder` | staff | `autoClose` | Nudge a client whose ticket awaits their reply. |
419
+ | `POST` | `/support/resend-notification` | staff | — | Re-send the email notification of one message. |
420
+ | `POST` | `/support/import-conversation` | staff, or `x-webhook-secret` | — | Import an existing email thread as a ticket. |
421
+ | `GET`/`POST` | `/support/typing` | authenticated | `collisionDetection` | Typing indicator. |
422
+ | `GET`/`POST` | `/support/presence` | staff | `collisionDetection` | Who is looking at this ticket. |
423
+ | `GET`/`POST` | `/support/signature` | staff | `signatures` | Per-agent email signature. |
424
+ | `GET`/`POST` | `/support/user-prefs` | staff | — | Per-user locale and signature. |
425
+
426
+ ### Portal authentication and GDPR
427
+
428
+ | Method | Path | Access | Purpose |
429
+ |---|---|---|---|
430
+ | `POST` | `/support/login` | public, rate-limited | Portal password login. Sets an `HttpOnly` cookie; the JWT is never returned in JSON. |
431
+ | `POST` | `/support/2fa` | public, rate-limited | Email second factor. |
432
+ | `POST` | `/support/oauth/google` | public | Google sign-in, optionally restricted by `allowedEmailDomains`. |
433
+ | `GET` | `/support/export-data` | client | GDPR data export. |
434
+ | `POST` | `/support/delete-account` | client | GDPR right to erasure. |
435
+ | `POST` | `/support/merge-clients` | staff | Merge two client records. |
436
+
437
+ ### AI
438
+
439
+ | Method | Path | Access | Flag | Purpose |
440
+ |---|---|---|---|---|
441
+ | `POST` | `/support/ai` | staff, rate-limited | `ai` | `sentiment`, `synthesis`, `suggest_reply` or `rewrite`. |
442
+ | `POST` | `/support/ai-agent` | staff, rate-limited | `ai` | Autonomous agent: answers from the knowledge base or escalates. |
443
+ | `POST` | `/support/ticket-synthesis` | staff, rate-limited | `ai` | Generate (or return the cached) per-ticket synthesis. |
444
+ | `GET`/`POST` | `/support/client-intelligence` | staff, rate-limited | `ai` | Cached client summary. |
445
+ | `POST` | `/support/chatbot` | public, rate-limited | `chatbot` | Knowledge-base chatbot used for deflection. |
446
+ | `POST` | `/support/seed-kb` | staff | — | Seed the knowledge base. |
213
447
 
214
448
  ```ts
215
449
  await fetch('/api/support/ai-agent', {
@@ -219,300 +453,300 @@ await fetch('/api/support/ai-agent', {
219
453
  })
220
454
  ```
221
455
 
222
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
223
-
224
- ## 🔌 API Reference
225
-
226
- ### `supportPlugin(config)`
227
-
228
- The Payload plugin. Adds collections, admin views, editor components and API endpoints.
229
-
230
- ```typescript
231
- supportPlugin(config?: SupportPluginConfig): Plugin
456
+ ### Live chat
457
+
458
+ | Method | Path | Access | Flag | Purpose |
459
+ |---|---|---|---|---|
460
+ | `GET`/`POST` | `/support/chat` | client | `chat` | Portal chat polling and send. |
461
+ | `GET` | `/support/chat-stream` | client | `chat` | SSE stream for the portal widget. |
462
+ | `GET`/`POST` | `/support/admin-chat` | staff | `chat` | Agent console: sessions, messages, send. |
463
+ | `GET` | `/support/admin-chat-stream` | staff | `chat` | SSE stream of the session list, or of one session's messages with `?session=`. Available for a custom agent console — the shipped Chat view polls `/support/admin-chat` instead. |
464
+
465
+ ### Reporting, billing and email
466
+
467
+ | Method | Path | Access | Flag | Purpose |
468
+ |---|---|---|---|---|
469
+ | `GET` | `/support/admin-stats` | staff | — | Dashboard KPIs, volume series, CSAT and NPS. |
470
+ | `GET` | `/support/export-csv` | staff | — | CSV export of the tickets. |
471
+ | `GET` | `/support/sla-check` | staff | `sla` | Tickets breaching or close to breaching SLA. |
472
+ | `GET` | `/support/billing` | staff | `timeTracking` | Pre-billing for a period, optionally per project. |
473
+ | `GET` | `/support/billing/invoice` | staff | `timeTracking` | Invoice for the period, `format=html\|pdf`. |
474
+ | `GET` | `/support/email-stats` | authenticated | `emailTracking` | Email open/sent aggregates. |
475
+ | `GET` | `/support/track-open` | public, HMAC-signed | `emailTracking` | Tracking pixel. |
476
+ | `POST` | `/support/pending-emails/:id/process` | staff | `pendingEmails` | Turn a pending email into a ticket or a reply. |
477
+ | `DELETE` | `/support/purge-logs` | staff | — | Delete logs older than N days. |
478
+
479
+ ### Settings, notifications and machine-to-machine
480
+
481
+ | Method | Path | Access | Flag | Purpose |
482
+ |---|---|---|---|---|
483
+ | `GET`/`POST` | `/support/settings` | staff | — | Read and write the support settings, including the runtime feature flags. |
484
+ | `GET`/`POST` | `/support/round-robin-config` | staff | `roundRobin` | Read and toggle round-robin assignment. |
485
+ | `GET` | `/support/push/vapid-public-key` | public | — | VAPID public key for the browser subscription. |
486
+ | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. |
487
+ | `GET` | `/support/auto-close` | `x-cron-secret` | `autoClose` | Remind, then close, inactive tickets. |
488
+ | `POST` | `/support/process-snooze` | `x-cron-secret` | `snooze` | Wake snoozed tickets. |
489
+ | `POST` | `/support/process-scheduled` | `x-cron-secret` | `scheduledReplies` | Release scheduled replies. |
490
+ | `POST` | `/support/process-digests` | `x-cron-secret` | — | Send the daily or weekly digests. |
491
+ | `POST` | `/support/channels/webhook` | `x-channel-secret` | — | Normalized inbound WhatsApp / Messenger webhook. |
492
+ | `POST` | `/support-webhook/inbound-email` | configured secret header | `capabilities.inboundEmail` | Inbound email, validated and bounded by the plugin. |
493
+ | `POST` | `/support/suggest-projects` | staff | `capabilities.projectSuggestions` | Deployment-specific project suggestions. |
494
+ | `POST` | `/support/ticket-title` | staff | `capabilities.aiTitles` | Generate a short display title for one ticket. |
495
+ | `POST` | `/support/generate-missing-titles` | staff | `capabilities.aiTitles` | Backfill the missing display titles. |
496
+
497
+ ## Webhooks
498
+
499
+ Create a row in `webhook-endpoints` with a URL, a `secret` and the events you want, and set it
500
+ `active`. Each event is delivered **once** per matching endpoint (up to 50, 10 s timeout each), as a
501
+ single `POST`:
502
+
503
+ ```http
504
+ Content-Type: application/json
505
+ User-Agent: PayloadSupport-Webhook/1.0
506
+ X-Webhook-Signature: <HMAC-SHA256 of the raw body, keyed with the endpoint secret>
507
+
508
+ { "event": "ticket_created", "data": { … }, "timestamp": "2026-09-07T10:00:00.000Z" }
232
509
  ```
233
510
 
234
- ### Exports
511
+ The signature header is only sent when the endpoint has a `secret`, so always set one. There is no
512
+ `X-Webhook-Secret` header any more — verify the HMAC signature instead. `lastTriggeredAt` and
513
+ `lastStatus` are written back on the endpoint after every attempt. Requires `features.webhooks`.
514
+
515
+ | Event | `data` |
516
+ |---|---|
517
+ | `ticket_created` | `ticketId`, `id`, `ticketNumber`, `subject`, `status`, `priority`, `category` |
518
+ | `ticket_resolved` | `ticketId`, `id`, `ticketNumber`, `subject`, `previousStatus` |
519
+ | `ticket_assigned` | `ticketId`, `id`, `ticketNumber`, `subject`, `assignedTo` |
520
+ | `ticket_replied` | `ticketId`, `messageId`, `authorType`, `body` (first 500 characters), plus `scheduled: true` when released by the scheduler |
521
+ | `sla_breached` | Selectable on an endpoint, but nothing dispatches it — subscribing to it delivers nothing. |
522
+
523
+ ## Collections
524
+
525
+ "Staff" is `userCollectionSlug` (default `users`), "client" is a `support-clients` user.
526
+
527
+ `collectionSlugs` accepts 19 keys: `tickets`, `ticketMessages`, `supportClients`, `timeEntries`,
528
+ `cannedResponses`, `ticketActivityLog`, `satisfactionSurveys`, `knowledgeBase`, `chatMessages`,
529
+ `pendingEmails`, `emailLogs`, `authLogs`, `webhookEndpoints`, `slaPolicies`, `macros`,
530
+ `ticketStatuses`, `ticketFeedback`, `rateLimits` and `counters`. Three caveats:
531
+
532
+ - **`media` has no key, and it is the blocking one.** The internal `CollectionSlugs` type carries a
533
+ `media` slug (default `media`) and uses it as the `relationTo` of the attachment fields on
534
+ `ticket-messages` and `pending-emails`, but `collectionSlugs` exposes no way to set it. If your
535
+ uploads collection is named anything else, Payload throws `InvalidFieldRelationship` at config
536
+ sanitisation and the whole `payload.config.ts` fails to build. Your uploads collection has to be
537
+ named `media`.
538
+ - Three slugs are genuinely fixed: `ticketFeedback` is accepted but ignored, and
539
+ `ticket-collaborators` and `client-summaries` have no key at all — those three files write their
540
+ own `slug:` as a literal.
541
+ - `notificationQueue`, `automationRules`, `supportTeams` and `pushSubscriptions` are a different
542
+ case: their collections *do* read `slugs.*`, only the public option type omits the keys. The
543
+ plumbing is there; widening the type would be enough.
544
+
545
+ | Slug | Injected | Role | Read | Write |
546
+ |---|---|---|---|---|
547
+ | `tickets` | always | The tickets themselves | staff; clients see the tickets they own or collaborate on | staff; a client create keeps only `subject`, `category`, `priority`, `project` |
548
+ | `ticket-messages` | always | Conversation, internal notes, scheduled replies | staff; clients see non-internal, already-sent messages of their tickets | staff; clients only on an accessible ticket, and may edit only their own messages |
549
+ | `support-clients` | always | Portal auth collection (2FA, OAuth, notification preferences) | staff; a client reads their own row | staff; a client updates their own row minus `tier`, `accountManager`, `opportunities`, `notes`, `googleId` and the 2FA internals |
550
+ | `ticket-collaborators` | always | Extra clients invited on a ticket — source of truth for client read scope | staff and the invited client | staff only (`update` denied to everyone); the invite endpoint writes with `overrideAccess`. **Fixed slug.** |
551
+ | `ticket-activity-log` | always | Append-only audit trail | staff; clients on their own tickets | nobody — hooks only (`create`/`update` denied) |
552
+ | `canned-responses` | always | Quick reply templates | staff | staff |
553
+ | `satisfaction-surveys` | always | CSAT after resolution | staff and the surveyed client | immutable once submitted |
554
+ | `ticket-feedback` | always | Portal rating and comment | staff and the author | immutable once submitted. **Fixed slug.** |
555
+ | `knowledge-base` | always | FAQ and chatbot corpus | staff; published entries publicly | staff |
556
+ | `notification-queue` | always | Digest queue | staff | hooks only |
557
+ | `automation-rules` | always | Event → conditions → actions | staff | staff |
558
+ | `support-teams` | always | Teams, used by team SLA and `SUPPORT_TEAM_SCOPING` | staff | staff |
559
+ | `push-subscriptions` | always | Agent Web Push subscriptions | staff | the subscribe endpoint only |
560
+ | `support-counters` | always | Atomic ticket-number counter | nobody | internal only |
561
+ | `auth-logs` | `authLogs` | Portal authentication audit | staff | system-generated |
562
+ | `time-entries` | `timeTracking` | Time spent, rolled up into `tickets.totalTimeMinutes` | staff | staff |
563
+ | `email-logs` | `emailTracking` | Sent/opened tracking | staff | system-generated |
564
+ | `webhook-endpoints` | `webhooks` | Outbound webhook subscriptions | staff | staff |
565
+ | `sla-policies` | `sla` | Default and per-team SLA targets | staff | staff |
566
+ | `macros` | `macros` | Multi-action shortcuts | staff | staff |
567
+ | `chat-messages` | `chat` | Live chat sessions | staff and the session's client | authenticated |
568
+ | `pending-emails` | `pendingEmails` | Inbound emails awaiting triage | staff | staff, or a create carrying a valid `x-webhook-secret` |
569
+ | `ticket-statuses` | `customStatuses` | Configurable statuses | authenticated | staff |
570
+ | `client-summaries` | `ai` | Cached client-intelligence summaries | staff | staff. **Fixed slug**, and its access rules compare against the literal `users` collection, so a renamed staff collection loses access to it. |
571
+ | `support-rate-limits` | `rateLimitStore: 'payload'` | Shared rate-limit counters | nobody | internal only |
572
+
573
+ ## Package Exports
574
+
575
+ | Subpath | Exposes | Formats | Environment |
576
+ |---|---|---|---|
577
+ | `@consilioweb/payload-support` | `supportPlugin`, the collection factories, the SLA and status-email hooks, `dispatchWebhook`, `generateTicketSynthesis`, the rate-limit stores, the settings and feature helpers, and the public types | ESM + CJS, typed | Server (`payload.config.ts`) |
578
+ | `@consilioweb/payload-support/client` | `SUPPORT_CLIENT_VERSION` — the `'use client'` barrel marker | ESM + CJS, typed | Client |
579
+ | `@consilioweb/payload-support/views` | The 13 admin views, for the Payload import map | **ESM only**, typed | Server components |
580
+ | `@consilioweb/payload-support/components/TicketConversation` | The conversation field component | **ESM only**, typed | Client |
581
+
582
+ The two view/component subpaths lost their CJS build in 3.0.0: the emitted `.cjs` barrels
583
+ `require`d their ESM siblings and threw `ERR_REQUIRE_ESM`, so they were unreachable. Payload 3 and
584
+ Next are ESM-first; import them with ESM.
235
585
 
236
- ```typescript
237
- import { supportPlugin, generateTicketSynthesis, dispatchWebhook } from '@consilioweb/payload-support'
586
+ ```ts
587
+ import { supportPlugin, dispatchWebhook, generateTicketSynthesis } from '@consilioweb/payload-support'
238
588
  import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-support'
239
589
  ```
240
590
 
241
- ### Key HTTP endpoints (under `/api/support`)
591
+ ## Requirements
242
592
 
243
- | Method | Path | Purpose |
593
+ | Requirement | Range | Source |
244
594
  |---|---|---|
245
- | `POST` | `/ai` | Sentiment, reply suggestion, rewrite, synthesis (admin). |
246
- | `POST` | `/ai-agent` | **Autonomous AI agent** — answers from the KB or escalates. |
247
- | `POST` | `/login`, `/2fa`, `/oauth/google` | Client portal auth (password, 2FA, OAuth). |
248
- | `GET` | `/admin-stats` | Dashboard KPIs (real volume series, CSAT, NPS). |
249
- | `GET` | `/billing`, `/billing/invoice` | Pre-billing + print-ready invoice. |
250
- | `POST` | `/process-snooze`, `/process-digests`, `/process-scheduled`, `/auto-close` | Cron jobs (guarded by `x-cron-secret`). |
251
-
252
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
253
-
254
- ## ⚙️ Configuration
255
-
256
- | Option | Type | Default | Description |
257
- |--------|------|---------|-------------|
258
- | `features` | `SupportFeatures` | all `true` | Toggle each feature on/off. |
259
- | `ai` | `AIProviderConfig` | `anthropic` | AI provider: `anthropic` \| `openai` \| `ollama` \| `custom`. |
260
- | `email` | `EmailConfig` | | `fromName`, `fromAddress`, `replyTo`. |
261
- | `rateLimitStore` | `RateLimitStore \| 'payload'` | memory | Persistent/shared storage for endpoint limits. |
262
- | `ticketNumber` | `{ prefix?, padding? }` | `TK-`, no padding | Atomic sequential ticket-number formatting. |
263
- | `capabilities` | `SupportCapabilities` | | Optional host adapters for SMS, inbound email, AI and deployment-specific workflows. |
264
- | `locale` | `'fr' \| 'en'` | `'fr'` | Admin/portal language. |
265
- | `basePath` | `string` | `'/support'` | Admin views prefix. |
266
- | `userCollectionSlug` | `string` | `'users'` | Agents collection. |
267
- | `allowedEmailDomains` | `string[]` | | Domains allowed for OAuth auto-registration. |
268
- | `collectionSlugs` | `object` | | Collection slug overrides. |
269
- | `skipCollections` / `skipViews` / `skipEndpoints` | `boolean` | `false` | Skip injecting that part. |
270
-
271
- ### Environment variables
272
-
273
- | Env var | Required | Description |
274
- |---------|----------|-------------|
275
- | `PAYLOAD_SECRET` | | Payload secret (also signs 2FA & tracking). |
276
- | `NEXT_PUBLIC_SERVER_URL` | | Public URL (email/portal links). |
277
- | `CRON_SECRET` | for crons | `x-cron-secret` header for the cron endpoints. |
278
- | `ANTHROPIC_API_KEY` / `OLLAMA_API_URL` | if AI | AI provider keys/URL. |
279
- | `GOOGLE_OAUTH_CLIENT_ID` / `_SECRET` | if OAuth | Portal Google sign-in. |
280
- | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | if push | Web Push keys (`npx web-push generate-vapid-keys`). |
281
- | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL), defaults to `mailto:support@example.com`. |
282
- | `SUPPORT_TEAM_SCOPING` | optional | `1` to scope agents to their team's tickets. |
283
- | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | From/reply-to addresses. |
284
-
285
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
286
-
287
- ## Performance
288
-
289
- Engineered to stay fast under load (no fabricated benchmarksfigures depend on your volume and host):
595
+ | Node.js | `>=20.9.0` | `engines.node` |
596
+ | Payload | `^3.37.0` | `peerDependencies.payload` |
597
+ | `@payloadcms/next` | `^3.37.0` | `peerDependencies` |
598
+ | Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
599
+ | React / React DOM | `^19.0.0` | `peerDependencies` |
600
+ | `lucide-react` | `>=0.300.0` | `peerDependencies` |
601
+ | `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
602
+ | `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
603
+
604
+ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outright under
605
+ `engine-strict` or a strict peer resolver.
606
+
607
+ ## Security
608
+
609
+ - **Cross-client isolation** reads on `tickets`, `ticket-messages` and `ticket-activity-log` are
610
+ constrained to the ticket ids a client owns or collaborates on, resolved server-side and failing
611
+ closed on an empty set. Client writes are validated against that same set.
612
+ - **Allow-listed client writes** a client-initiated ticket create keeps only `subject`, `category`,
613
+ `priority` and `project`; `client`, `status` and `source` are forced. A client updating their own
614
+ profile cannot touch `tier`, `accountManager`, `opportunities`, `notes`, `googleId` or the 2FA
615
+ internals.
616
+ - **Cookie-only portal JWTs** `HttpOnly`, `SameSite=Lax`, `Path=/`, 2 h max age, and `Secure`
617
+ outside development. Login, OAuth and 2FA responses never return the token in JSON.
618
+ - **2FA enforced server-side** (`beforeLogin`), and the endpoint refuses to run without
619
+ `PAYLOAD_SECRET` rather than falling back to an insecure default. OAuth verifies the Google email.
620
+ - **Stored-XSS protection** — message HTML is sanitized on write, not on render.
621
+ - **HMAC everywhere it matters** — webhook deliveries and tracking pixels are signed and verified in
622
+ constant time, with idempotent writes. Cron and webhook secrets are read from headers only; a
623
+ query-string secret is rejected.
624
+ - **Persistent rate limiting** on login, 2FA, chat, invitations, transfers, imports and every AI
625
+ endpoint. Use `rateLimitStore: 'payload'` (or your own shared store) on multi-instance deployments.
626
+ - **No silent third-party AI host** — the `ollama` provider fails loudly on a missing
627
+ `OLLAMA_API_URL` instead of defaulting to someone else's server.
628
+ - **Bounded inbound email** payloads and attachments.
629
+
630
+ ### Reporting security issues
631
+
632
+ Email **contact@consilioweb.fr** instead of opening a public issue.
633
+
634
+ ### Best practices
635
+
636
+ - Set a strong `PAYLOAD_SECRET` and a real `CRON_SECRET`.
637
+ - Restrict read access on your `media` collection to the ticket owner.
638
+ - Put the app behind a trusted proxy so `x-forwarded-for` is meaningful.
639
+ - Rotate every webhook endpoint secret that existed before 3.0.0 the legacy transport sent it in
640
+ clear on every delivery.
641
+
642
+ ## Performance
643
+
644
+ No benchmarks are claimed here; the figures depend on your volume and host. What the code actually
645
+ does:
290
646
 
291
647
  | Concern | Approach |
292
- |---------|----------|
293
- | Settings reads | In-process cache (TTL + invalidation)avoids ~8 redundant DB reads per ticket mutation. |
294
- | List queries | SQLite indexes on filtered fields inbox/dashboard/SLA in `O(index)` instead of full scans. |
295
- | Email sends | Fire-and-forget the response no longer waits on the SMTP round-trip (−200-500 ms). |
296
- | Stats & billing | Paginated aggregation + bounded `select` never loads all tickets in memory. |
297
- | AI synthesis | Cached per ticket no LLM recompute on every view. |
298
-
299
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
300
-
301
- ## 📚 Examples
302
-
303
- ### Automation rule (no code)
304
-
305
- Create a **Automation Rules** entry: `event = ticket_created`, condition `category = bug`, action `set_priority = urgent`. New "bug" tickets become "urgent" automatically.
306
-
307
- ### Wire the cron jobs
308
-
309
- ```bash
310
- # Hourly: auto-close, snooze wake-up, scheduled replies
311
- curl -X POST https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
312
- curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
313
- # Daily / weekly: notification digests
314
- curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" -d '{"frequency":"daily"}'
315
- ```
316
-
317
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
318
-
319
- ## ❓ FAQ
320
-
321
- <details>
322
- <summary><b>Which databases are supported?</b></summary>
323
-
324
- Built and tested on **SQLite** (`@payloadcms/db-sqlite`) with a sequential seed and `busyTimeout`. It works with any Payload adapter, but indexes and queries were validated on SQLite.
325
-
326
- </details>
327
-
328
- <details>
329
- <summary><b>What Node / Payload versions are supported?</b></summary>
330
-
331
- Node.js 18+, Payload 3.x, React 18 or 19.
332
-
333
- </details>
334
-
335
- <details>
336
- <summary><b>Is there TypeScript support?</b></summary>
337
-
338
- Yes — strict TypeScript, with business types exported (`SupportPluginConfig`, `SupportFeatures`, …).
648
+ |---|---|
649
+ | Settings reads | In-process cache with a 60 s TTL and explicit invalidation on save the `afterChange` hook chain reads the same preference row many times per ticket mutation. |
650
+ | List queries | Indexed fields on `tickets` for the columns the inbox, dashboard and SLA views filter on. |
651
+ | Stats and billing | Paginated aggregation (100 rows per page) with a bounded `select` never loads all tickets in memory. |
652
+ | Counters | The inbox counters and the portal poller call `/api/*/count` instead of fetching rows. |
653
+ | Client time totals | The CRM sums the per-ticket `totalTimeMinutes` rollup instead of scanning `time-entries`. |
654
+ | AI synthesis | Persisted on the ticket (`aiSummary`, `aiSummaryGeneratedAt`) and reused until forced. |
339
655
 
340
- </details>
656
+ ## Troubleshooting
341
657
 
342
- <details>
343
- <summary><b>Do I need an external AI service?</b></summary>
658
+ **`useServerFunctions must be used within ServerFunctionsProvider`** — align every `@payloadcms/*`
659
+ package on the same version, 3.75 or later.
344
660
 
345
- No. AI features are optional and you can run **self-hosted Ollama** to depend on no cloud (sovereignty/GDPR). Anthropic and OpenAI are also supported.
661
+ **`SQLITE_BUSY` / database is locked during seed** make the seed sequential (no `Promise.all` on
662
+ inserts) and add `busyTimeout: 10000` to the SQLite adapter.
346
663
 
347
- </details>
664
+ **Admin views do not load** — regenerate the import map (`pnpm payload generate:importmap`). In a
665
+ headless context, pass `skipViews: true`.
348
666
 
349
- <details>
350
- <summary><b>How do I migrate the schema in production?</b></summary>
667
+ **New fields or collections missing in production** — standalone builds do not run migrations. Push
668
+ the schema before deploying, including `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`,
669
+ `mentions`, and the `notification-queue`, `automation-rules`, `ticket-collaborators`,
670
+ `support-counters` collections.
351
671
 
352
- In standalone mode the schema isn't auto-migrated: generate/push the schema (new fields and collections) before deploying. See [Troubleshooting](#-troubleshooting).
672
+ **`Cannot find module '@anthropic-ai/sdk'`** install it in your app; the plugin does not bundle or
673
+ declare it.
353
674
 
354
- </details>
675
+ **`ERR_REQUIRE_ESM` on `@consilioweb/payload-support/views`** — that subpath is ESM-only since 3.0.0.
355
676
 
356
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
677
+ ## FAQ
357
678
 
358
- ## 🔧 Troubleshooting
679
+ **Which databases are supported?** — built and tested on SQLite (`@payloadcms/db-sqlite`) with a
680
+ sequential seed and `busyTimeout`. It works with any Payload adapter, but the indexes and queries
681
+ were validated on SQLite.
359
682
 
360
- ### `useServerFunctions must be used within ServerFunctionsProvider`
683
+ **Do I need an external AI service?** — no. The AI features are optional, and `provider: 'ollama'`
684
+ points the SDK at the `OLLAMA_API_URL` you choose, so no ticket content has to leave your
685
+ infrastructure. `provider: 'openai'` and `provider: 'custom'` are accepted by the `AIProviderConfig`
686
+ type but not implemented: at runtime everything that is not `ollama` goes to Anthropic.
361
687
 
362
- Use Payload **3.75+** for all `@payloadcms/*` packages (aligned versions).
688
+ **Is TypeScript supported?** yes, strict, with the business types exported
689
+ (`SupportPluginConfig`, `SupportFeatures`, `TicketData`, …). All four export subpaths ship
690
+ declarations.
363
691
 
364
- ### `SQLITE_BUSY` / database is locked during seed
692
+ **How do I migrate the schema in production?** — in standalone mode the schema is not
693
+ auto-migrated: generate and push it before deploying. See [Troubleshooting](#troubleshooting).
365
694
 
366
- Make the seed **sequential** (no `Promise.all` on inserts) and add `busyTimeout: 10000` to the SQLite adapter.
695
+ ## Upgrading
367
696
 
368
- ### Admin views don't load
697
+ ### 2.x 3.0
369
698
 
370
- Regenerate the import map after adding components: `pnpm payload generate:importmap`. In a plugin/headless context, use `skipViews: true` if you don't mount the admin UI.
699
+ 1. **Rotate the secret of every webhook endpoint.** The legacy transport sent it in clear in
700
+ `X-Webhook-Secret` on every delivery. Subscribers must now verify `X-Webhook-Signature`.
701
+ 2. **Set `OLLAMA_API_URL`** before restarting if you use `provider: 'ollama'` — the hard-coded
702
+ fallback host is gone and its absence is now a hard error.
703
+ 3. **Move any integration that authenticated as a portal client** to a staff account or to a
704
+ server-side call with `overrideAccess: true`: client writes are scoped and allow-listed now.
705
+ 4. **Recompute `tickets.totalTimeMinutes`** where time entries were deleted under 2.x. The new
706
+ `afterDelete` hook only fires on future deletions, and the CRM now trusts the rollup instead of
707
+ recomputing it — so a stale total keeps over-billing until something touches a time entry on that
708
+ ticket.
709
+ 5. **Import `./views` and `./components/TicketConversation` with ESM** — their CJS builds are gone.
710
+ 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.37+, and install
711
+ `lucide-react` explicitly if you were relying on it being optional.
371
712
 
372
- ### New fields/collections missing in production
713
+ ### 1.x 2.0
373
714
 
374
- Standalone doesn't run migrations: push the schema for `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`, `mentions`, and the `notification-queue` / `automation-rules` collections.
375
-
376
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
377
-
378
- ## 🔐 Security
379
-
380
- Security is a first-class concern several guardrails are validated by integration tests.
381
-
382
- - **Cross-client isolation** a client can never read another's tickets/messages (filtered by owned tickets).
383
- - **2FA** enforced server-side (`beforeLogin`); **OAuth** verifies the Google email.
384
- - **Sanitization** of message HTML server-side (stored-XSS protection).
385
- - **HMAC-signed** webhooks and tracking pixels with constant-time verification and idempotent writes.
386
- - **Cookie-only JWTs** for portal authentication (`HttpOnly`, `Secure`, `SameSite=Lax`).
387
- - **Persistent rate limiting** for authentication, 2FA, chats, invitations, transfers and AI endpoints.
388
- - **Bounded inbound email** payloads and attachments; cron/webhook secrets are accepted in headers only.
389
-
390
- ### Reporting Security Issues
391
-
392
- Please email **contact@consilioweb.fr** instead of opening a public issue.
393
-
394
- ### Best Practices
395
-
396
- - ✅ Set a strong `PAYLOAD_SECRET` (never a default value)
397
- - ✅ Set `CRON_SECRET` to protect the cron endpoints
398
- - ✅ Restrict read access to the `media` collection to the ticket owner
399
- - ✅ Put the app behind a trusted proxy (`x-forwarded-for` header)
400
- - ✅ Keep the plugin up-to-date
401
-
402
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
403
-
404
- ## 🤝 Contributing
715
+ 1. Generate an additive Payload migration for the `support-counters` collection and, when
716
+ `rateLimitStore: 'payload'` is enabled, `support-rate-limits`. Run it before restart.
717
+ 2. Regenerate the Payload types and the admin import map.
718
+ 3. Remove duplicated host routes and enable the plugin endpoints (`skipEndpoints: false`).
719
+ 4. Read portal authentication exclusively from the `HttpOnly` cookie. Login, OAuth and 2FA responses
720
+ no longer expose the JWT in JSON.
721
+ 5. Send cron and webhook secrets only through their configured headers. Query-string secrets are
722
+ rejected.
723
+ 6. Custom rate-limit stores must implement the asynchronous `RateLimitStore` interface.
405
724
 
406
- Contributions are very welcome!
725
+ ## Contributing
407
726
 
408
- 1. **Fork** the repository
409
- 2. **Create** a feature branch (`git checkout -b feature/AmazingFeature`)
410
- 3. **Commit** your changes (`git commit -m 'feat: add AmazingFeature'`)
411
- 4. **Push** the branch (`git push origin feature/AmazingFeature`)
412
- 5. **Open** a Pull Request
727
+ 1. Fork the repository.
728
+ 2. Create a feature branch (`git checkout -b feature/AmazingFeature`).
729
+ 3. Commit your changes (`git commit -m 'feat: add AmazingFeature'`).
730
+ 4. Push the branch and open a Pull Request.
413
731
 
414
- Run the checks before submitting:
732
+ Run the checks before submitting — the suite is 179 vitest tests:
415
733
 
416
734
  ```bash
417
735
  pnpm typecheck && pnpm test && pnpm build
418
736
  ```
419
737
 
420
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
421
-
422
- ## 📝 Changelog
738
+ ## Changelog
423
739
 
424
740
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
425
741
 
426
- ### [2.0.1] — 2026-07-16
427
-
428
- - 🌍 Loads the complete shared FR/EN catalogs in every support admin view, fixing raw keys such as ticket statuses, relative dates, time tracking, tags and billing labels.
429
- - 🧭 Removes translation-key collisions in the support dashboard and settings views.
430
- - ✅ Adds regression coverage for all 518 literal translation keys used by the support UI and keeps both locales aligned (132 tests passing).
431
-
432
- ### [2.0.0] — 2026-07-16
433
-
434
- - 🔐 Security hardening for authentication, tracking pixels, webhook/cron secrets, HTML/AI output and inbound-email limits.
435
- - 🚦 Asynchronous persistent rate limiting with a Payload-backed store and memory fallback.
436
- - 🔢 Atomic persistent ticket counters with configurable prefix and padding.
437
- - 🧩 Typed deployment adapters for SMS, inbound email, AI workflows, billing, volunteering, cleanup and project suggestions.
438
- - 🏗️ The plugin is now the single owner of generic support collections, hooks, views and endpoints.
439
- - ✅ 124 tests covering the security regressions and new capabilities.
440
-
441
- ### [1.1.1] — 2026-06-26
442
-
443
- - 🐛 **Fix client ticket reopen**: allow clients to set `waiting_support` (the status the portal's "Reopen" button sends). It was silently rejected, leaving the ticket `resolved`.
444
-
445
- ### [1.1.0] — 2026-06-25
446
-
447
- - 🎯 **Per-team SLA policies & dashboards** (team policy overrides the default, `?teamId=` scoping).
448
- - 🔔 **Native push / browser notifications** (Web Push / VAPID, agent pushed on new client messages).
449
- - 🎭 **End-to-end UI test harness** (browser-driven admin via Playwright, `pnpm test:e2e`).
450
- - ✅ 109 integration tests (up from 102).
451
-
452
- ### [1.0.0] — 2026-06-25
453
-
454
- - ✨ Complete ticketing, SLA (+ pause-on-hold), automation & **visual rules engine**.
455
- - 🤖 AI (sentiment, suggestion, synthesis, chatbot) + **autonomous AI agent**.
456
- - 💬 Live chat, client portal (2FA, OAuth), knowledge base.
457
- - 💶 Time tracking, pre-billing & print-ready invoice.
458
- - 📊 CSAT + NPS, real volume series, digests, webhooks.
459
- - 🔐 Security hardening (cross-client isolation, anti-XSS, server-side 2FA).
460
- - ✅ Integration test harness (Payload + in-memory SQLite), 102 tests.
461
-
462
- <details>
463
- <summary><b>Previous versions (0.x)</b></summary>
464
-
465
- - **0.16.0** — Manual client reminder + auto-close after 24h
466
- - **0.15.0** — Next 16 compatibility + accumulated features
467
- - **0.9.0 → 0.9.13** — Enriched views (Client Intelligence, Billing), bundled RichTextEditor, code blocks in emails, inline message editing, enriched pre-billing, per-ticket cached AI synthesis, multi-style rewriting
468
- - **0.6.0 → 0.6.4** — Split build (`bundle:false`) for Next.js RSC compatibility + barrel exports
469
- - **0.5.0** — Full i18n across the 13 admin views
470
- - **0.4.0** — Per-user preferences (locale, signature) vs global settings
471
- - **0.3.0** — Feature parity with ConsilioWEB + `skipCollections` / `skipViews` / `skipEndpoints`
472
- - **0.2.0** — 2026-04-08 — Security overhaul, SSE live chat, webhooks, SLA, scheduled replies, client portal
473
- - **0.1.0** — 2026-04-08 — Initial scaffold: `supportPlugin()`, 15 collections, feature flags, AI provider abstraction
474
-
475
- </details>
476
-
477
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
478
-
479
- ## 🗺️ Roadmap
480
-
481
- - [x] Ticketing, SLA, time tracking & billing
482
- - [x] AI assists + autonomous AI agent
483
- - [x] Live chat, client portal, knowledge base
484
- - [x] Visual automation-rules engine
485
- - [x] Native binary PDF invoice (alongside the print-ready HTML)
486
- - [x] Full admin i18n (all views)
487
- - [x] Social channels (WhatsApp, Messenger)
488
- - [x] Multi-team / workspaces mode
489
- - [x] Per-team SLA policies & dashboards
490
- - [x] Native push / browser notifications
491
- - [x] End-to-end UI test harness (browser-driven admin)
492
-
493
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
494
-
495
- ## ☕ Support
496
-
497
- If this plugin saves you time, consider buying me a coffee!
498
-
499
- <a href="https://buymeacoffee.com/pown3d">
500
- <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" />
501
- </a>
502
-
503
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
742
+ ## Support
504
743
 
505
- ## 📄 License
744
+ If this plugin saves you time, consider [buying me a coffee](https://buymeacoffee.com/pown3d).
506
745
 
507
- Licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
746
+ ## License
508
747
 
509
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
748
+ MIT see [LICENSE](LICENSE).
510
749
 
511
- <div align="center" style="padding: 40px 0; color: #666; border-top: 1px solid #e0e0e0; margin-top: 50px;">
512
- <p style="margin: 10px 0;">Built and maintained by <a href="https://consilioweb.fr" style="color: #1f8a5b; text-decoration: none;">ConsilioWEB</a></p>
513
- <p style="margin: 10px 0; font-size: 13px;">
514
- <a href="https://github.com/pOwn3d/payload-support#readme" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Documentation</a>
515
- <a href="https://github.com/pOwn3d/payload-support/issues" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Issues</a>
516
- <a href="https://www.npmjs.com/package/@consilioweb/payload-support" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">npm</a>
517
- </p>
518
- </div>
750
+ Built and maintained by [ConsilioWEB](https://consilioweb.fr) ·
751
+ [Issues](https://github.com/pOwn3d/payload-support/issues) ·
752
+ [npm](https://www.npmjs.com/package/@consilioweb/payload-support)