domma-cms 1.6.60 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +4349 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/mail-compose.js +1 -0
  8. package/admin/js/lib/page-editor-reference.js +5 -5
  9. package/admin/js/lib/shortcode-tags.js +1 -1
  10. package/admin/js/templates/call-new.html +1 -1
  11. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  12. package/admin/js/views/call-new.js +1 -1
  13. package/admin/js/views/contacts.js +15 -15
  14. package/admin/js/views/form-submissions.js +2 -2
  15. package/docs/DommaFeatures.md +3005 -0
  16. package/docs/MultiTenant.md +176 -0
  17. package/docs/api-reference.md +1667 -0
  18. package/docs/cache.md +96 -0
  19. package/docs/configuration.md +1811 -0
  20. package/docs/editions-and-licences.md +190 -0
  21. package/docs/getting-started.md +201 -0
  22. package/docs/guides/form-triggers.md +260 -0
  23. package/docs/guides/mongodb-storage-adapter.md +282 -0
  24. package/docs/guides/multi-page-forms.md +121 -0
  25. package/docs/images/scribe-composer.png +0 -0
  26. package/docs/markdown-shortcodes.md +1738 -0
  27. package/docs/pentest-report-2026-04-03.md +181 -0
  28. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  29. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  30. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  31. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  32. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  34. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  35. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  37. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  38. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  39. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  40. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  42. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  43. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  44. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  45. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  46. package/docs/plugin-development.md +827 -0
  47. package/docs/recruitment-recipe.md +501 -0
  48. package/docs/scaffolding.md +312 -0
  49. package/docs/screenshots/calculator-floating.png +0 -0
  50. package/docs/screenshots/calculator-standard.png +0 -0
  51. package/docs/screenshots/email-settings.png +0 -0
  52. package/docs/screenshots/messages-count.png +0 -0
  53. package/docs/structure/user-types.md +107 -0
  54. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  55. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  56. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  58. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  59. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  60. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  61. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  62. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  63. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  64. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  65. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  66. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  67. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  68. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  69. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  70. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  71. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  72. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  73. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  74. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  75. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  76. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  77. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  78. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  79. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  80. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  81. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  82. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  83. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  84. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  85. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  86. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  87. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  89. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  90. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  91. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  92. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  93. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  95. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  96. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  97. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  98. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  99. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  100. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  101. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  102. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  103. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  106. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  107. package/docs/theming.md +320 -0
  108. package/package.json +2 -2
  109. package/plugins/_lib/admin/mail/compose-window.js +3 -1
  110. package/plugins/_lib/admin/mail/identity.js +18 -3
  111. package/plugins/_lib/admin/mail/reader-view.js +32 -3
  112. package/plugins/blog/CLAUDE.md +4 -0
  113. package/plugins/blog/admin/lib/email.js +191 -0
  114. package/plugins/blog/admin/views/comments.js +3 -1
  115. package/plugins/blog/plugin.json +2 -2
  116. package/plugins/blog/plugin.public.js +1 -1
  117. package/plugins/blog/tests/email.test.js +69 -0
  118. package/plugins/booking/CLAUDE.md +4 -0
  119. package/plugins/booking/admin/lib/email.js +191 -0
  120. package/plugins/booking/admin/views/panels.js +7 -3
  121. package/plugins/booking/plugin.json +1 -1
  122. package/plugins/booking/tests/email.test.js +69 -0
  123. package/plugins/free-tier.lock.json +27 -19
  124. package/plugins/shopping-cart/CLAUDE.md +4 -0
  125. package/plugins/shopping-cart/admin/lib/email.js +191 -0
  126. package/plugins/shopping-cart/admin/views/orders.js +11 -3
  127. package/plugins/shopping-cart/plugin.json +2 -2
  128. package/plugins/shopping-cart/tests/email.test.js +69 -0
  129. package/plugins/support/CLAUDE.md +4 -0
  130. package/plugins/support/admin/lib/email.js +191 -0
  131. package/plugins/support/admin/views/desk.js +4 -1
  132. package/plugins/support/admin/views/inbox.js +6 -2
  133. package/plugins/support/plugin.json +2 -2
  134. package/plugins/support/tests/email.test.js +69 -0
  135. package/public/js/celebrations/core/canvas.js +1 -1
  136. package/public/js/celebrations/index.js +1 -1
  137. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  138. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  139. package/public/js/celebrations/message-attrs.mjs +1 -0
  140. package/public/js/celebrations/themes/anniversary.js +1 -0
  141. package/public/js/celebrations/themes/autumn.js +1 -0
  142. package/public/js/celebrations/themes/birthday.js +1 -0
  143. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  144. package/public/js/celebrations/themes/calendar.js +1 -0
  145. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  146. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  147. package/public/js/celebrations/themes/christmas.js +1 -1
  148. package/public/js/celebrations/themes/easter.js +1 -0
  149. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  150. package/public/js/celebrations/themes/garden.js +1 -0
  151. package/public/js/celebrations/themes/graduation.js +1 -0
  152. package/public/js/celebrations/themes/greeting.js +1 -0
  153. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  154. package/public/js/celebrations/themes/money-rain.js +1 -0
  155. package/public/js/celebrations/themes/new-year.js +1 -0
  156. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  157. package/public/js/celebrations/themes/occasions.js +1 -0
  158. package/public/js/celebrations/themes/rain-control.js +1 -0
  159. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  160. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  161. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  162. package/public/js/celebrations/themes/summer-garden.js +1 -0
  163. package/public/js/celebrations/themes/winter.js +1 -0
  164. package/public/js/celebrations/trait-data.mjs +1 -1
  165. package/public/js/effects.js +1 -1
  166. package/scripts/build.js +4 -0
  167. package/server/services/markdown.js +5 -2
  168. package/server/templates/page.html +1 -1
@@ -0,0 +1,3005 @@
1
+ # Domma CMS - Feature Reference
2
+
3
+ The complete feature inventory for **Domma CMS 0.93.0** and the **Domma CMS Marketplace** plugins, swept from
4
+ the code on 2026-09-27. It is the source for tutorials, documentation and site updates: each feature says
5
+ what it does for a site owner, **Where:** to find it (admin screen, config file, shortcode, endpoint or source
6
+ file) and, where the CHANGELOG records it, **Since:** which version it arrived in.
7
+
8
+ Code was treated as the truth. Where docs disagree with it, the disagreement is listed in
9
+ [Appendix B](#appendix-b---doc-gaps-and-code-discrepancies) rather than repeated in the feature text.
10
+ `FEATURES.md` in the repo root is the older, marketing-style summary; this document supersedes it for detail.
11
+
12
+ ## At a glance
13
+
14
+ - **Content** - Markdown pages with YAML frontmatter, a visual page editor with live preview, version
15
+ history, drafts with share links, 30+ shortcodes, reusable blocks and components, a media library.
16
+ - **Design** - 37 built-in themes (28 public, 9 admin), auto day/night, per-element theme overrides,
17
+ a visitor theme switcher, multi-menu system with slots and overlays, authored right-click menus.
18
+ - **Data** - schema-defined collections on file or MongoDB storage, a 12-operator filter engine, saved
19
+ views, actions, a form builder with conditional logic, wizards and triggers, eight public display types.
20
+ - **API platform** - `/api/v1` collection REST API, the read-only `/api/content/v1` Content API, project-scoped API tokens, no-code API Builder endpoints.
21
+ - **Admin** - dashboard, live sidebar count badges, notifications, in-admin docs, tutorials and plugin guides,
22
+ projects, dd/mm/yyyy dates throughout, dynamic roles and permissions, built-in Tools (Analytics, Contacts, Notes, Todo, SEO) that can be switched off.
23
+ - **Operations** - Node 24, `npx` scaffolder and CLI, an updater that merges config, fleet management through
24
+ domma-cms-manager, response cache, built-in security hardening.
25
+ - **Marketplace** - signed `.dcmsplugin` packages, licensed paid plugins, safe upgrades with rollback,
26
+ `minCmsVersion` checks, user guides in the admin's Documentation folder, and the 25 plugins below.
27
+
28
+ ## Marketplace catalogue
29
+
30
+ | Plugin | Tier | Version | In one line |
31
+ |---|---|---|---|
32
+ | `analytics-pro` | paid | 2.3.4 | Visitors, visits and page views: where visitors came from (sources, campaigns, channels, country and city by GeoIP), IP addresses, each visit's pages in order, devices and browsers, busiest times and a live view. Cookieless. Several display styles, CSV/JSON export and a printable report. |
33
+ | `backup-pro` | paid | 0.1.4 | A site's own view of the backups Domma keeps of it: how they stand, every backup, Back up now, restore of the whole site or chosen collections, pages or media after a safety backup, downloads without secrets, and Activity - every backup, restore and change, including Domma's own, named and never hidden. Needs a site run by Domma. |
34
+ | `blog` | free | 1.9.6 | Public-facing blog with posts, categories and comments - layouts and listings built from editable blocks. |
35
+ | `blog-pro` | paid | 0.5.11 | Several blogs on one site, each with its own address, look and settings - dozens of layouts, self-hosted fonts, colours, page transitions, author pages, and privacy-first media: videos, audio, galleries, link cards and maps. |
36
+ | `booking` | free | 1.6.3 | Let people book appointments with you or your team, and rooms or equipment, from a booking page on your site (`/booking`, or the `[booking /]` shortcode). Opening hours, days off, buffers and notice; confirmation and reminder emails with a link to move or cancel; a Diary in the admin; an online booking's call chat kept on the booking, and Add people during its call. Book straight from a contact or an email; bookings show in the Calendar. |
37
+ | `booking-pro` | paid | 0.4.1 | More for Booking: online appointments as a video call on your site (optional host recording), Call now - open a booking's call at once or call anyone from a contact or an email (switches on the CMS's instant calls), a person and a room together, classes with places and a waiting list that fills a freed place by itself, repeat bookings, questions on the booking form, a daily limit and one booking per customer every N days. A lapse leaves Booking and every booking as they were. |
38
+ | `calendar` | paid | 1.15.4 | Events calendar - month, day and list views, online meetings as a video call on your site (with the call's chat kept, and Add people during the call), call anyone now from a contact or an email, all-day and recurring events, sharing with people and roles, RSVPs and an iCal feed. |
39
+ | `campaigns-pro` | paid | 1.0.2 | Marketing campaigns, one paid Tool: compose a message, send it to an audience imported from Contacts (shared with Surveys), schedule sends, attach an offer that closes by a date or a claim limit. Opens and clicks (cookieless), links tagged for Analytics, a linked Shop discount code, send a survey as a campaign, follow-up sequences, resend to non-openers, A/B subject tests, and a version-1 stats feed other Tools read. |
40
+ | `contacts-pro` | paid | 0.9.5 | Contacts, with more to them: your own custom fields, birthdays and ages with the ones coming up and a reminder in the sidebar, and groups with their own emblem and colours. Share a contact, several, or a whole group with people or roles - to view, or to edit. Call a contact now on a video call on your site. Works over the same contacts as the built-in Tool, so nothing moves and nothing is lost. |
41
+ | `data-transfer` | paid | 1.3.3 | Move collections between file and MongoDB storage, back them up and restore them (all or some, merged or replaced) - keeping every entry's id - and clone them (cloned entries get new ids, so references stay unambiguous). Every restore and transfer takes a safety snapshot first. |
42
+ | `docs` | paid | 2.8.1 | Document editor: Home and editor screens, sharing with Viewers/Editors, public links, import/export, send to page/blog/email, edit lock, Trash, version history, templates. |
43
+ | `docs-pro` | paid | 0.8.1 | Domma Spaces (publish a Docs folder at `/spaces` with a Kindle-style reader), Cloud copies (Google Drive, OneDrive, Dropbox, S3), Signatures (emailed signing links, typed or drawn, signed copy with a certificate page) and protected public links (expiry date, password). Adds to Docs. |
44
+ | `email-pro` | paid | 0.5.9 | Full mail: read, write, send and search, over a synchronised store. Search, threads, rules, scheduled sends, snooze and vacation replies need MongoDB; without it, reading, filing and sending still work. |
45
+ | `feedback` | free | 1.1.4 | Tell Domma what is broken, missing or good - straight from your admin - and see what became of it: seen, planned, in progress, done, with Domma's reply. |
46
+ | `garage` | paid | 1.4.3 | UK vehicle management with DVLA API lookup, save vehicles, and search history. |
47
+ | `invoice` | paid | 1.7.1 | Invoices and quotes. Issuer profiles, receivers importable from Contacts, line items, optional VAT, payments, archiving, CSV/Excel export, and a print-ready invoice you can email or download as a PDF. Quotes with a validity date and an emailed accept link - the client accepts or declines online, and one click turns an accepted quote into a draft invoice. Self-billing too: invoices raised on a supplier's behalf, marked and numbered their own way, with agreement review reminders. |
48
+ | `job-board` | paid | 2.0.0 | Job board with three kinds of account - companies, their agents and candidates - sign-up for each (companies when opened, approved by staff), credits that pay for publishing a role (Stripe, staff gifts, company-to-agent), IR35 on every contract, candidate bios, work history and preferences, agents onboarding and messaging their candidates, interviews, notes and reviews - every record scoped to who may see it. |
49
+ | `mail-reader` | free | 1.2.7 | Read-only IMAP mailbox reader. Connects to a standard IMAP server and reads mail without ever writing to it. |
50
+ | `messages` | paid | 1.7.2 | Internal user-to-user and group messaging with real-time delivery and video calls (what is typed in a call lands in the conversation; ring another team member into a call) - conversations you can archive, mark unread and search, drafts kept per conversation, attachments, and an unread badge in the sidebar. |
51
+ | `mongodb-adapter` | paid | 1.0.2 | Provision a dedicated MongoDB database per site. Credentials are managed centrally by the manager. |
52
+ | `onboard-pro` | paid | 1.0.1 | Onboarding journeys, one paid Tool: read/form/details/manual steps, a personal link per person, signature steps through Docs Pro, admin-approved account steps (never an administrator), due dates, automatic nudges and auto-enrolment from a contact group, countersigned steps, waive-with-a-reason, and a funnel showing where people stop. |
53
+ | `security` | free | 1.1.3 | Keeps a Domma CMS site safe: a health check of what is weak and how to fix it, sign-in lockout after repeated wrong passwords, password rules, a log of every sign-in, and blocking of the scanners that probe every site for WordPress, .env files and the like. |
54
+ | `security-pro` | paid | 1.3.2 | Everything in Security, plus two-factor sign-in (authenticator apps, recovery codes, required per role), a firewall that bans scanners for longer each time they return, per-route rate limits, country rules for the admin, breached-password checks and every signed-in session in one place. |
55
+ | `seo-pro` | paid | 0.6.2 | More for SEO: Google Search Console in the admin (with your own Google client) - searches, clicks, positions and where to win more; structured data for rich results (FAQ, how-to, product, event, local business); links worth adding between pages, pages competing for one search, readability, near duplicates and image checks; every title and description in one grid, and moving an old site's addresses across as redirects; a not-found log with one-click redirects; a scheduled check of every link; SEO history and notices. Adds to SEO, which is built into Domma CMS (0.90 and later). |
56
+ | `sheets` | paid | 0.4.1 | A spreadsheet in your admin: workbooks with formulas, sorting and filters; import CSV, JSON or Excel, export CSV or Excel, print. Feed a sheet live from Contacts, collections, forms, Surveys, Campaigns, Analytics or the Shop - rows kept even if the source goes away. |
57
+ | `shopping-cart` | free | 1.6.1 | Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order. |
58
+ | `shopping-cart-pro` | paid | 0.2.2 | More for the Shop: discount codes, shipping zones with rates by weight or order value, tax by country and class, branded order emails from confirmation to dispatch, and Insights - takings by week, best sellers, repeat customers and abandoned checkouts you can nudge. Adds to the free Shopping Cart; a lapsed licence leaves the Cart and every order just as they were. |
59
+ | `social-media-pro` | paid | 0.2.5 | How the site looks when it is shared, and posting it: share cards per page for Facebook, X, LinkedIn, WhatsApp and Slack with previews; a share image drawn for every page that has none, in the site's colours; share and follow buttons; posting to Bluesky, Mastodon and LinkedIn (through the site's own LinkedIn app, with a reminder before its 60-day sign-in runs out) - now, scheduled, or drafted automatically for every new blog post or page - with links tagged for Analytics and the posts on the Calendar. Sold with SEO Pro. |
60
+ | `surveys` | paid | 1.5.1 | Targeted surveys over the Forms engine: shared audiences, tokenised invites, reminders, and per-survey statistics. |
61
+ | `theme-roller` | paid | 1.1.3 | Make your own themes: start from any built-in theme, change its colours, fonts, spacing and borders with a live preview, and save it. Saved themes join the built-in ones everywhere a theme is chosen - the site, the admin, day and night, a region of a page. Needs Domma CMS 0.73 or later to use them. |
62
+ | `waypoint-pro` | paid | 1.3.1 | Plan and track work: projects with their own workflows, issues on a board or in a list, a backlog planned into sprints with a burndown, and comments, mentions and watchers that tell the right people. |
63
+
64
+ Versions are read from `dcms-marketplace/plugins/*/plugin.json` (every plugin folder except `_lib`).
65
+
66
+ ## Contents
67
+
68
+ 1. [Content, authoring and the public site](#content-authoring-and-the-public-site)
69
+ 2. [Structured data](#structured-data)
70
+ 3. [Admin Experience & Built-in Tools](#admin-experience--built-in-tools)
71
+ 4. [Users, Roles & Security](#users-roles--security)
72
+ 5. [Operations & Deployment](#operations--deployment)
73
+ 6. [Plugins & the Marketplace](#plugins--the-marketplace)
74
+ 7. [Building Plugins](#building-plugins)
75
+ 8. [Marketplace Plugins](#marketplace-plugins)
76
+ 9. [Feature history](#feature-history)
77
+ 10. [Appendix A - Tutorial ideas](#appendix-a---tutorial-ideas)
78
+ 11. [Appendix B - Doc gaps and code discrepancies](#appendix-b---doc-gaps-and-code-discrepancies)
79
+
80
+ ---
81
+
82
+ ## Content, authoring and the public site
83
+
84
+ Everything a site owner uses to write pages, lay them out, style them and publish them - plus how the public site renders, caches, searches and presents itself to search engines. Verified against the code in 0.93.0.
85
+
86
+ ### Pages
87
+
88
+ - **Markdown pages as files** - every page is a Markdown file with YAML frontmatter in `content/pages/`, and its path is its URL (`/about` is `about.md`, `/services` is `services/index.md`, `/` is `index.md`). No database and no build step: save and the next request renders it.
89
+ - Where: **Content > Pages**; `server/services/content.js`
90
+ - Since: 0.5.0
91
+
92
+ - **Frontmatter fields** - the page editor writes these; any extra key you add by hand is kept on save.
93
+ - `title`, `description`, `slug` (derived), `layout`, `status` (`draft` | `published`, new pages default to `draft`), `sortOrder` (default 99), `category`, `tags` (editorial tags used by search and the Pages list; cleaned of blanks and duplicates on save), `visibility`, `project` (blank means inherit from the URL), `theme` (per-page theme override), `showInNav` (legacy: no longer offered by the editor since 0.93.0 and read by nothing; a stored value is kept), `sidebar`, `breadcrumbs` (`false` hides the trail on this page), `bundled` (shipped with fresh installs), `seo` (`title`, `description`, `image`, `noindex`, `ogType`; merged, never replaced, on save), `dconfig`, `createdAt`, `updatedAt`.
94
+ - Hand-written only: `width` (`narrow` | `normal` | `wide` | `full`) overrides the layout's width for that page.
95
+ - Where: **Content > Pages > (page) > Page Details / SEO / DConfig**; `config/content.json` (`pageDefaults`)
96
+
97
+ ```yaml
98
+ ---
99
+ title: My Applications
100
+ layout: default
101
+ status: published
102
+ visibility: "=candidate"
103
+ tags: [careers, candidates]
104
+ theme: ocean-light
105
+ width: wide
106
+ seo:
107
+ title: Your applications | Acme Jobs
108
+ description: Track every job you have applied for.
109
+ ---
110
+ ```
111
+
112
+ - **Visibility ladder** - who may view a page. `public` (default), `private` (super-admin only), a role name (that role and anyone more privileged - the role hierarchy), `=role` (holders of exactly that role, primary or additional, plus the super-admin - no hierarchy), or an array meaning "any of these". The editor lists live roles twice: "X and above" and "X only". Hand-written values the editor does not recognise are kept as "(custom)".
113
+ - Anonymous or under-privileged visitors get a 403 page; pages are cached per set of roles, so gated content never reaches the anonymous cache.
114
+ - Where: **Page Details > Visibility**; `checkVisibility()` in `server/middleware/auth.js`
115
+ - Since: array form earlier; `=role` exact gate 0.71.0
116
+
117
+ ```yaml
118
+ visibility: [candidate, employer] # both roles and anyone above either
119
+ visibility: ["=candidate", "=employer"] # those two roles and nobody else
120
+ ```
121
+
122
+ - **Drafts** - a page whose status is not `published` returns the same 404 as a missing page to the public, never enters the sitemap, search results or response cache, and is sent with `no-store` and `X-Robots-Tag: noindex, nofollow`.
123
+ - Where: **Page Details > Status**
124
+
125
+ - **Draft preview banner** - a signed-in user with the `pages.read` permission who visits a draft at its real URL sees the real page (layout, navbar, theme, menus, custom CSS) with a striped banner pinned to the bottom. It names the viewer, links to **Edit page**, and has a one-click **Publish** button (with confirm). Dismissable, hidden when printing.
126
+ - Where: public site; `buildDraftBanner()` in `server/services/renderer.js`
127
+ - Since: 0.50.0
128
+
129
+ - **Share (preview) links** - mint a signed, revocable link to one unpublished page for someone without an account (client sign-off, external designer). Choose expiry (1 hour, 24 hours, 7 days, 30 days) and an optional note; list and revoke live links. The recipient sees the page rendered as anonymous with a "Shared preview" banner showing the expiry; links are single-page, `no-referrer`, `noindex`, and stop working if the page's project is disabled.
130
+ - Where: **Page editor > Live Preview > Share link**; public URL `/_preview?token=…`; `server/services/previewLinks.js`, index in `content/preview-links.json`
131
+ - Since: 0.51.0
132
+
133
+ - **Custom 404 page** - create a page at `/404` (`content/pages/404.md`) and publish it; it is rendered with the full site theme for every not-found URL (with a real 404 status). Without it a minimal inline 404 is served. It is excluded from the sitemap.
134
+ - Where: **Content > Pages** (URL path `/404`); `render404()` in `server/routes/public.js`
135
+
136
+ - **Pages list** - one row per page with address, layout, revision count, last change and tags, plus chips for Published/Draft, visibility, project, Bundled and plugin pages. Filter All / Published / Drafts, search, sort, and a **Tree view** (remembered per viewer). Right-click (or the row menu, or Shift+F10) to edit, view on site, copy the address, trim revisions or delete. The sidebar badge counts drafts.
137
+ - Where: **Content > Pages**; also `#/projects/<slug>/pages` scoped to a project
138
+ - Since: Tool redesign 0.75.0
139
+
140
+ - **Project scoping** - a page belongs to a project either explicitly (`project:`) or by URL inheritance; disabling a project takes all its pages off the public site (404) regardless of visibility.
141
+ - Where: **Page Details > Project**
142
+
143
+ - **Rename-safe URLs** - changing a page's URL path moves the file, its version history and cache entries, updates menu links pointing at the old address, and (with SEO on) adds a redirect from the old URL.
144
+ - Where: **Page Details > URL Path**
145
+ - Since: menu link fix 0.89.1
146
+
147
+ - **Breadcrumbs** - an optional fixed-position breadcrumb trail built from the URL segments and page title, with configurable home label, corner and offsets; can be switched off per page. A `BreadcrumbList` JSON-LD block is emitted independently of the visual setting.
148
+ - Where: **System > Site Settings > Breadcrumbs**; frontmatter `breadcrumbs: false`; `config/site.json` `breadcrumbs`
149
+
150
+ ### The page editor
151
+
152
+ - **Split Markdown editor with live preview** - write Markdown on the left, see a server-rendered preview on the right (in an iframe, so the site theme and CSS apply). View modes: Split, Write only, Preview only. The divider is draggable (ratio remembered; double-click or Enter resets; arrow keys, Home/End from the keyboard). Stacks vertically on small screens.
153
+ - Where: **Content > Pages > New / Edit** (`#/pages/edit/<url>`); `admin/js/views/page-editor.js`
154
+
155
+ - **Editor tabs** - **Edit** (Markdown and CSS panes), **Page Details** (URL, title, layout, status, description, sort order, category, visibility, project, theme override, tags pillbox with autocomplete, Show Sidebar, Show Breadcrumbs, Bundled), **SEO** (SEO title and description), **DConfig** (page-level declarative JSON), **Live Preview**, **History**. The site's navigation is edited in Menus; "Show in Navigation" was removed in 0.93.0.
156
+
157
+ - **Live Preview (full page, unsaved)** - renders the whole page shell (navbar, footer, theme, menus, custom CSS) from the editor's current, unsaved state at its real URL, with Full / Tablet (1024px) / Mobile (390px) width buttons and Refresh. Only renders while the tab is visible. Previews are flagged so they never count as analytics visits.
158
+ - Where: **Page editor > Live Preview**
159
+ - Since: 0.51.0
160
+
161
+ - **Site CSS pane** - a `</>` toolbar toggle swaps the Markdown pane for a CSS editor (with line numbers) editing the site-wide `content/custom.css`; unsaved CSS is pushed live into the preview and saved together with the page.
162
+ - Where: **Page editor > Edit > CSS toggle**
163
+ - Since: 0.6.21
164
+
165
+ - **Formatting toolbar and shortcuts** - headings, bold/italic, lists, blockquote, horizontal rule, inline code, code block; Ctrl+B bold, Ctrl+I italic, Ctrl+K link dialog, Tab indent, Ctrl+S save.
166
+
167
+ - **Insert menu and shortcode forms** - an Insert dropdown grouped Layout / Content / Components / Data / Media, generated from a shortcode registry, opens a form for each shortcode with every attribute and a live preview of the tag. Bespoke builders exist for Card, Timeline, Embed, Scribe (script composer), Image (media picker), Component and Colour picker. An **Effects** button opens the effects catalogue builder.
168
+ - Where: `admin/js/lib/shortcode-registry.js`, `shortcode-insert.js`, `card-builder.js`, `timeline-builder.js`, `embed-builder.js`, `effects-builder.js`
169
+
170
+ - **Right-click shortcode menu** - right-click inside any shortcode to toggle or edit its attributes live, open its full form (**Edit <tag>…**), or use **Insert…** and **Wrap in…** / **Wrap selection in…** submenus. Lookup attributes (collection slugs, menus, blocks, actions) are filled from live CMS data. Effects that can be injected into host tags are listed too.
171
+ - Where: `admin/js/lib/shortcode-context-menu.js`
172
+ - Since: 0.36.0
173
+
174
+ - **Shortcode folding** - multi-line `[card]`, `[box]`, `[grid]`, `[row]`, `[tabs]`, `[accordion]`, `[carousel]`, `[hero]`, `[timeline]`, `[slideover]` and `[dconfig]` blocks can be collapsed to one summary line from the gutter; nested blocks fold independently.
175
+ - Where: `admin/js/lib/editor-folding.js`
176
+
177
+ - **Editor settings** - editor font (Fira Code default, JetBrains Mono, Source Code Pro, IBM Plex Mono, Roboto Mono, Ubuntu Mono, Space Mono, Courier Prime, system monospace, sans-serif, Inter, Georgia), size, line spacing and wrapping.
178
+ - Where: page editor banner cog
179
+ - Since: 0.90.3
180
+
181
+ - **Safety nets** - Unsaved marker and Discard; Save checks the URL path (must start with `/`, no spaces) and DConfig JSON and jumps to the tab with the problem; leaving (or closing the browser tab) with unsaved changes asks and keeps a local draft that is offered back next time, warning if the saved page has changed since. A page switcher jumps to another page (disabled while dirty). Banner actions: Back, Editor reference (built-in shortcode help), View on the site, Save, Settings.
182
+ - Since: Tool redesign 0.77.0; reactive bindings 0.89.1
183
+
184
+ - **Built-in reference and tutorial** - an Editor reference slideover documents every shortcode (including the named celebration tags), and a guided CRUD tutorial can be launched from the toolbar.
185
+ - Where: page editor banner help icon; `admin/js/lib/page-editor-reference.js`
186
+
187
+ ### Version history
188
+
189
+ - **Automatic versions** - every save snapshots the previous content before overwriting (`type: auto`), with date, author, type and label.
190
+ - Where: **Page editor > History**; stored in `content/versions/<url>/` with a `_meta.json` index; `server/services/versions.js`
191
+ - **Named versions** - **Save Named Version** creates a labelled restore point; named versions are never removed by pruning.
192
+ - **Compare and restore** - click a version to compare it side by side with the current (unsaved) editor text; restore overwrites the page after taking a "Pre-restore snapshot" automatically. Delete single versions or select several for bulk delete.
193
+ - Since: bulk delete 0.6.21
194
+ - **Trim revisions** - from the Pages list, prune a page's history to the newest N auto versions (default 5), keeping all named ones.
195
+ - Where: **Content > Pages > row menu > Trim revisions**; `POST /api/versions/prune/*`
196
+
197
+ ### Layouts
198
+
199
+ - **Layout presets** - a layout decides a page's width (`narrow`, `normal`, `wide`, `full`), whether it has the navbar, footer and sidebar (and which side the sidebar is on - `sidebarSide: left | right`; a page's own `sidebarSide` wins), and an optional background colour, background image and body class. Ships with nine: Default, Landing Page, Blank (built in), plus With Sidebar, Minimal, Article, Product, Dashboard and Wide. Add your own, filter Built in / Yours.
200
+ - Where: **System > Layouts**; `config/presets.json`
201
+ - Since: 0.5.0
202
+ - **Layout options** - default `[spacer]` height and CSS class.
203
+ - Where: **System > Layouts > Options**; `config/site.json` `layoutOptions`
204
+
205
+ ### Shortcodes
206
+
207
+ Shortcodes are processed on the server before Markdown, so Markdown works inside their bodies. Attributes take single or double quotes; bare flags are allowed. Code inside fenced blocks and inline code is never processed, so you can document shortcodes on a page. Plugins can register their own shortcodes (`hooks.registerShortcode`).
208
+
209
+ Where: `server/services/markdown.js`; reference `docs/markdown-shortcodes.md`; admin **Documentation > Usage > Shortcodes**.
210
+
211
+ #### Layout and containers
212
+
213
+ - **`[card]…[/card]`** - a Domma card. Key attributes: `title`, `subtitle`, `icon`, `icon-layout` (`inline` | `stacked`), `footer`, `variant` (`clean`, `gradient` + `gradient="<name>"`, `glass`, `accent`, `dark`, `glow`, legacy `primary`), `collapsible="true"`, `hover`, `image`, `font` (`serif` | `mono`), `font-size` (`sm` | `lg` | `xl`), `borderless`, `shadow` (`none` | `md` | `lg`), `rounded` (`none` | `xs` | `sm` | `lg` | `full`), `text-align`, `padding` (`compact` | `spacious`), `span` (`2`-`6` | `full`), `class`, `id`. `[header]` and `[footer]` sub-tags take Markdown and win over the matching attributes. Since 0.5.0; sub-tags 0.6.5.
214
+ - **Card layouts (`layout="…"`)** - 33 ready-made card designs: `basic`, `header-body`, `header-body-footer`, `no-header-footer`, `icon-top`, `icon-inline`, `image-top`, `image-overlay`, `thumb-left`, `thumb-right`, `wide-left-image`, `full-bg`, `split-half`, `avatar-profile`, `stat-metric` (`delta`, `progress`), `quote-testimonial`, `callout` (`callout-type`), `video-media`, `location-map` (`address`), `step-numbered`, `corner-badge` / `badge-band` (`badge`), `glass-gradient-border`, `rating-review` (`rating` 0-5), `tag-cloud` (`tags`), `timeline-entry`, `code-snippet` (`lang`), `before-after`, `pricing` (`price`, `period`), `feature-comparison`, `activity-feed`, `progress-goal` (`progress`), `file-document` (`filename`, `filesize`, `filetype`). The Card builder in the editor exposes them.
215
+ - **`[box]…[/box]`** - a card with no header: plain container, no shadow and 2px radius by default; accepts `shadow`, `rounded`, `padding`, `borderless`, `span`, `variant`, `color`, `footer`, `class`, `id` and injected effects. Since 0.91.0.
216
+ - **Span in a grid** - `[card span="2"]` or `[box span="2"]` (2-6, or `full`) takes that many columns of a `[grid]`, whether it sits straight in the grid or inside a `[col]`. Capped at the columns the grid has at each width (responsive grids: 1 on phones, md-K from 768px, lg-K from 1024px; fixed grids their own count), so a wide card never adds a phantom column. In the Card Builder (Presentation > Span in a grid), the Edit Card and Edit Box slideovers and the right-click menu (Span (in a grid)). Where: `cardVariantClasses()` (`card-span-N`) in `server/services/markdown.js`, the span block in `public/css/site.css`, `shortcode-registry.js`, `card-builder.js`. Since: Unreleased.
217
+ - **Hover off** - boxes and cards lift on hover by default (site.css); `hover="off"` (or `false`/`none`/`no`/`0`) adds `card-no-hover`, which every lift rule skips. Works on `[card]` too; a bare `hover` still opts in to `card-hover`. Where: `isHoverOff()` / `cardVariantClasses()` in `server/services/markdown.js`, `:not(.card-no-hover)` on the lift rules in `public/css/site.css`. Since: Unreleased.
218
+ - **Theme-aware colour** - a plain box is drawn from the theme's card tokens (`--dm-card-bg`, `--dm-card-border`, `--dm-text`), so it follows every theme and the light/dark switcher. `color="primary|secondary|success|info|warning|danger"` tints it with `color-mix()` of that `--dm-*` token over `--dm-card-bg` (classes `dm-box-tone dm-box-<name>`); `color="#hex"` keeps the author's colour and the server picks black or white text for it (`dm-box-custom`, inline `--cms-box-bg` / `--cms-box-fg` / `--cms-box-shade`; only `#rgb`/`#rrggbb` accepted). `colour` is an alias; a valid colour replaces `variant`; `variant="primary"` now works on a box. Where: `processBoxBlocks()` / `boxTone()` in `server/services/markdown.js`, "[box color]" in `public/css/site.css`. Since: Unreleased.
219
+ - **Card variants follow the theme** - shared by `[card]` and `[box]`: `variant="dark"` keeps its dark panel but everything on it is set for it (headings, `.card-title`, links via `--cms-link-color`, code, a readable footer, transparent header/footer strips); `glass` is a translucent pane of `--dm-surface` instead of a white film; variant footers use `--dm-text-secondary` (was `--dm-text-muted`, under 4.5:1 on some themes; `glow` used `--dm-primary`). Page links read `--cms-link-color` before `--dm-primary`, so a container can recolour its links. Since: Unreleased.
220
+ - **`[grid cols="N"]` / `[row]` with `[col]`** - CSS-grid layouts. Grid: `cols`, `gap`, `fullwidth="true"` (break out of the content column), `responsive="false"` (disable mobile stacking), `class`, `id`. Row: `gap`, plus scroll-reveal of children (`reveal`, `reveal-mode` stagger | sequence, `reveal-animation`, `reveal-duration`, `reveal-stagger`, `reveal-delay`, `reveal-direction` ltr | rtl). Col: `span`, `id`, injected effects.
221
+ - **`[hero]…[/hero]`** - a hero section: `title`, `tagline`, `size` (`sm` | `lg` | `full`), `variant` (8 generic gradients plus light/dark pastels per theme), `image`, `overlay`, `align`, `bg`, `color`, `min-height`, `fullwidth="true"`, legacy `twinkle` and `blobs`, injected effects.
222
+ - **`[banner]…[/banner]`** - an alert-style message strip: `type` (`info` | `success` | `warning` | `danger` | `neutral`), `title`, `icon`, `dismissible`, `class`, injected effects.
223
+ - **`[slideover title trigger size position]…[/slideover]`** - a button that opens a side panel holding any content.
224
+ - **`[tabs]` / `[tab title icon]`** - tabbed content; `style` (`pills` | `bordered`), `align` (`center` | `right`), `fade`, `id`.
225
+ - **`[accordion multiple="true"]` / `[item title]`** - collapsible sections.
226
+ - **`[carousel]` / `[slide title image]`** - slideshow; `autoplay`, `interval`, `loop`, `animation` (`slide` | `fade` | `crossfade`), `animation-duration`, `animation-easing`.
227
+ - **`[timeline layout theme mode]` / `[event title date status icon]`** - timelines and roadmaps; layouts vertical | centred | horizontal, themes minimal | corporate | modern.
228
+ - **`[listgroup variant size itemvariant action]` / `[item]`** - a Domma list group (`flush` | `numbered` | `horizontal`).
229
+ - **`[table striped bordered compact caption]`** - wraps a Markdown table in styled, responsive markup.
230
+ - **`[center]`**, **`[spacer size /]`** - centring and vertical gaps.
231
+ - **`[contents ... /]`** - a page's own settings for its contents list (side, width, floating, collapse, headings, numbering, look) over its layout's.
232
+ - **`[navbar ... /]`** - a page's own navbar settings over the menu's (any Behaviour setting, docked options included).
233
+ - **`[theme-switcher /]`** - the site's theme dots on a page: inline, or pinned to a corner or edge with offsets; replaces the floating switcher on its page.
234
+
235
+ #### Inline content
236
+
237
+ - **`[button href label variant size icon icon-after target /]`** - a link styled as a button (wrapping or self-closing); accepts injected effects.
238
+ - **`[link href label icon icon-after target /]`** - an anchor with icon and target extras.
239
+ - **`[badge variant pill outline size]`** - inline badge or colour dot.
240
+ - **`[text]`** - inline text styling: `size`, `point-size`, `weight`, `bold`, `italic`, `color`, `font`, `transform`, `decoration`, `spacing`. Since 0.6.20.
241
+ - **`[icon name size color /]`** - a Domma icon.
242
+ - **`[countdown to|duration format /]`** - countdown timer.
243
+
244
+ #### Media and embeds
245
+
246
+ - **`[embed url ratio title width align autoplay loop muted /]`** - YouTube/Vimeo (privacy-friendly `youtube-nocookie`), self-hosted `.mp4`/`.webm`/`.ogg` as a native player, or any other URL as an iframe only if its host is on the allow-list. Since 0.36.0.
247
+ - Where: `config/embeds.json` `allowedHosts`
248
+ - **`[demo src height title base /]`** - embeds a same-origin demo page (default under `/public/demos`) in a captioned frame. Built in since 0.67.0 (was the `demo-viewer` plugin).
249
+
250
+ #### Data, forms and reuse
251
+
252
+ - **`[block template="name" field="…" /]`** and the short form **`[name field="…" /]`** - render a reusable block with inline data (see Blocks). Since 0.6.20.
253
+ - **`[component name="counter" initial="5" /]`** or `<dm-counter initial="5"></dm-counter>` - mount a custom web component (see Components); only the scripts for components used on the page are loaded.
254
+ - **`[menu slug|location depth variant orientation float class /]`** - render any menu inline (see Menus).
255
+ - **`[form name="contact" /]`** - embed a CMS form.
256
+ - **`[collection slug display … /]`** and **`[view slug … /]`** - render collection entries or a saved view (table, cards, list, accordion, timeline, carousel, listgroup, block) with filtering, sorting, per-user scope and a right-click filter menu. Covered in the Data section.
257
+ - **`[cta action entry style icon confirm]`** - a button that runs a CMS Action.
258
+ - **`[dconfig]{ JSON }[/dconfig]`** - declarative page behaviour (click handlers, class toggles) via Domma's `$.setup()`; merged with the page's DConfig tab, inline blocks win on conflict.
259
+
260
+ #### Effects
261
+
262
+ - **Scroll and motion** - `[reveal animation duration delay threshold once]`, `[breathe amplitude duration]`, `[pulse scale duration]`, `[shake intensity duration direction]`, `[ripple colour duration opacity]`, `[animate type duration delay repeat]` (CSS only), `[ambient type speed intensity]` (CSS animated background).
263
+ - **Text** - `[scribe speed cursor loop]` typewriter, with a script mode of `[render effect]`, `[wait]ms[/wait]`, `[undo all|count /]` and `[redraw]` (clear then type); `[scramble speed loop]`; `[counter to from duration prefix suffix decimals separator /]`.
264
+ - **Particles and overlays** - `[twinkle count shape colour min-size max-size]`, `[ticker-tape palette density speed sway burst /]`, `[butterflies palette density wander rise-speed flap-speed two-tone /]`, `[strobe preset motion speed … /]` (presets club, concert, police, searchlight, scanner, mood). Self-closing covers the whole page; the wrapping form is scoped to its content. Since 0.35.0.
265
+ - **Fireworks** - `[firework type colour size continuous hover /]` and a `[fireworks]` container (CSS).
266
+ - **Celebrations** - 20 built-in scenes, available through the page editor, right-click menu and named shortcodes. Automatic seasons include Christmas, New Year, Chinese New Year, Easter, Summer Garden, Autumn, Winter and the existing holidays. Graduation, Birthday, Anniversary, standalone sparkler greetings, actual weather rain and Money Rain run on demand. Custom `message` text supports names and `{year}`, stroke or cursive lettering, and gold or silver sparks. The User Manual and shortcode reference cover all options.
267
+ - **Effect injection** - add an effect as a flag to a host tag instead of wrapping it, with prefixed parameters: cards and boxes (reveal, breathe, pulse, shake, twinkle, ambient, animate, ripple), hero (reveal, breathe, pulse, twinkle, ticker-tape, butterflies, strobe, ambient, animate), banner (reveal, pulse, shake, ambient, animate), col (reveal, breathe, animate, ripple), button (breathe, pulse, shake, animate, ripple).
268
+
269
+ ```markdown
270
+ [card title="Why us" layout="icon-top" icon="star" reveal reveal-animation="zoom"]
271
+ Fast, file-based and friendly.
272
+ [/card]
273
+
274
+ [scribe loop="true"]
275
+ [render]We build things that matter.[/render]
276
+ [wait]1200[/wait]
277
+ [redraw]We design for humans.[/redraw]
278
+ [/scribe]
279
+ ```
280
+
281
+ - **Effects settings** - site-wide reveal defaults (animation, duration, threshold) and "Respect reduced motion"; plus an in-admin catalogue and guide of every effect grouped Entrance / Animation / Text / Visual / Celebrations, with examples.
282
+ - Where: **System > Effects**; `config/effects.json`
283
+ - Since: Tool redesign 0.76.0
284
+
285
+ ### Blocks
286
+
287
+ - **Reusable HTML blocks** - an HTML fragment with `{{placeholder}}` fields, used on pages via `[block]` or its own short tag, and as the per-entry template for collection displays (`display="block"`, carousel and listgroup `block=`). Values are HTML-escaped; `{{{field}}}` passes raw; `{{field:DD MMM YYYY}}` formats dates; `{{_id}}`, `{{_createdAt}}`, `{{_updatedAt}}` are available for entries. Template HTML comments are stripped before rendering. 15 bundled blocks (contact-card, contact-info, cta-banner, feedback-card, plugin-card, quickstart-step and more).
288
+ - Where: **Data > Blocks**; `content/blocks/<name>.html`
289
+ - **Companion CSS** - each block may have `<name>.css`, written as plain rules and automatically scoped to `[data-block="<name>"]` using CSS nesting, emitted once alongside the block.
290
+ - Where: block editor **Custom CSS** pane; `content/blocks/<name>.css`
291
+ - **Block editor** - HTML and CSS editors with syntax highlighting, Edit / Split / Preview modes, a sample-values table that drives a live preview, a placeholder and date-format reference, Copy shortcode, project assignment, Bundled flag, and unsaved-change drafts. New blocks cannot silently overwrite an existing name.
292
+ - Where: **Data > Blocks > (block)**; `admin/js/views/block-editor.js`
293
+ - Since: Tool redesign 0.77.0
294
+ - **Block preview in the site's own page** - the preview is an iframe holding the site's real page (blank layout, from `POST /api/pages/preview/full`), so the theme, fonts, `site.css` and Custom CSS apply; each keystroke redraws only the block. Width (full / 1024 / 390), One or Three in a grid (the `[block]` and `[collection display="block" cols="3"]` wrappers), and a site theme to try (pinned like a page's `theme`); remembered per browser. `{{{raw}}}` placeholders render raw and template comments are stripped, as on the site; image fields sample a grey picture, links `#`, class/style hooks nothing. The block's markup is sanitised with DOMPurify before drawing (parity with the site's sanitize-html: no on* handlers, `javascript:` URLs, scripts or frames) and handed to the frame by `postMessage` (`previewBridge()`), which also blocks navigation, hides the site's fixed-position chrome and forwards Ctrl+S. The frame keeps `allow-same-origin` because every asset is served with `Cross-Origin-Resource-Policy: same-origin`. Raw sample rows read `{{{field}}}`; untouched samples follow a switch between raw and escaped; placeholders in template comments are ignored.
295
+ - Where: `admin/js/views/block-editor.js`, `renderPreview()` / `sampleValue()` in `admin/js/lib/block-editor-arrange.js`
296
+ - Since: Unreleased
297
+
298
+ ```markdown
299
+ [block template="contact-card" full_name="Jane Doe" email_address="jane@example.com" /]
300
+ [contact-card full_name="Jane Doe" email_address="jane@example.com" /]
301
+ ```
302
+
303
+ ### Components
304
+
305
+ - **Custom web components (`.dmc`)** - single-file components with `<template>` (mustache-style, `{{#if}}`), `<props>` (typed JSON with defaults and labels), `<script>` (`data`, `methods`, `onMount`, `this.set`) and optional `<style>`, compiled to shadow-DOM custom elements `<dm-name>`. Ships with copy-button, countdown-timer, counter, reveal-panel and testimonial-card.
306
+ - Where: **Data > Components**; `content/components/<name>.dmc`; `server/services/components.js`
307
+ - **Component editor** - four source tabs, a live preview recompiled as you type, preview-prop rows (reset, copy as attribute or whole tag, jump to `<props>`), unsaved drafts; plugin components open read-only with "Copy to a new component". The list supports duplicate, export and import of component bundles.
308
+ - Where: **Data > Components > (component)**
309
+ - Since: Tool redesign 0.77.0
310
+
311
+ ### Media library
312
+
313
+ - **Media library** - upload by button or by dropping files anywhere on the screen (images, documents, audio, video; 10 MB default limit). Tiles with thumbnail, size, age and type; filter All / Images / Video / Audio / Documents / Other; sort newest, oldest, name or largest. Details slideover with rename, Copy address and Copy for a page (Markdown); right-click menu, F2 to rename, Delete key. Replacing a same-named file asks first.
314
+ - Where: **Content > Media**; `content/media/`; `config/server.json` `uploads.maxFileSize`
315
+ - Since: 0.5.0
316
+ - **Image editor** - edit JPEG, PNG, WebP, GIF or TIFF in place: crop (aspect lock), resize, rotate, flip, colour presets (grayscale, sepia, warm, cool, enhance, sharpen, soften, invert), adjustments, watermark (position, scale, opacity), border (solid, mirror, repeat) and output format (JPEG, PNG, WebP with quality). Save as a copy or overwrite.
317
+ - Where: **Content > Media > Edit image…**; `server/services/images.js` (Sharp), `admin/js/lib/image-editor.js` (Cropper.js)
318
+ - **Insert Image** - the page editor's Insert > Image opens a picker of uploaded images and inserts Markdown.
319
+
320
+ ### Theming
321
+
322
+ - **37 built-in themes** - 28 public themes (14 palettes x light and dark: Charcoal, Ocean, Forest, Sunset, Royal, Lemon, Silver, Grayve, Mint, Unicorn, Dreamy, Christmas, Wedding, Corporate) and 9 admin-chrome themes (Sharp, Slate, Smooth x Indigo, Steel, Teal). Site and admin themes are chosen separately.
323
+ - Where: **Content > Theme > Themes**; `config/theme.json` (authoritative; `site.json` keeps a one-way mirror)
324
+ - Since: `theme.json` 0.57.0; Corporate and Admin Slate 0.85.0
325
+ - **Automatic day/night** - switch between a day and a night theme at set clock times (default 07:00 and 19:00, visitor's local time). A page with its own theme opts out.
326
+ - **Typography** - 18 font families (Roboto default, Inter, Open Sans, Lato, Nunito, Poppins, Source Sans 3, Montserrat, Raleway, DM Sans, Work Sans, Merriweather, Playfair Display, Lora, Roboto Slab, Lexend, Atkinson Hyperlegible, Andika) or System UI, and base size 13-20px.
327
+ - **Per-page theme** - override the site theme for one page.
328
+ - Where: **Page Details > Theme Override**; frontmatter `theme`
329
+ - **Element overrides** - re-theme one region (e.g. a light navbar on a dark site) by giving a CSS selector a whole theme to adopt, individual `--dm-*` token values, and/or raw CSS. Compiled to one rule injected before custom CSS; unsafe selectors and values are refused; a Generated CSS panel shows the output.
330
+ - Where: **Content > Theme > Element overrides**; `theme.json` `overrides`
331
+ - **Public theme switcher** - an optional floating disc of theme dots for visitors; position (six anchors), offsets, and an optional filter of which themes to offer.
332
+ - Where: **Content > Theme > Theme switcher**; `theme.json` `switcher`
333
+ - Since: built in 0.67.0 (was a plugin)
334
+ - **Custom themes hook** - plugins (the Theme Roller) register themes via `hooks.registerThemes()`: a base theme plus changed tokens, usable everywhere a theme is chosen; the base is remembered so the site keeps working if the plugin goes away.
335
+ - Since: 0.73.0
336
+ - **Site custom CSS** - one stylesheet, `content/custom.css`, inlined last on every public page so it always wins.
337
+ - Where: **System > Site Settings > Custom CSS**, or the page editor's CSS pane
338
+
339
+ ### Site menus (authoring view)
340
+
341
+ Two kinds of menu, named apart: **site menus** are built in the admin (Content → Site menus) and
342
+ placed in slots; **page contents** is the list a layout with a sidebar builds from a page's own
343
+ headings (Layouts → Page contents settings, and `[contents]` per page).
344
+
345
+
346
+ - **Menus as data** - any number of menus in `config/menus/<slug>.json`, mapped to slots (`navbar`, `footer-primary`, `footer-legal`, plugin slots) in `config/menu-locations.json`. Items can be links, separators, spacers or the theme switcher, with icons, badges and per-item `visibility` (same ladder and `=role` as pages) or `hidden`.
347
+ - Where: **Content > Menus**, **Menu locations**
348
+ - **Drag to move and nest** - in the Menus editor, drag an item by its grip: a line shows where it will land (above or below a row), and an outline shows it will go inside a row (the middle half of the row). The item follows the pointer and the rows glide to their new places when you let go. Esc cancels. Works with a mouse, pen or finger. Built on Domma's `E.sortable` (domma-js 0.45.0); on an older Domma core the right-click menu, Alt+arrows and buttons still move items.
349
+ - **Bindings** - a menu can take over a slot only on chosen pages or projects.
350
+ - **Overlay slot** - menus bound to the core `overlay` slot render as floating panels anywhere on the page; floating panels can be collapsible, movable, resizable, with radius, shadow, accent and opacity. Since 0.42.0.
351
+ - **Presentation** - variant, position (sticky, fixed, static, floating), orientation (vertical navbar docks as a side rail), alignment (left, centre, right, spread), and highlight effects for hover (colour, underline, overline, background, uppercase, grow, breathe, glow) and current page (bold, underline, overline, background, darken, pill).
352
+ - **`[menu]` shortcode** - render a menu by `slug` or `location` inline, with `depth`, `orientation` and `float="no"` per placement; per-user filtering is server-side.
353
+
354
+ ### Context menus (right-click)
355
+
356
+ - **Authored context menus** - attach a right-click menu to any region of the public site by CSS selector (plus optional match and exclude selectors), scoped site-wide or bound to pages and projects, with priority and a nesting rule when regions overlap (append, prepend, or only mine).
357
+ - Where: **Content > Context Menus**; `config/context-menus/<slug>.json`; `server/services/contextMenus.js`
358
+ - Since: 0.56.0
359
+ - **Item types** - item, heading, divider, checkbox, radio.
360
+ - **Arranging items** - in the editor's Items tab, drag a row (anywhere but its buttons) and the others slide aside (Domma `E.sortable`); the ↑/↓ buttons, the row menu and Alt+Up/Down still work.
361
+ - **Action types** - `navigate` (URL), `filter` / `search` / `sort` / `export` (collection displays), `copy-link`, `print`, `open-modal` (render a block in a modal), `run-action` (a CMS Action), `mark-as-spam`, plus browser-style specials `reload`, `back`, `forward`, `copy`, `cut`, `paste`, `select-all` and `site-search`. Arbitrary JavaScript is deliberately not allowed.
362
+ - **Appearance** - accent colour, corner radius, transition (scale, fade, slide, none), density, scale.
363
+ - **Designer classes** - your own CSS classes on the menu panel (`class`, Appearance > CSS classes) and on any item (`item.class`, in the item's panel), for styling from Custom CSS. Item classes need Domma 0.51.7+.
364
+
365
+ ### Public site search
366
+
367
+ - **Built-in site search** - a full-screen overlay with live results as you type; opened from a navbar icon or a floating button, Ctrl+K / Cmd+K, or a context-menu action. Arrow keys move, Enter opens, Esc closes.
368
+ - Where: **System > Search**; `config/search.json`; `server/services/search.js`; `GET /api/search?q=`
369
+ - Since: built in 0.64.2 (was the `site-search` plugin)
370
+ - **Settings** - enable/disable, placeholder, keyboard shortcut, max results, minimum query length, debounce, display mode (navbar or floating), floating button icon-only, size (32-96px), corner radius, position and offsets; a **Try it** tab runs real queries.
371
+ - **Ranking** - title matches score highest, then tags, description and body text (Markdown and shortcode syntax stripped). Drafts and `private` pages are excluded. No index to build; cost is linear in page count.
372
+ - **Results follow the searcher's access** - pages gated to a role are listed only for visitors who could open them (signed-in visitors are recognised by their session, as on pages), and `noindex` pages (in the page's own SEO or set in SEO) and pages in a disabled project are left out. **System > Search**'s "Left out" count says which. Since: 0.92.1 (before it, role-gated pages appeared to anonymous visitors with title, description and snippet).
373
+
374
+ ### SEO (core)
375
+
376
+ - **Per-page SEO** - SEO title and description in the editor; share image, noindex, Open Graph type and focus keyphrase via the SEO tool. Every page gets canonical and `og:url` (from the site URL), Open Graph and Twitter tags, `WebPage` and `BreadcrumbList` JSON-LD; share images are made absolute.
377
+ - Where: **Page editor > SEO**; **System > Site Settings > SEO** (default title, separator, description, image); **Site Settings > General** (site URL, 0.88.0)
378
+ - **sitemap.xml** - built from published, public, indexable pages (not `/404`, not `noindex`), plus plugin sources such as blog posts; cached and refreshed on any page change.
379
+ - Where: `/sitemap.xml`; `server/services/sitemap.js`
380
+ - Since: plugin sitemap sources 0.89.0
381
+ - **robots.txt** - allows the site, disallows `/admin/` and `/api/`, points at the sitemap; extra lines and a "block all" switch in SEO settings.
382
+ - Where: `/robots.txt`
383
+ - **SEO tool** - an audit of every sitemap URL, fetched in-process and scored (title and description length, single H1, heading order, word count, image alt text, internal links, focus keyphrase placement and density, noindex, canonical, share image, structured data, language), with site-wide duplicate title/description detection and a scheduled re-audit. Open a page to see its search result and share card as they will look and edit its fields. **Redirects** tab (301, 302, 307, 308, 410; CSV import; hit counts; automatic redirect on page rename). **Sitemap** tab shows what is listed and left out, plus robots.txt. Organisation / LocalBusiness / Person structured data for the home page and BlogPosting for blog posts. Can be switched off as a Tool; SEO Pro plugs into its registry.
384
+ - Where: **Tools > SEO** (`#/seo`, in the Tools folder by default); `config/seo.json`; `content/seo/redirects.json`, `overrides.json`; `server/services/seo/`
385
+ - Who: the `seo` permission (View / Manage). The level-0 role holds it, and the seeded `admin` role is granted it once at boot with the other built-in Tools (removed by hand, it stays removed); other roles need it ticked under System > Roles.
386
+ - Since: plugin 0.89.0; built in 0.90.0
387
+
388
+ ### Rendering and performance
389
+
390
+ - **Server-side rendering** - pages are rendered to full HTML on the server (layout, navbar, footers, theme classes, fonts, overrides, custom CSS, breadcrumbs, DConfig), so content is visible without JavaScript and crawlable.
391
+ - Where: `server/services/renderer.js`, `server/templates/page.html`
392
+ - **Response cache** - fully rendered pages are cached per URL, file modification time, set of viewer roles and origin, and invalidated by tags when pages, collections, forms, blocks, views, menus, context menus or settings change; concurrent cold requests render once. Memory driver by default, Redis optional (Pro), toggle and flush from the admin. Drafts and previews are never cached; responses are `private, no-cache` so shared proxies never mix roles.
393
+ - Where: **System > Site Settings > Cache** (key viewer, on/off, clear); dashboard widget; `config/cache.json`; `/api/cache/*`; `docs/cache.md`
394
+ - Since: file-change self-invalidation 0.36.12
395
+
396
+ ---
397
+
398
+ ## Structured data
399
+
400
+ Domma CMS's data layer covers collections (schema-defined records), the filter engine, public display shortcodes, saved views, actions, forms, the scaffolder, projects and the external API platform. Everything below was checked against the code in v0.93.0. Where the docs say something different, the code wins and the difference is listed in Appendix B.
401
+
402
+ ### Collections - the core model
403
+
404
+ - **Collections**. A collection is a named set of entries that share the same fields, for example products, bookings or team members. Each one has a `schema.json` with its fields, API access, export rules and storage. Entries are stored as `{id, data, meta}`, where `meta` holds `createdAt`, `updatedAt`, `createdBy`, `source` (`admin`, `api`, `import`, `form:<slug>`, `action`, `migration`) and, for a form that ended early, `outcome`.
405
+ Where: **Data > Collections**; `content/collections/<slug>/schema.json` + `data.json`; `server/services/collections.js`. Since: 0.5.0.
406
+ - **Collections list screen**. Lists every collection with its entry count and chips for MongoDB, Built in, Bundled and Plugin. It has filters and search. **New collection** opens a slideover that keeps a draft if you leave. Right-click a row to edit fields, open its form, copy the slug or delete it. The sidebar badge shows collection and entry totals, the largest collections and how many are on MongoDB.
407
+ Where: `#/collections`, `GET /api/collections/_count`. Since: 0.75.0.
408
+ - **Built-in collections are protected**. Preset collections (see below), `roles` and `user-profiles` cannot be deleted (403). Built-in roles `super-admin`, `admin` and `user` cannot be deleted or renamed, the level-0 root role can never be deleted, and `roles` cannot be cleared in bulk.
409
+ Where: `server/routes/api/collections.js`. Since: 0.76.0 (role guards).
410
+ - **Collection editor**. This is a full-page editor with four tabs, and one Save covers all of them (banner button, unsaved bar or Ctrl+S). If you leave with unsaved changes it asks first and keeps a per-collection draft that it offers back on your next visit.
411
+ - **Settings** tab: title, slug (taken from the title, fixed once the collection is created), project, description, entry-form layout (stacked, or a grid of 1-6 columns) and a **Bundled** flag (ships with fresh installs through the seed script).
412
+ - **Fields** tab: fields are rows. Click one to edit it in a resizable slideover. Drag (the other rows slide aside - Domma `E.sortable`), right-click, ⋮, Shift+F10 or Alt+Up/Down to reorder, duplicate or remove. The editor checks for duplicate names and invalid names (`^[A-Za-z_][A-Za-z0-9_-]*$`), and option-type fields need at least one option.
413
+ - **API & Export** tab: per-verb public API switches and the export gate (see "External API platform" and "Collection displays").
414
+ - **Storage** tab (only when a MongoDB connection exists): choose File or MongoDB plus a named connection.
415
+ Where: **Data > Collections > edit** (`#/collections/edit/<slug>`); `admin/js/lib/collection-editor-arrange.js`. Since: 0.77.0 (Tool rewrite).
416
+ - **Auto-generated form**. Creating a collection also creates a matching form with the same slug, so it can take submissions straight away. `roles` and `user-profiles` are skipped. An existing form is never overwritten.
417
+ Where: `ensureFormForCollection()` in `server/services/forms.js`.
418
+
419
+ ### Field types
420
+
421
+ **Offered in the collection editor's Type picker (13):**
422
+
423
+ | Value | Label |
424
+ |---|---|
425
+ | `string` | Text (single line) |
426
+ | `email` | Email |
427
+ | `tel` | Phone |
428
+ | `number` | Number |
429
+ | `textarea` | Textarea (multi-line) |
430
+ | `select` | Dropdown (select) - needs options |
431
+ | `radio` | Radio buttons - needs options |
432
+ | `checkbox` | Single checkbox (boolean) |
433
+ | `checkbox-group` | Checkbox group - needs options |
434
+ | `date` | Date |
435
+ | `time` | Time |
436
+ | `url` | URL |
437
+ | `hidden` | Hidden field |
438
+
439
+ Options are typed one per line as `value: Label`.
440
+
441
+ The field panel also edits:
442
+ - label and name (the name is taken from the label, so "First name" becomes `first_name`)
443
+ - required
444
+ - placeholder and helper text
445
+ - a clear button (text, email, phone, URL, textarea), - / + buttons (number) and a character counter (text, textarea) on the entry form input (`clear`, `stepper`, `counter`)
446
+ - an icon or short text joined to the start or end of the input (`prefix` / `suffix`: `{icon}` or `{text}`, up to 24 characters; text, number, dropdown, date, time, URL and textarea fields), drawn in the entry form by Domma's input groups
447
+ - grid span (1-6) or full width
448
+
449
+ Any other properties stored on a field, such as `validation`, `logic`, `reference` and `file`, are kept unchanged when you save.
450
+
451
+ **Types the server and displays understand, but that can only be set in `schema.json`, recipes or plugins:**
452
+ - `reference`. A relational field (see below). The server validates it and the display, form and API code resolve it.
453
+ - `file`. An uploaded-file reference `{url, name, size, mime}` with a `file: {accept, maxSize}` config. Displays show an image thumbnail for image MIME types and a download link for anything else.
454
+ - `boolean`, `datetime`, `multiselect` / `multi-select`, `tags`, `chooser`, `json`, `object`, `array`, `markdown`, `text`. These are used by presets, plugins and recipes. The Collection Browser builds a filter control from `text`/`textarea`/`email`, `number`, `date`/`datetime`, `select`/`chooser`, `multiselect`/`tags`/`tag` and `boolean`/`checkbox`.
455
+
456
+ Examples of where they appear: `projects` uses `boolean`, `notifications` uses `datetime` and `array`, `api-endpoints` uses `object`, the calendar plugin uses `markdown`, the onboarding recipe uses `multiselect` and `file`.
457
+
458
+ **Form-only field types** (Form Builder): `chooser`, `icon`, `image`, `password` and `file`, plus the layout pseudo-fields `page-break` and `spacer`. See Forms.
459
+
460
+ - **Number fields are stored as numbers**. On every write path (admin, `/api/v1`, form submissions, import, Actions), a `number` field sent as plain decimal text such as `"42"` is converted to `42`, so it sorts, filters and totals correctly. Text that is not a number (`"abc"`, `"0x10"`) is stored exactly as sent and never rejected.
461
+ A one-off boot migration fixed numbers stored as text before 0.77.1, including in MongoDB collections. It records itself in `content/.migrations.json`.
462
+ Form number fields also accept `min`/`max`, and text fields `minLength`/`maxLength`. The `[form]` embed and the server both enforce them.
463
+ Where: `server/services/entryValues.js`, `server/services/number-fields-migration.js`. Since: 0.77.1 (form fix), 0.77.2 (migration), 0.78.0 (server coercion), 0.81.1 (limits enforced server-side).
464
+
465
+ ### Reference fields and integrity
466
+
467
+ - **Reference fields**. A field stores the id of an entry in another collection. Setting `multiple: true` makes it one-to-many and stores an array of ids.
468
+ - Wherever the field is shown, the stored id is replaced by the target entry's `displayField`.
469
+ - `linkTemplate` optionally turns the label into a link.
470
+ - A deleted target shows as `<id> (missing)` instead of breaking the page.
471
+ ```json
472
+ {"name": "jobId", "label": "Job", "type": "reference", "required": true,
473
+ "reference": {"collection": "jobs", "displayField": "title", "searchField": "title", "linkTemplate": "/jobs/{{id}}"}}
474
+ ```
475
+ Where: `server/services/references.js`; `docs/configuration.md` ("Collection schema fields"). Since: 0.20.0 (see [Feature history](#feature-history)).
476
+ - **Integrity on write**. Every create and update, from the admin, the API, forms or Actions, checks that each referenced id exists in the target collection. If one does not, the write fails with `Validation failed: "Job" → no entry "abc" in "jobs"`.
477
+ Where: `validateReferences()` in `collections.js`.
478
+ - **Batch resolution**. `listEntries(slug, {resolveRefs: true})` adds `entry._refs.<field> = {id, display, missing, link}` to each entry, fetching each target collection once. `[collection]` and the Collection Browser always resolve references. The public API does so when you pass `?resolveRefs=true`.
479
+ - **References in forms**. When a `[form]` is rendered, a reference field becomes a dropdown filled from the target collection. Only the first 200 target entries are listed.
480
+ Where: `expandReferenceFields()` in `markdown.js`.
481
+
482
+ ### File fields
483
+
484
+ - **File uploads**. A form `file` field switches the submission to `multipart/form-data`.
485
+ - The server checks each file against the field's `file.accept` (MIME list, wildcards such as `image/*` allowed) and `file.maxSize` (default 5 MB).
486
+ - The filename is cleaned and given a uuid prefix, and the file is saved to `content/media/`.
487
+ - The entry stores `{url, name, size, mime}`.
488
+ - Files are publicly reachable under `/media/`.
489
+ Where: `parseMultipartForm()` in `server/routes/api/forms.js`; `docs/configuration.md`. Since: 0.20.0.
490
+
491
+ ### Entries screen
492
+
493
+ - **Entries admin**. A keyboard-navigable table showing the first 5 fields as columns.
494
+ - Click a row to edit it. Right-click, ⋮ or Shift+F10 offers: edit, see all fields, duplicate, copy id, run an action, filter by the clicked value, delete. Space ticks a row; Delete deletes it.
495
+ - You can tick several rows and delete them together.
496
+ - Search, filter chips (`=`, `≠`, `contains`, `>`, `<`, with number and date aware comparison) and column sort.
497
+ - Entries load from the server 500 at a time with "Load more", and 100 rows are drawn at a time with "Show more". Filters and search only cover the rows loaded so far, and the count line says so.
498
+ - The entry form opens in a resizable slideover that uses the collection's stacked or grid layout. It keeps a draft (never secret fields) if you leave.
499
+ - Banner actions: Add, Import, Export, Edit fields, Open its form (when a form exists), Clear all.
500
+ - The ⚡ run-an-action buttons only appear when Actions are available (MongoDB). Each button is filtered per row through `/api/actions/:slug/check-access`.
501
+ Where: **Data > Collections > (collection)** `#/collections/<slug>/entries`. Since: 0.77.0.
502
+ - **Mark as spam**. Flags an entry (`spam: true` by default, or a custom field name) without touching its other fields.
503
+ Where: `POST /api/collections/:slug/entries/:id/spam`. Since: 0.61.0.
504
+
505
+ ### Import and export
506
+
507
+ - **Export** is available as JSON (full `{id, data, meta}` entries) or CSV (`id`, every schema field, `createdAt`). Admin exports and public right-click exports share one serialiser, so the same values produce byte-identical files. The CSV includes a UTF-8 BOM and is protected against spreadsheet formula injection.
508
+ Where: entries screen banner **Export**; `GET /api/collections/:slug/export?format=json|csv`; `public/js/collection-export.mjs`. Since: 0.43.0 (CSV hardening).
509
+ - **Import** accepts JSON only. Paste a JSON array or choose a `.json` file: `[{"data": {...}}]` (the format Export produces; bare objects also work).
510
+ - Every item becomes a new entry with a new id and `source: 'import'`.
511
+ - Rows that fail required-field validation, or that reference an entry that does not exist, are skipped and counted (the reference check since 0.93.0).
512
+ - Fields the collection lacks are stored as given, not dropped.
513
+ - Number fields are converted.
514
+ Where: entries screen banner **Import**; `POST /api/collections/:slug/import` with `{entries: [...]}`. Since: 0.77.0 (file picker).
515
+ - **Clear all** deletes every entry in a collection (`DELETE /api/collections/:slug/entries`).
516
+
517
+ ### Row-level access
518
+
519
+ - **Row-level rules** limit Actions and Views to the entries a user may touch. Admins (level 0, counting additional roles) always bypass them. A misconfigured rule fails closed.
520
+ - `owner`: the entry's `meta.createdBy` equals the user.
521
+ - `field`: `data.<field>` equals the user.
522
+ - `reference`: follow a reference field to its target and check that the user owns the target, via `meta.createdBy` or `data.<targetField>`. With an array of ids, owning any one target is enough. This is the "recruiter sees applications to their own jobs" pattern.
523
+ - All modes accept `userKey` (`id` by default, or `email`, `name` or `role`) to choose which user property is compared.
524
+ ```json
525
+ {"mode": "reference", "field": "jobId", "targetCollection": "jobs", "targetField": "createdBy"}
526
+ ```
527
+ The Action and View editors only offer `owner` and `field`. `reference` has to be written in the stored JSON.
528
+ Where: `access.rowLevel` on actions and views; `server/services/rowAccess.js`. Since: 0.5.0 (`owner`, `field`), 0.20.0 (`reference`), 0.92.1 (`reference` applied to views).
529
+ - **Per-user displays** (`scope="mine"`). Covered under "Collection displays" below.
530
+
531
+ ### Preset (built-in) collections
532
+
533
+ These schemas are defined in code and rewritten on every boot. Their data files are never touched. They cannot be deleted, and the listing flags them `builtIn: true`. The system presets (roles, user profiles, projects, notifications, API tokens and endpoints) always use file storage and cannot be moved.
534
+
535
+ - **User-data presets on MongoDB** - `notes-notes`, `todo-items`, `contacts-contacts` and `contacts-groups` (the Tools that hold people's own data) use MongoDB when their schema declares it (Notes and Todo do; Contacts declare files) and the site has a working MongoDB connection. A site is never switched to an empty store: while `data.json` holds rows the collection stays on files, and where both stores hold rows it stays on files and the server log names the collection with both counts. A Storage move (which keeps ids) settles it, and the choice now survives a restart. No connection configured means files, silently; a configured connection that is down means files plus the storage-down notice.
536
+ Where: `USER_DATA_PRESET_SLUGS` in `presetCollections.js`, `resolveUserDataPreset()` in `adapterRegistry.js`. Since: 0.93.0 (before it, Notes and Todo declared MongoDB but were always kept in files).
537
+
538
+ | Slug | Purpose |
539
+ |---|---|
540
+ | `notifications` | Admin bell notifications (system-managed) |
541
+ | `projects` | Project records |
542
+ | `api-tokens` | External API tokens (hash stored, never returned by the entry endpoints) |
543
+ | `api-endpoints` | API Builder endpoint definitions |
544
+ | `notes-notes` | Notes tool (moved out of a plugin in 0.67.0) |
545
+ | `todo-items` | To-do tool (moved out of a plugin in 0.67.0) |
546
+ | `contacts-contacts` | Contacts tool |
547
+ | `contacts-groups` | Contact groups |
548
+
549
+ `roles` and `user-profiles` are seeded separately (by `roles.js` and `userProfiles.js`) but get the same protection.
550
+
551
+ Where: `server/services/presetCollections.js` (`PRESETS`, `seedAll()`), `server/services/adapterRegistry.js`.
552
+
553
+ ### Filter engine
554
+
555
+ - **Structured filters**. One syntax works across `[collection]` (`where_*` attributes), the admin and public list APIs (`filter[...]`), `/api/v1`, and API Builder endpoints. It behaves the same on file storage (filtered in memory) and MongoDB (translated to a native query). Conditions are combined with AND.
556
+
557
+ | Suffix | Meaning | Example |
558
+ |---|---|---|
559
+ | none / `_eq` | equals (on an array value, means "contains") | `where_location="London"` |
560
+ | `_ne` | not equal | `where_status_ne="closed"` |
561
+ | `_gt` `_gte` `_lt` `_lte` | numeric or ordered comparison | `where_salary_gte="50000"` |
562
+ | `_in` / `_nin` | in / not in a comma list | `where_type_in="full-time,contract"` |
563
+ | `_contains` | substring, case-insensitive | `where_title_contains="engineer"` |
564
+ | `_starts` / `_ends` | starts or ends with, case-insensitive | `where_email_ends="@example.com"` |
565
+ | `_exists` | present and non-empty (`true`/`false`/`1`/`0`) | `where_remote_exists="true"` |
566
+
567
+ - **Pseudo-fields**: `createdBy` matches `meta.createdBy` and `id` matches the entry id. Dot paths reach into nested data (`where_address.city_eq="London"`). A legacy `where="a=1,b=2"` attribute (equality only) is still supported. An unknown operator matches nothing rather than erroring.
568
+ - **Other list parameters**: `page`, `limit` (`0` means no limit), `sort`, `order`, `search` (substring match across every field).
569
+ Where: `server/services/filterEngine.js`; e.g. `GET /api/collections/jobs/public?filter[location]=London&filter[salary_gte]=50000`.
570
+
571
+ ### Storage adapters (File and MongoDB)
572
+
573
+ - **File adapter (default)**. Entries live in `content/collections/<slug>/data.json`. No setup is needed.
574
+ - **MongoDB adapter (Pro)**. Stores a collection in MongoDB as `cms_<slug>`, with a unique index on `id`. You can choose per collection, so file and MongoDB collections can sit side by side. The `mongodb` package is only loaded when a collection needs it.
575
+ - If MongoDB fails to load or is unreachable, the collection falls back to the file adapter and a critical "storage down" notification is raised (since 0.80.0).
576
+ - Deleting a MongoDB collection drops its backing collection (since 0.34.1).
577
+ Where: `server/services/adapters/{FileAdapter,MongoAdapter}.js`, `adapterRegistry.js`, `connectionManager.js`; `docs/guides/mongodb-storage-adapter.md`. Since: 0.5.0.
578
+ - **Named connections**. `config/connections.json` holds one or more named connections (`{type: "mongodb", uri, database, options}`), with `default` used when a collection names none. The file is gitignored.
579
+ You can edit connections in the admin: **Data > Collections > Database connections** tab (admins only), with row or raw-JSON editing. Passwords never reach the browser: `GET` sends each URI's password, secret query parameters and secret-looking `options` keys as a bullet mask, and `PUT` puts the stored value back for a mask left untouched - only from a stored connection with the same scheme, user, hosts, query and driver options (a rename or a new database is fine), so a masked URI pointed at another host or proxy can never carry the stored password there; a URI with a raw `?` in its password is masked whole; anything else still masked is refused with a 400 (`server/services/connectionSecrets.js`, since 0.101.8). The endpoints are `GET/PUT /api/collections/connections`, and `GET /api/collections/pro-status` reports whether Pro is active.
580
+ The CLI commands are `npm run pro enable | status | disable` (`scripts/pro.js`).
581
+ - **Switch storage from the editor**. Changing the adapter on a collection's Storage tab asks whether to move its entries when you save.
582
+ - Moving copies every entry as it is - same id, data and `meta` (`createdAt`, `updatedAt`, `createdBy`, `source`) - in both directions, so references, links and row ownership keep working. Nothing is re-validated, so no entry is dropped.
583
+ - It refuses and changes nothing when the target already holds an entry with one of the ids, or the source holds an id twice (409 with the ids), and answers 503 when the MongoDB connection is not available.
584
+ - The old copy is set aside, never deleted: `data.json` becomes `data.json.bak` (timestamped if one exists), and a MongoDB collection is renamed `cms_<slug>__moved_<time>`.
585
+ - System collections (roles, user profiles, projects, ...) cannot be moved.
586
+ Where: `POST /api/collections/:slug/migrate-storage`, `migrateStorage()` in `server/services/collections.js`, `archive()` on each adapter. Since: 0.6.20 (first migration route), 0.77.0 (editor flow), 0.93.0 (ids and meta kept, collision refusals, archive instead of delete).
587
+
588
+ ### Collection displays: `[collection]` and `[view]`
589
+
590
+ - **Eight display types**: `table` (the default, a Domma Table with search, sort, paging and column chooser), `cards`, `list`, `accordion`, `timeline`, `carousel`, `listgroup`, `block` (each entry rendered through a reusable block template).
591
+ Since: 0.14.3 (`timeline`), 0.38.0 (`carousel`, `listgroup`); see [Feature history](#feature-history).
592
+ Where: server-side rendering in `server/services/markdown.js`; browser behaviour in `public/js/site.js`, `collection-context.js` and `collection-browser.js`.
593
+ - **Common attributes**:
594
+ - `slug` (required)
595
+ - `display`
596
+ - `fields` (comma list of fields to show)
597
+ - `limit`
598
+ - `sort` / `order`
599
+ - `title-field`
600
+ - `columns` (cards)
601
+ - `cols` (block, 2-6)
602
+ - `empty` (text when there are no entries)
603
+ - `class` / `id`
604
+ - `where` / `where_*` filters
605
+ - `scope="mine"`
606
+ - **Table-only attributes**: `search` and `sortable` (both default true), `exportable`, `page-size` (default 25).
607
+ - **Per-display attributes**:
608
+ - accordion: `title-field`, `body-field` (default `description`), `multiple="true"`
609
+ - timeline: `date-field`, `status-field`, `icon-field`, `body-field` (Markdown), `layout` (vertical/centred/horizontal), `theme` (minimal/corporate/modern), `mode` (timeline/roadmap)
610
+ - carousel: `image-field`, `body-field`, `autoplay`, `interval`, `loop`, `animation*`
611
+ - listgroup: `variant` (flush/numbered/horizontal), `size`, `itemvariant`, `action="true"`
612
+ - `block="<name>"` works for `block`, `carousel` and `listgroup`
613
+ - **Per-entry action buttons**:
614
+ - on `[collection]`: `cta="<action>"`, `cta-label`, `cta-icon`, `cta-style`, `cta-confirm`
615
+ - on `[view]`: the same options, but the action slug goes in `action=` rather than `cta=`
616
+
617
+ ```
618
+ [collection slug="team" display="cards" columns="3" title-field="name" /]
619
+ [collection slug="jobs" display="list" where_location="London" where_salary_gte="50000" sort="salary" order="desc" /]
620
+ [collection slug="events" display="timeline" title-field="title" date-field="date" body-field="summary" layout="centred" /]
621
+ [collection slug="team" display="carousel" block="profile-card" autoplay="true" /]
622
+ ```
623
+ - **Per-user displays** (`scope="mine"`). Shows only the signed-in user's own entries. The page itself stays in the per-role cache: a placeholder is filled in by `POST /api/collections/render-scope`, where the server adds `createdBy = <user id>` from the verified login token. This works even when the collection's public read is switched off. Anonymous visitors see a sign-in prompt.
624
+ - **Interactive "my entries" with transitions** - adding `searchable`, `sortable`, `filterable` or `paginate` renders the block as the Collection Browser over the viewer's own rows, so `transitions` gives each row its state-machine buttons (the recruitment recipe's candidate dashboard). Paging, the transition list and running a transition or `cta` from the block all send `scope: "mine"`; a row the viewer did not create is refused with 403 before the state check, so an illegal move on their own row is still 409. It used to fall back to a static list and drop the buttons.
625
+ ```
626
+ [collection slug="applications" display="cards" scope="mine" searchable transitions title-field="job" /]
627
+ ```
628
+ Where: `POST /api/collections/render-scope`, `POST /api/actions/:slug/public` and `GET /api/actions/transitions?scope=mine`. Since: 0.20.0 (`scope="mine"`), 0.93.0 (interactive + transitions).
629
+ - **Interactive Collection Browser**. Adding any of `searchable`, `sortable` (bare, or a list of fields), `filterable="a,b"` or `paginate` turns a static display into a browser:
630
+ - a search box and sort dropdown
631
+ - a filter rail whose controls come from the schema field types, with distinct values found automatically when a field has no options
632
+ - facet counts (client mode only)
633
+ - a mobile filter drawer
634
+ - saved searches kept in the visitor's browser (`no-saved-searches` turns them off)
635
+ - URL query sync (`cb_<id>_…`; `no-url-sync` turns it off)
636
+ - relevance sort when searching
637
+ - `/` to focus search, and arrow keys to page
638
+ - an optional `exportable` CSV button
639
+ - `pagination="scroll"` for infinite scroll
640
+ - `mode="client|server"`: client mode switches to server mode automatically above `max-client-entries` (default 1000)
641
+ - `page-size` (default 12)
642
+
643
+ `where_*` filters set by the author stay fixed; visitors can narrow within them but not widen past them. `transitions` adds per-row state-machine buttons (see Actions), honouring the action's roles and row-level rule.
644
+
645
+ Since: 0.20.0. In releases before 0.93.0 the browser never started (`collection-browser.js` built its browsers before their class was defined, so every interactive block stayed on its no-JS fallback), and its `cta` and transition buttons were not wired; both were fixed in 0.93.0.
646
+ ```
647
+ [collection slug="jobs" display="cards" columns="3" searchable filterable="location,type,salary,tags" sortable page-size="9" where_status="open" /]
648
+ ```
649
+ - **Public right-click menu**. Works on every display type, on the public site as well as the admin. Right-clicking an entry offers:
650
+ - quick filters on the clicked value (`=`, `≠`, and `>`/`<` for numbers and dates)
651
+ - free-text search that understands `field: value`, `field = x`, `field != x`, `field > x`, `field < x`
652
+ - removable filter chips
653
+ - Sort…
654
+ - Group by… (only on cards, list, listgroup, accordion and block; hidden on table, timeline and carousel)
655
+ - Copy… (the visible entries as CSV, JSON or Markdown)
656
+ - Export… (a download)
657
+ - Print (prints only that display)
658
+ - Copy link to this view
659
+ - Clear all
660
+
661
+ Only the fields the display shows are sent to the page. The menu is registered as exclusive, so another context menu on the page cannot take the display over.
662
+ Where: `public/js/collection-context.js`, `collection-query.mjs`, `collection-sort.mjs`. Since: 0.43.0 (drill-in menu, sort, group, export, print, share link); 0.56.0 (exclusive registration).
663
+ - **`dmf=` share links**. Filters, sort and grouping are saved in the URL hash as `#dmf=<base64url JSON>`, one entry per display. Reloading or sharing the link restores the view, and any existing anchor in the hash is kept.
664
+ - **Export permission**. Set in the collection's `export` block (editor **API & Export** tab):
665
+ - `enabled`
666
+ - `access`: `public` or a role name; the default is the least-privileged role
667
+ - `formats`: `csv`, `json`
668
+
669
+ `exportable="true"` on the shortcode opens export to the public unless `enabled: false`. This is a convenience gate, not a security boundary, because the displayed data is already in the page source.
670
+ ```json
671
+ "export": {"enabled": true, "access": "user", "formats": ["csv", "json"]}
672
+ ```
673
+ Since: 0.43.0.
674
+
675
+ ### Saved Views
676
+
677
+ - **Saved Views**. A reusable query over a source collection, built as a pipeline of stages and shown anywhere with `[view slug="…"]`. It works on both storage types: MongoDB sources run a native aggregation, and file sources run the built-in JS evaluator, which gives the same results. Configs are stored in `content/views/<slug>.json`. Configs from before 0.34 that were stored in MongoDB `cms__views` are migrated to files automatically, once, without changing the MongoDB copy.
678
+ Where: **Data > Views**; `server/services/views.js`, `viewPipeline.js`. Since: 0.34.0 (file-backed, no Pro needed).
679
+ - **Pipeline stages** (allow-list):
680
+ - `$match` (Filter rows): the view editor offers equals, not equals, >, <, ≥, ≤, contains and in, joined with AND or OR. The file evaluator also supports `$and`, `$or`, `$regex`, `$not`, `$elemMatch`, `$exists`.
681
+ - `$sort` (Order rows)
682
+ - `$lookup` (Join a collection; equality form only)
683
+ - `$project` (Reshape fields)
684
+ - `$unwind` (Flatten an array)
685
+ - `$addFields` (Compute fields; literals and `$field` references)
686
+ - `$group` (Group and total, with `$sum`, `$avg`, `$min`, `$max`, `$first`, `$last`, `$push`, `$addToSet`)
687
+ - `$limit`, `$skip`, `$count`
688
+
689
+ `$out`, `$merge`, `$function`, `$accumulator` and `$graphLookup` are refused. Field paths are raw-document paths (`data.price`, `meta.createdAt`).
690
+ - **View editor** has tabs for Source, Pipeline, Display and Access.
691
+ - Pipeline: stages are cards with a summary line. Right-click a stage to move, duplicate or delete it, or to run the pipeline up to that stage.
692
+ - Display: mode table, list or block, plus columns and page size.
693
+ - Access: roles taken from the site's roles, a Public switch, and "Show each user only their own results" (row-level `owner` or `field`).
694
+ - A live **Results** tab runs the unsaved pipeline (`POST /api/views/_preview`, up to 50 rows). You cannot preview a `$lookup` into a collection outside your projects.
695
+ - It has the same Save, draft and leave-guard behaviour as the other editors.
696
+ Where: `#/views/edit/<slug>`. Since: 0.77.0.
697
+ - **Views list and preview**.
698
+ - The list flags public, restricted and missing-source views. The sidebar badge turns red when a view reads a deleted collection.
699
+ - Right-click a view to preview it, copy its shortcode, duplicate it or delete it.
700
+ - The preview screen runs the saved view a page at a time.
701
+ Where: `#/views`, `#/views/<slug>/preview`, `GET /api/views/_count`. Since: 0.75.0.
702
+ - **Using a view**.
703
+ - `[view]` accepts every `[collection]` display type and attribute listed above.
704
+ - If you set no `display`, it uses the view's stored display mode.
705
+ - Adding `searchable`, `sortable` or `paginate` gives it the interactive browser with search, sort, paging, saved searches and export, but no filter rail (the view defines its own filtering).
706
+ - Export permission follows the source collection's `export` block, and falls back to closed if the source cannot be found.
707
+ - Public API: `GET /api/views/:slug/public`, which checks the view's `access.public` and `access.roles`.
708
+ - **One access decision everywhere** (`canReadView`, 0.92.1): the static `[view]`, the interactive one and the public API all apply it - public views for everyone, otherwise the view's roles on the usual ladder, and row-level rules applied as the viewer (`reference` mode included; totals and page counts count only the rows that viewer may see). A page with a per-user view is rendered fresh for signed-in visitors rather than served from the role-shared cache; an anonymous visitor to a row-level view gets no rows. A new view with no roles chosen is for the admin tier by level (levels 0 and 1), not by role name. Before 0.92.1 `[view]` showed a view's rows to anyone who could open the page.
709
+ ```
710
+ [view slug="employer-applications" display="cards" title-field="candidate" searchable sortable page-size="20" /]
711
+ ```
712
+
713
+ ### Actions (Pro - needs MongoDB)
714
+
715
+ - **Actions**. Admin-designed workflows that run a sequence of steps against one collection entry, for example "Approve application" or "Archive". Action configs are stored in MongoDB (`cms__actions`, on the `default` connection). Without MongoDB, the Actions list explains the Pro requirement instead of showing an error, and `/api/actions/_count` returns `available: false`.
716
+ Where: **Data > Actions**; `server/services/actions.js`. Since: 0.75.0 (list), 0.77.0 (editor).
717
+ - **Step types**:
718
+ - `updateField`: set one field
719
+ - `deleteEntry`
720
+ - `moveToCollection`: copy the entry to a target collection, then delete the original
721
+ - `createInCollection`: add a related entry elsewhere (apply, bookmark or upvote patterns), with interpolated `data` and optional `createdBy`
722
+ - `webhook`: GET, POST or PUT with an interpolated JSON body and headers
723
+ - `email`: through the site's SMTP settings, using `to`, `subject`, `template`, `from`
724
+ - `notify`: raise an admin bell or desktop notification with `title`, `body`, `severity` and `link`; since 0.81.0
725
+ - **Template variables** in step configs: `{{entry.id}}`, `{{entry.data.<field>}}`, `{{now}}`, `{{user.id|name|email|role}}`, `{{env.CMS_PUBLIC_*}}` (only environment variables with that prefix). Execution is not transactional: a failure returns partial results.
726
+ - **Trigger**. The only trigger type is `manual`: a button on each entry with a label, an icon and an optional confirmation message. In practice actions run from:
727
+ - the entries screen
728
+ - `[collection cta="…"]` / `[view action="…"]` buttons
729
+ - `[cta]`
730
+ - Collection Browser transition buttons
731
+ - a form's "Run CMS action" setting or trigger
732
+ - **Access**. Allowed roles, plus optional row-level rules (`owner`/`field` in the editor, `reference` in JSON). Public execution (`POST /api/actions/:slug/public`) always requires a signed-in user.
733
+ - **Any listed role is enough**: each role the action lists lets its holders in, and everyone more senior, across all the roles a user holds - the same rule as view visibility (`canReadView`). Before 0.93.0 the check took the most senior listed role only, so an action for `admin` and `hr` ignored `hr`.
734
+ - A role name the site does not have counts for nothing; an action naming no role that exists is for the level-0 role only (a fix in 0.93.0 - such an action could be run by any signed-in user). An action naming no roles at all is for admins (levels 0 and 1).
735
+ - The same decision (`canRunAction()` in `server/services/actions.js`) governs the public execute route and which transition buttons a row offers. Since: 0.93.0.
736
+ - **State-machine transitions**. `transition: {field, from: [...], to}` means the action only applies while the entry's `field` holds one of the `from` values. `GET /api/actions/transitions?collection=&entryId=` returns the transitions allowed for that entry and user (roles and row-level rule), and the Collection Browser's `transitions` flag draws them as per-row buttons. Running an action whose transition no longer applies answers **409** on both `POST /api/actions/:slug/execute` and `/public` (400 before 0.93.0). Since: 0.20.0.
737
+ - **Step picker**. The action editor offers every step type, `createInCollection` included (with a check that its data is a JSON object), and it is described in Documentation > Actions. Since: 0.93.0.
738
+ - **`[cta]` shortcode**. A button that runs an action against an entry.
739
+ ```
740
+ [cta action="send-welcome" entry="abc123" style="primary" icon="mail" confirm="Send it?"]Send welcome email[/cta]
741
+ [cta action="archive" entry="abc123" label="Archive" size="sm" /]
742
+ ```
743
+ Where: `processCtaBlocks()` in `markdown.js`, wired up in `public/js/site.js`.
744
+
745
+ ### Forms
746
+
747
+ - **Form Builder**. Definition-driven forms stored in `content/forms/<slug>.json`.
748
+ - Input helpers (Domma 0.47 input extras), ticked per field: a show / hide button on password fields (`formConfig.reveal`), a clear button on text, email, phone, URL and textarea fields (`clear`), - / + buttons on number fields that honour min, max and step (`stepper`), and a character counter on text and textarea fields, against Max Length when set (`counter`).
749
+ - Fields can have an icon or a short text joined to the start or end of their input (Before / After in the field card, with the icon picker; stored as `formConfig.prefix` / `suffix`, `{icon}` or `{text}`): single-line inputs, dropdowns and textareas. Drawn by Domma's input groups (domma-js 0.46.0).
750
+ - The editor is a list of field cards with drag-to-reorder, duplicate and remove. Cards drag by the six-dot grip in their header (Domma `E.sortable`): the others slide aside, touch works, Esc cancels, and Alt+Up/Down moves a focused card.
751
+ - An always-on live preview renders exactly as the public page does, and can submit a test entry once the form is saved.
752
+ - It has the same Save, draft and leave-guard behaviour as the other editors.
753
+ Where: **Data > Forms** (`#/forms/edit/<slug>`); `admin/js/views/form-editor.js`. Since: 0.5.0 (as a plugin); 0.77.0 (core Tool).
754
+ - **Field types (18)**:
755
+ - Text (`string`), Email, Phone (`tel`), Number, Textarea
756
+ - Dropdown (`select`), Radio, Single checkbox, Checkbox group
757
+ - Chooser (visual option cards with per-option metadata)
758
+ - Icon (icon picker) and Image (media library), since 0.37.0
759
+ - Date, Time, URL, Password
760
+ - File upload
761
+ - Hidden
762
+
763
+ Layout pseudo-fields: **Page Break** (wizard step) and **Spacer**. `reference` fields (in JSON) render as dropdowns.
764
+
765
+ Each field also has:
766
+ - label, name and required
767
+ - placeholder, helper text and tooltip (a help icon; since 0.81.1)
768
+ - options
769
+ - column span or full width
770
+ - min/max (numbers) and min/max length (text)
771
+ - **Conditional logic** (per field ⚡ panel, stored in `field.logic`):
772
+ - `visibility`: show or hide on conditions, with an optional transition and duration
773
+ - `requirement`: conditionally required
774
+ - `validation`: `regex`, or `match` another field
775
+ - `cascade`: this field's options depend on another field's answer
776
+
777
+ Conditions use the operators `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `is_empty`, `is_not_empty`, `in`, `not_in`, `matches_regex`, grouped as all or any. For multi-value fields, a positive operator passes when any value matches.
778
+ Where: `public/js/form-logic-engine.js`, shared by the browser and the server.
779
+ - **Multi-step wizards**. Adding a Page Break turns a form into a wizard with Previous and Next buttons. Each break has a step title and description.
780
+ - Each page validates before moving on.
781
+ - Logic can test answers given on earlier pages.
782
+ - Files chosen on earlier pages stay attached.
783
+ - The honeypot covers the whole submission.
784
+ - Empty leading or trailing pages are dropped.
785
+
786
+ Forms default to a 2-column grid (3 columns is the other option; Stacked and Inline are also available).
787
+ - **Submit button position** (Settings tab, `settings.submitAlign`): Default (`''`, straight after the last field - in a grid, the next free column), `left`, `center` (Centre), `right`, or `full` (stretched across the form). Any chosen position puts the button on a row of its own, in every layout.
788
+ - Applied by one helper on every render path - the `[form]` shortcode, `data-form` embeds (including the no-Domma fallback) and the editor's live preview - as `fb-submit fb-submit--<position>` classes on the `.form-buttons` wrapper (plus `col-span-full` in a grid). Domma's own `buttonAlignment` only writes a text-align class, which the CMS's flex button row ignores.
789
+ - It replaces the grid-only Submit Button Span: a form saved with `submitSpan: 'full'` reads as Left, which is what it drew, and saves as `submitAlign` next time.
790
+ - A multi-step form's footer (Previous, Next / Finish) is navigation and stays as it is. An end-form trigger's Finish stays where the button was put.
791
+ - The preview's Default now matches the public form (it used to show the button at the right while visitors saw it at the left).
792
+ Where: `public/js/form-submit.mjs`, `public/css/forms.css`. Since: 0.99.0.
793
+ - **Submit button look and second button** (Settings tab, Submit Button card). All optional; a form without these keys renders exactly as before, and the editor only writes a key that differs from its default.
794
+ - `settings.submitButton = {style, size, iconStart, iconEnd}` - `style` is a Domma button class suffix: `primary` (default), `secondary`, `success`, `danger`, `warning`, `info`, `outline` (the primary-coloured outline - Domma has no `btn-outline-primary`), `outline-success`, `outline-danger`, `outline-warning`, `outline-info`, `ghost`; `size` `''` / `sm` / `lg`; icons are Domma icon names, drawn as `<span data-icon>` either side of `.fb-btn-label` and scanned.
795
+ - `settings.secondaryButton = {kind: 'reset'|'link', label, url, style, iconStart, iconEnd, first, stack}` - Clear form (a `type="reset"` button, default label "Clear form") or a Cancel link-button (default "Cancel"), sharing the submit's size, in the same `.form-buttons` row so `submitAlign` places the pair. `first: true` puts it before Submit; `stack: true` stacks the two (`fb-submit--stack`, lined up by the position; Full width stretches both).
796
+ - Clear form asks first (`E.confirm`) when anything differs from the form's start, then fires `input` + `change` on every field: Domma posts what its model holds, and the logic runtime, triggers and picker previews only hear those events - a bare reset left them all on the old answers. `.fb-field-invalid` and Domma's field errors are cleared.
797
+ - Cancel URLs: a path, `#anchor`, `?query`, or `http(s):` / `mailto:` / `tel:`; no whitespace, control characters or backslashes. `POST`/`PUT /api/forms` and `createForm()` answer 400 otherwise (`buttonSettingsProblems`); `GET /api/forms/:slug/public` and the `[form]` shortcode drop an unsafe one from the page (`publicButtonSettings`), and the browser refuses it again at render.
798
+ - Invalid styles, sizes and icon names fall back to the defaults; labels are escaped.
799
+ - Multi-step forms: Next and Finish take the style and size, Finish the icons (not Next - a "send" icon would promise too much), Previous the size, re-applied on every step. No second button and no position change.
800
+ - End-form triggers relabel only `.fb-btn-label`, so Finish keeps its icons and style. The dressing runs before the logic runtime attaches on every path (`applySubmitButtons` / `applyWizardButtons`).
801
+ - The editor preview's Cancel link does not navigate; it says where it would go.
802
+ Where: `public/js/form-submit.mjs`, `public/css/forms.css`, `public/js/form-logic-engine.js` (`setButtonText`).
803
+ - **Designer classes** (2026-10): your own CSS class names, for styling in Custom CSS. `settings.class` on the `<form>` (Settings > CSS classes), `settings.submitClass` on the submit button and a wizard's Finish (Submit Button > CSS classes), and `field.class` on the field's wrapper `[data-field="name"]` (each field's CSS classes box). Forma has no option for them, so `applyFormClasses()` stamps them after every render on all three paths and per wizard step; the manual (no-Domma) path gives its field groups the same `data-field` hook. Rules shared with menus in `public/js/css-classes.mjs`; `formClassProblems()` refuses a bad name on create/update (400) and in the builder before saving.
804
+ Where: `public/js/form-steps.mjs`; `docs/guides/multi-page-forms.md`. Since: 0.45.0-0.46.0.
805
+ - **Form triggers** (per field 🎯 panel, stored in `field.triggers[]`). Each trigger is "when (all or any conditions on any field) then … otherwise …".
806
+ - **State actions** apply while the condition holds and reverse when it stops:
807
+ - `banner`: above the field, below it, or at the top of the form; with a level; dismissible or not
808
+ - `hide-fields`
809
+ - `disable-fields`
810
+ - `require-fields`
811
+ - `block-submit`: with a reason
812
+ - `end-form`: masks the rest of the form, shows a message and turns the button into Finish. It has `record` (save the answers so far, or keep nothing), `finish` (let the visitor finish, or stop dead), `finishLabel` and `doneMessage`. The stored entry gets `meta.outcome`.
813
+ - **Event actions** fire once when a branch is entered (never on first render; "Only once" option):
814
+ - `toast`
815
+ - `goto-step`: jump to a wizard step
816
+ - `redirect`: after submit; overrides the success redirect, and the first one wins
817
+ - `celebrate`: respects reduced motion
818
+ - `run-action`: a CMS Action, run on the server after the entry is stored
819
+ - `notify`: tell the admins, through the "Form triggers" notification source, capped at 30 an hour
820
+ - Hide, disable and require actions target "the rest of the form", "every other field" or chosen fields, never the trigger's own field.
821
+ - **Answers in messages**: banner, toast, block reason and the end-form message and closing message fill `{{field_name}}` tokens from the answers ("Hello {{name}}"); choices show labels, several joined with commas, a tickbox is Yes; passwords, files, images and fields a trigger hid, disabled or masked fill as nothing; plain text, one line, 200 characters max. Filled in the engine after every trigger resolves, so the server's 400 and closing message match. An Insert field picker sits beside those boxes and saving warns about tokens that will be blank. Redirect URLs and notify titles are not filled yet. Since 0.99.0.
822
+ - The server re-checks block, hide, disable, require, end, redirect, run-action and notify on submit. Re-enabling the button in dev tools gets a 400.
823
+
824
+ Where: `docs/guides/form-triggers.md`. Since: 0.57.0; `end-form` and `notify` since 0.81.0.
825
+ - **Anti-spam**:
826
+ - a honeypot field (renamed to `_website` if the form already has a `website` field)
827
+ - a 2-second minimum time to fill in the form. Both of these silently "succeed" for bots.
828
+ - a per-IP rate limit per form (default 3 a minute, settable 1-60)
829
+ - admins can flag or unflag spam from the submissions screen
830
+
831
+ Settings: `settings.honeypot`, `settings.rateLimitPerMinute`.
832
+ - **What happens on submit**:
833
+ - **Store**: to the form's own collection, or to another collection chosen in `actions.collection`. Several forms can share one collection, and each submission is stamped `source: form:<slug>`.
834
+ - **Email notification**: to recipients with a subject prefix, using the site's SMTP settings. **Forms > Test email** sends a test message.
835
+ - **Webhook**: POST (or another method) of `{form, data}`.
836
+ - **CMS Action**: `settings.actionSlug`, which receives the signed-in submitter as `{{user.*}}`.
837
+ - **Success message or redirect**: `successRedirect` supports `{{entryId}}`.
838
+
839
+ If an email, webhook or action fails, the submission is still kept and a "Form actions failing" notification is raised (at most once a day per form). An optional "Form submitted" notification is off by default. The embed never exposes recipients or webhook URLs (since 0.57.0).
840
+ - **Self-healing backing collection**. If a form's target collection is missing on submit, it is created from the form's fields (with admin-only API access) and the submission is saved, not lost. Saving a form also warns about mismatches with its collection, such as required collection fields the form does not collect, or form fields the collection lacks.
841
+ Since: 0.23.0.
842
+ - **Submissions view**.
843
+ - One row per submission, named after the sender.
844
+ - Filters: Inbox, New this week, Spam, plus search across every answer and a date range.
845
+ - Click a row to see everything that was sent.
846
+ - Right-click to copy it, copy or email the sender, mark it as spam or delete it.
847
+ - Banner: Export (CSV or JSON), Clear all.
848
+ - The Forms list badge shows submissions in the last 7 days.
849
+ Where: `#/forms/<slug>/submissions`; `GET /api/forms/:slug/submissions[/export[/json]]`. Since: 0.77.0.
850
+ - **`[form]` shortcode**.
851
+ ```
852
+ [form name="contact" /]
853
+ [form slug="newsletter" class="mt-4" id="newsletter-form" /]
854
+ ```
855
+ Public endpoints: `GET /api/forms/:slug/public`, `POST /api/forms/submit/:slug` (JSON or multipart).
856
+
857
+ ### Scaffolder and recipes
858
+
859
+ - **Scaffolder**. Applies a JSON "recipe" to build a working feature in one step. Every option (such as `namespace`, `collectionSlug`, `formSlug`, `actionPrefix`) can be overridden, and `{{tokens}}` are substituted throughout the recipe. What it creates, in order:
860
+ 1. an optional project
861
+ 2. roles
862
+ 3. a collection
863
+ 4. a form
864
+ 5. seeded users
865
+ 6. menus (with slot mapping)
866
+ 7. actions
867
+ 8. API tokens
868
+ 9. API endpoints
869
+
870
+ It returns a Markdown `snippet` to insert into the page. Everything is tagged with the recipe's project. `wireForm: true` points the form at the first action created.
871
+ - A pre-flight check refuses with 409 and a conflict list if a collection, form, action or menu slug already exists. The project is created after it, so a refused apply writes nothing.
872
+ - Option `type`: `slug` (default, slugified), `email` (validated), `password` (kept as typed, at least 8 characters, never echoed), `text`. Only slugs are slugified.
873
+ - Project, tokens and endpoints are idempotent on re-apply.
874
+ - Actions are skipped with a warning when there is no MongoDB.
875
+ Where: page editor toolbar **CRUD tutorial** > "Apply template"; **Documentation > Tutorials > Building a CRUD App**; `GET /api/scaffold/recipes`, `GET /api/scaffold/recipes/:slug`, `POST /api/scaffold/apply` (needs `collections.create`, plus the permission for everything the recipe creates - projects, `users.create` for roles and users, menus, actions, API tokens and endpoints - checked before anything is written; a recipe role must sit below the caller and grant only what they hold); `server/services/scaffolder.js`; `docs/scaffolding.md`. Since: 0.24.0 (`apiTokens`), 0.25.0 (`apiEndpoints`).
876
+ - **Bundled recipes (2)**:
877
+ - **Contact list (CRM-lite)** (`contact-list`) builds:
878
+ - a `contacts` collection: name, email, phone, company, tags, notes, status
879
+ - a `contact-quick-add` form
880
+ - a `contact-followup` transition (active to followup)
881
+ - a snippet with an interactive table using `transitions`
882
+ - **Employee Onboarding** (`onboarding`, options `namespace`, `hrEmail`, `hrPassword`) builds:
883
+ - a collection with a right-to-work `file` upload
884
+ - a form
885
+ - an `hr` role and a seeded HR user
886
+ - review, approve (with email) and reject transitions (Pending, then Reviewing, then Approved or Rejected)
887
+ - a cards browser snippet
888
+ - **Recruitment walkthrough**. `docs/recruitment-recipe.md` is a hand-built job-board tutorial (jobs and applications, reference fields, `scope="mine"`, transitions, reference row-level access), not a recipe you can apply.
889
+
890
+ ### Projects (data-relevant parts)
891
+
892
+ - **Projects**. Group artefacts (pages, collections, forms, actions, menus, blocks, components, views, roles, users, API endpoints) under a slug using `meta.project`. Anything untagged belongs to the protected **core** project.
893
+ - A user's `projects: []` list limits which artefacts they can see.
894
+ - A disabled project switches off its whole external API (404).
895
+ - API tokens and API Builder endpoints are bound to a project.
896
+ - Collection, form and view lists can be filtered per project (`#/projects/<slug>/collections`, and so on).
897
+ Where: **Projects > Manage Projects** (`#/projects`), **Overview** (`#/projects/<slug>`: tiles and counts per artefact type with quick-create), **Settings** (`#/projects/<slug>/settings`: name, icon, root URL, sort order, enabled, and a danger zone with untag-all and delete); `.claude/skills/domma-projects/SKILL.md`. Since: 0.22.x-0.23.0; 0.77.0 (Overview and Settings as Tools).
898
+
899
+ ### External API platform
900
+
901
+ - **Per-collection public API**. Each verb (create, read, update, delete) can be switched on with an access mode:
902
+ - `public`
903
+ - a role name (a signed-in user at that level or above)
904
+ - `token` (a project API token)
905
+
906
+ A default collection allows public read and nothing else. `api.read.fields` limits which fields reads return. Admin screens are never governed by these settings.
907
+ - **A role the site does not have admits only the level-0 role** (0.92.1), on `/api/v1`, the public collection route and API Builder endpoints alike; the same for a collection's export access. Before 0.92.1 an unknown role came out as the lowest level, which every signed-in user met. The editor lists the site's real roles ("User and above") and marks a stale one "manager (role not found)" with a note instead of quietly swapping it.
908
+ Where: collection editor **API & Export** tab; `schema.api`.
909
+ - **`/api/v1` external REST**. A stable, versioned alias of the public collection endpoints.
910
+ - `GET/POST /api/v1/:slug`, `GET/PUT/DELETE /api/v1/:slug/:id`
911
+ - `page`, `limit`, `sort`, `order`, `search`, `filter[…]` and `resolveRefs` work as described above
912
+ - `scope=mine` returns the caller's own entries (needs a login token)
913
+ - Entries created with a token are stamped `createdBy: token:<id>` and `source: api`
914
+ - Number fields come back as numbers (since 0.78.0)
915
+ ```bash
916
+ curl -H 'Authorization: Bearer dcms_…' 'https://example.com/api/v1/jobs?filter[location]=London&limit=10'
917
+ ```
918
+ Since: 0.24.0.
919
+ - **API tokens**. Machine credentials in the form `dcms_` + 64 hex characters.
920
+ - Only a SHA-256 hash is stored; the plaintext is shown once.
921
+ - Each token is bound to one project (cannot be changed later), with optional `scopes: [{collection, verbs}]`, an expiry date, an enabled switch and a last-used time.
922
+ - Strict matching: a token only works where the access mode is `token`, and a login token never works in token mode.
923
+ - The management screen can edit, disable, copy the token hint and revoke. Its badge counts tokens that need attention, and tokens about to expire raise notifications (since 0.80.0).
924
+ Where: **System > API Tokens** (`#/api-tokens`); `/api/api-tokens`; `server/services/apiTokens.js`; permission family `api-tokens.*`. Since: 0.24.0; 0.76.0 (Tool).
925
+ - **Content API**. Read-only, headless access to what the site publishes, at `/api/content/v1`: the page list, one page (`?format=html|markdown|both`), the menu at a location, the site settings public pages already show, and search.
926
+ - Off until switched on; one access mode for the whole API - `public`, `token` (a project API token, which reads only its own project's pages) or a role (a Bearer access token, never the login cookie).
927
+ - A page it serves is exactly a page the site would serve the same caller - the two share one decision (`resolvePublishedPage()`). Front matter `api: false` keeps a page off it.
928
+ - Every payload is an allowlist: no custom front matter, no mail settings. Each page says which Domma behaviour its HTML needs (`requires`).
929
+ - Tokens take content scopes, `@pages`, `@menus`, `@site`, `@search`; a token scoped only to collections gets no content.
930
+ - The screen's Try it fetches a page as an anonymous caller would get it, before the API is switched on.
931
+ ```bash
932
+ curl -H 'Authorization: Bearer dcms_…' 'https://example.com/api/content/v1/pages/about?format=both'
933
+ ```
934
+ Where: **System > Content API** (`#/content-api`); `config/content-api.json`; `server/routes/api/content-public.js`; **API Reference > Content API**. Since: unreleased.
935
+ - **API Builder**. Custom named GET endpoints at `/api/x/<project><path>`, with no code.
936
+ - Each endpoint is a path with `:param` segments bound to a collection query:
937
+ - filter clauses using the filter-engine operators, whose values can use `{{params.x}}` (required) or `{{query.x}}` (the clause is dropped when the parameter is absent)
938
+ - sort, order and limit (callers can page and sort, capped at the endpoint's limit)
939
+ - a field allowlist
940
+ - `mode: list|single`
941
+ - `auth: public|token|<role>`
942
+ - Public responses are cached and refreshed automatically when data changes.
943
+ - An endpoint can only expose its own project's collections or core ones, never system collections. Duplicate path shapes are refused with 409, and static path segments take priority over `:param` segments.
944
+ - The editor includes a try-it console that sends raw requests without admin credentials against the saved definition.
945
+ ```json
946
+ {"name": "Fixtures by day", "project": "world-cup", "path": "/fixtures-day/:date", "collection": "wc-schedule",
947
+ "auth": "public", "mode": "list", "filter": {"date": "{{params.date}}"}, "sort": "seq", "order": "asc", "fields": ["seq", "title"]}
948
+ ```
949
+ Where: **Data > API Builder** (`#/api-endpoints`); `/api/api-endpoints`; `server/services/apiEndpoints.js`, `server/routes/api/endpoints-public.js`; permission family `api-endpoints.*`. Since: 0.25.0; 0.33.x (query filters, paging); 0.77.0 (editor Tool).
950
+ - **In-admin API reference**. **Documentation > API Reference** has pages for Authentication, Pages, Settings, Layouts, Navigation, Media, Users, Plugins, Collections, Views and Actions, and points to API Tokens, API Builder and the Content API for the external surface. The full reference is `docs/api-reference.md`.
951
+ Where: `#/docs/api/<area>`; `admin/js/templates/docs/api-*.html`.
952
+ - **Hooks for developers**. `collection:entryCreated`, `collection:entryUpdated`, `collection:entryDeleted` and `form:submitted` are emitted for plugins to listen to.
953
+
954
+ ---
955
+
956
+ ## Admin Experience & Built-in Tools
957
+
958
+ ### The admin shell
959
+
960
+ - **Single-page admin app** - The admin at `/admin` is a hash-routed single-page app built on Domma. Every screen is a "Tool" with a shared banner (title, actions, settings cog), keyboard-reachable rows, right-click / ⋮ / Shift+F10 context menus and help popovers.
961
+ Where: `admin/js/app.js`, `admin/js/views/*`, `admin/js/lib/tool-kit.js` - Since: 0.5.0 (SPA); the "Tool upgrade path" came in 0.69-0.77.
962
+ - **One Save everywhere** - Editors (pages, forms, roles, users, settings, My Profile and others) have a single Save in the banner, an "Unsaved" bar with Discard, and Ctrl+S / Cmd+S. Leaving with unsaved changes asks first and keeps a draft that is offered back on the next visit. Passwords are never kept in drafts.
963
+ Where: every editor; global Ctrl+S in `admin/js/app.js` - Since: 0.76.0-0.77.0.
964
+ - **Sign-out guard** - Signing out while an editor has unsaved changes asks first, and Cancel leaves you signed in on the editor.
965
+ Since: 0.77.0.
966
+ - **Back to where you were** - Opening an admin link while signed out (or after a session runs out) goes to sign-in and then straight back to that screen. An expired session says so.
967
+ Where: `#/login?next=...&why=expired` - Since: 0.88.0.
968
+ - **Sidebar driven by menu data** - The sidebar is the `admin-sidebar` menu, with folders Overview, Projects, Content, Data, Tools, System and Documentation, and Marketplace at the top level. Admins can move, hide or rename any Tool from **Content > Menus > Admin sidebar**. Tools that plugins or core register go to their default folder unless the menu places them somewhere else.
969
+ Where: `config/menus/admin-sidebar.json`, `admin/js/lib/sidebar-grouping.js` - Since: 0.72.0.
970
+ - **Tool groups (folders inside folders)** - A Tool can name a `group` as well as a folder, and then sits in a sub-folder of that folder. The built-in Tools (Notes, Todo, Analytics, Contacts, SEO) register with group `Core`, so a default install shows **Tools > Core > Notes**; Marketplace plugin Tools stay directly in Tools unless they name a group. The group folder is keyed `<folder>.<slug>` (`tools.core`), found by key anywhere in the menu (so an admin can move the whole group) or by name inside the folder, and made at render time when the menu has none - it is only saved once something the admin saved is inside it. An admin's own arrangement still wins, a user who may use none of a group's Tools does not see it, closed groups add up their Tools' counts like any folder, and **Back to its default place** in the Menus editor returns a Tool to its group. Each folder's open/closed state is now kept per position (its key, or its parent path), so Tools > Core and a project called Core no longer open and close together; top-level folders keep the state they had.
971
+ Where: `registerSidebarItem({group})`, plugin.json `admin.sidebar[].group`, `placeTools` and `sidebarStateKey` in `admin/js/lib/sidebar-grouping.js` - Since: Unreleased.
972
+ - **Dashboard Pro (paid) - a widget board** - with the Dashboard Pro Tool, `#/` becomes a board of widgets instead of the fixed Dashboard. **Add widget** opens a gallery of every widget the user may add, grouped by Tool and searchable; cards drag by their grip (Domma `E.sortable`, indicator mode on a wrapping grid), size Small / Medium / Large / Full width and move or go from their ⋮ / right-click menu (Alt+arrows and Delete on a focused card). Each user's layout is their own (`content/dashboard/layouts/<user>.json`, ignored in a site repo); **Reset** returns to the site default, which an admin sets with **Make this the default** (`content/dashboard/default.json`). Widgets: core's six (traffic, visitors now, needs attention, recent activity, site health, top pages - `GET /api/dashboard/widget/:name`), one `count` widget for every sidebar badge (so Email, Booking, Support... arrive without changes), and any a plugin declares in `admin.widgets`. The board is core code switched on only by a paid, closed-source Tool calling `hooks.enableDashboardPro()` (the instant-calls contract); without it every widget route answers 404. `server/services/widgets.js`, `server/routes/api/widgets.js`, `admin/js/views/dashboard-board.js`, `admin/js/lib/dashboard-board-arrange.js`, `admin/js/lib/widget-render.js`; plugin docs under "Dashboard widgets" in `docs/plugin-development.md`.
973
+ - **The cog** (banner): per person - the Dashboards name (banner, tab title and the sidebar's `#/` entry, relabelled by `/api/sidebar/menu` and `/registered-items` while it still says "Dashboard"), greeting, 3/4/6 columns (wide screens; narrow cards drop their icon and wrap the title), compact cards, updates live/slower (3x)/paused, and "open on my boards" (`/api/auth/permissions` answers `adminHome: /dashboards` for an unconfined user; the Overview tab gets past it once via `allowOverviewOnce()`); kept in `content/dashboard/prefs/<user>.json` (ignored). For everyone (admins) - the board's default name and **lock the layout**: non-admins get the site default (`source: locked`), PUT/DELETE of their layout answer 423, and the board offers fold and refresh only; `content/dashboard/site.json`. `server/services/dashboardSettings.js`, routes `GET/PUT /api/dashboard/settings`, `PUT /api/dashboard/settings/site`.
974
+ - **Dashboards (1.5)** - with Dashboard Pro, one sidebar entry (Overview > Dashboards, the `#/` item relabelled) and tabs: **Overview** first and fixed - whichever view owns `#/` (the core Dashboard, or a Tool's, like the fleet manager's), wrapped in `app.js` by `withDashboardStrip()` so its strip needs nothing from the Tool - then the person's boards at `#/dashboards?board=<id>` (`views/dashboards.js` -> `dashboard-board.js`). `#/my-dashboard` redirects there; `overview` is reserved and never a board id. Pure: `admin/js/lib/dashboards-arrange.js` (tabs, addresses, label); strip and banner: `admin/js/lib/dashboards-strip.js`.
975
+ - **Sidebar right-click menu and Frequent** - right-click any Tool in the sidebar for Open, Open in a new tab, Add to / Remove from Frequent and Copy link; right-click a folder to open or close it, or every folder at once. **Frequent** is each admin's own folder at the top of the sidebar (up to 12 Tools, in their order - Move up / Move down inside it, Clear Frequent on the folder). It is personal, kept on the server per user (`content/sidebar/frequent/<user id>.json`, via `GET`/`PUT /api/sidebar/frequent`) so it follows them to any browser, never written into the shared `admin-sidebar` menu, and built from the sidebar after permission filtering, so it can only ever list Tools that user may open. Count badges show on both copies. `admin/js/lib/sidebar-frequent.js` (pure, tested), wired in `sidebar-renderer.js` (`wireSidebarMenu`).
976
+ - **Hotkeys and the palette** - **G then a letter** opens a Tool from anywhere (G E Email, G D Dashboards; built-in letters in `CORE_HOTKEYS`, a plugin declares `hotkey` on its `admin.sidebar` entry), **G then 1-9** the Frequent folder's Tools in order, **Ctrl+K or Ctrl+D** (Cmd on a Mac) a palette that finds any Tool by name, initials or folder, and **?** the list of every hotkey. Pausing after G shows a hint chip (Frequent numbers, then letters; `chipEntries`) until the letter or the lapse. Shown on hover in the sidebar and beside Open in a Tool's right-click menu. Built-ins claim first, then sidebar order; a later claimant goes without. Keys wait while typing; the sequence and palette listen in the capture phase and claim their key, so a Tool's own single-letter keys (Email's S) never hear the second key, while `?` gives way to a Tool that claims it. `admin/js/lib/hotkeys.js` (pure, tested) and `hotkeys-ui.js`.
977
+ - **Arrange the sidebar in place** - a six-dot button in the sidebar header (users who may update menus) turns on Arrange: the sidebar's items are set aside and the `admin-sidebar` menu is shown as draggable rows, hidden items dimmed, Tools where they will appear. Drag to move, drop on the middle of a row to put it inside, the eye hides or shows (locked Tools show a padlock), Alt+arrows move the focused row. Done saves the menu and redraws the sidebar for everyone; Cancel or Esc leaves it untouched, asking first if anything moved. It edits exactly what the Menus editor edits (same rows, same drops via `admin/js/lib/menu-drag.js`, same save shape); labels, icons, badges and visibility stay in the Menus editor. `admin/js/lib/sidebar-arrange.js`.
978
+ - **Tools that cannot be lost** - Dashboard, Menus, Users and My Profile can be moved but not hidden or removed. If a menu edit hides one, the sidebar puts it back, so an admin can always find a way back.
979
+ Since: 0.72.0.
980
+ - **Live count badges with hover details** - Sidebar items show live counts, for example Pages (with drafts), Forms (submissions), Notifications (unread), Todo (open tasks, red when any are overdue; tasks completed once all are done), Notes, Analytics (visitors now, with a pulsing dot; page views today while nobody is on), Users, Roles, Layouts, API Tokens, Theme overrides, Effects, My Profile (empty details) and Site Settings (problems). Hovering or focusing a badge opens a popover with the detail behind it. Badges refresh every minute, when the tab regains focus, and straight away when you act. A closed folder shows the total of its Tools' "signal" counts, with a breakdown on hover.
981
+ Where: `server/services/sidebarBadges.js`, `admin/js/lib/sidebar-renderer.js`, `/api/<x>/_count` routes. Plugins add a badge with `countUrl` and the `dm:sidebar-count` event - Since: 0.69.0 (popovers, folder totals), extended in 0.75.0 and 0.80.0.
982
+ - **Admin home setting** - Choose which screen the admin opens on: the built-in dashboard or any Tool or page in the sidebar. If a user lacks permission for that screen, or its plugin has been switched off, they get the built-in dashboard instead. A confined role always uses its own home.
983
+ Where: **System > Site Settings > General > Admin home** (`site.json` `adminHome`), `server/services/adminHome.js` - Since: 0.72.0.
984
+ - **Resizable, remembered slideovers** - Every side panel, including those that plugins open, can be resized by dragging, and remembers its width per panel.
985
+ Where: `admin/js/lib/slideover-resizable.js` (patches `E.slideover` once at boot) - Since: 0.66.0.
986
+ - **Activity throbber** - One loader appears when any request takes longer than 200 ms. It sits beside the banner title, or in the top bar if the screen has no banner.
987
+ Where: `admin/js/lib/activity-throbber.js` - Since: 0.72.0.
988
+ - **Loading placeholders (skeletons)** - While a screen's first load runs, its loading slot fills with Domma skeletons shaped like what is coming: rows on Pages, Collections, Forms, submissions, Users, Menus, Marketplace (installed) and Notifications, a table on a collection's entries, tiles on Media, and figures, lists and a chart block on the Dashboard. Nothing shows for a load under 150 ms, and once shown a skeleton stays at least 300 ms so it never flickers; it is always removed on an error (the screen's own error message shows) and `aria-busy` is always cleared. A refresh that keeps the list on screen shows no skeleton. On a Domma core without `E.skeleton` (before 0.48) the old "Loading..." text shows instead. New screens use the same helper: `whileLoading(slot, promise, SHAPES.rows)` or, across several requests, `showLoading(slot, shape).done()`.
989
+ Where: `admin/js/lib/loading.js` (the helper, `SHAPES`), the `.dm-loading-slot` element in each view's template, `admin/css/admin.css` - Since: Unreleased.
990
+ - **Help popovers** - Click-to-open (?) help icons on fields and figures across the Tools. They also work on touch screens.
991
+ Mark any element with `data-help="text"` (plain text, never HTML) and optionally `data-help-title`, then call
992
+ `helpPopovers(root, {position})`; it returns a disposer that destroys its popovers. Each (?) opens a Domma `E.popover`: one help
993
+ open at a time, Esc or a click outside closes it, Esc returns focus to the (?), it follows the page as it scrolls
994
+ and flips side when there is no room, and Esc inside a slideover closes only the help. On a Domma core without
995
+ `E.popover` (before 0.49) the older built-in panel is used instead, so help is never lost.
996
+ Where: `admin/js/lib/help-popover.js` - Since: 0.69.0.
997
+ - **Admin theme** - The admin has its own theme, separate from the public site. You can use any site theme or one of nine admin-only themes (Sharp, Slate and Smooth finishes, each in Indigo, Steel or Teal). Custom themes from the Theme Roller are supported too. The site's day/night auto-theme is set on the same screen. Picking an admin theme applies instantly as a try-on; Save makes it everyone's, discarding or leaving without saving puts the saved theme back.
998
+ Where: **Content > Theme** (`site.json` `adminTheme`, `autoTheme`), `admin/js/lib/admin-theme.js`, `GET /api/theme/admin` - Since: Admin Slate 0.85.0.
999
+ - **Dates read dd/mm/yyyy** - Every date a person sees reads dd/mm/yyyy, with dd/mm/yyyy HH:mm (24-hour) where a time is shown: admin lists (which used to fall back to ISO `2026-09-01` after a fortnight), draft-restore notes, entry, version, media, user and project details, collection date fields and their filter chips, the SEO screen, related items, the analytics chart axis, sidebar badge popovers, notifications, the password-changed email, the share-link expiry note, and the shared mail reader, compose window, send-later text and the `{date}` / `{time}` template tokens. Relative wording ("3 days ago", "tomorrow at 09:00") is kept. Stored values, API payloads, date inputs, filenames, feeds and CSV exports are unchanged.
1000
+ - **One helper per side**, each exporting `toDate`, `fmtDate`, `fmtTime`, `fmtDateTime`: `admin/js/lib/dates.js` (browser, local time), `server/services/dates.js` (server; `{utc: true}` option), and for plugins `plugins/_lib/admin/ui/dates.js` (browser and server). Marketplace plugins that use the plugin helper need 0.93.0.
1001
+ - The exception: a plugin's release date (Marketplace rows and plugin banners) reads dd.mm.yyyy, a format asked for on purpose.
1002
+ Where: commits `3fbac25` (the helpers), `9d87cf0`, `f722195`, `2a6a6fd`, `0bbfab3` - Since: 0.93.0.
1003
+ - **Admin brand** - The site title shown in the sidebar header can be overridden for the admin (`adminBrand.title`).
1004
+ Where: `config/site.json`.
1005
+ - **Server-restart handling** - Switching a plugin or Tool can restart the server. The admin waits for the server to come back instead of failing requests.
1006
+ Where: `admin/js/lib/server-restart.js`; `server.json` `restartOnPluginToggle`. Auto-restart needs a supervisor such as PM2.
1007
+
1008
+ ### My Profile & avatars
1009
+
1010
+ - **My Profile** - Any signed-in user can change their name, email and password, plus any extra profile fields an administrator has defined, in one form with one Save that sends only what changed. A read-only card shows their role, project access and dates.
1011
+ Where: **System > My Profile** (`#/my-profile`), `PUT /api/auth/me` - Since: 0.76.0 as a Tool.
1012
+ - **Profile completeness badge** - The sidebar badge counts your details that are still empty.
1013
+ Where: `GET /api/auth/me/_count` - Since: 0.80.0.
1014
+ - **Avatars** - Click or drop an image onto the profile circle to set an avatar (any common photo up to 8 MB, iPhone HEIC included). It is re-encoded on the server into a square 256 px WebP with metadata removed, and shows in the top bar. Remove it to go back to initials.
1015
+ Where: `POST/DELETE /api/auth/me/avatar`, files in `content/avatars/`, `server/services/avatars.js` - Since: 0.82.0.
1016
+
1017
+ ### Dashboard & site health
1018
+
1019
+ - **Dashboard** - The site at a glance, with KPI cards for page views today (and change on yesterday), visitors now, pages and drafts, form submissions this week and site health. It also shows:
1020
+ - "Needs attention", which gathers the red and amber sidebar badges and any failing health check.
1021
+ - Recent activity, a 30-day traffic chart in theme colours, top pages, spikes, visits and the response cache.
1022
+
1023
+ It refreshes every 30 seconds. The Refresh and New page buttons are in the banner.
1024
+ Where: **Overview > Dashboard** (`#/`), `admin/js/views/dashboard.js`, `GET /api/dashboard/summary` - Since: 0.77.0 as a Tool. Sites that run the manager's site-manager plugin see its dashboard instead.
1025
+ - **Health checks** - Server and site checks, each rated ok, warn or fail: uptime, memory, content disk usage, stale pages, pages missing meta descriptions, last form submission, last email sent, whether an SMTP host is set, and plugins that failed to load.
1026
+ Where: `server/services/health.js`. Shown only through the dashboard summary, with no standalone endpoint.
1027
+
1028
+ ### Notifications
1029
+
1030
+ - **Notifications inbox** - One row per notification with severity, time, source and link. You can filter by New or severity and search. Click a notification to read it in full. From its menu you can open its link, copy it, mark it unread, dismiss it or delete it for everyone. Dismiss all is in the banner. A topbar bell shows the unread count, and sticky toasts appear at sign-in for unread warning and critical notifications.
1031
+ Where: **System > Notifications** (`#/system/notifications`), `server/routes/api/notifications.js` - Since: 0.76.0 as a Tool.
1032
+ - **Site-raised notifications** - Core and plugins raise their own notifications, which go to Super Admins by default. Repeats update one entry rather than stacking, and a notification clears itself when its cause goes away. Each source is limited to 30 new notifications an hour.
1033
+ Where: `server/services/notify.js` - Since: 0.80.0.
1034
+ - **Core notification sources** - Each can be switched off or sent to all admins or everyone. Timed checks have a Run now button.
1035
+ - Critical: plugin failed to start, site restarted after a crash, database unavailable.
1036
+ - Warning: plugin licences, API tokens expiring, disk space low, start-up problems, email not delivered, repeated failed sign-ins, password reset not sent, admin rights given, form actions failing.
1037
+ - Info: form triggers, Actions.
1038
+ - Off by default: every form submission, users added or removed.
1039
+ - Overdue and due-today to-dos go to their owner (0.87.0).
1040
+
1041
+ Where: Notifications settings cog > "What this site tells you" (Super Admins only), `server/services/notification-sources.js`.
1042
+ - **Audiences** - A notification can target roles or specific users. Notifications with no audience (manager pushes, older entries) go to admins and above.
1043
+ Since: 0.86.0.
1044
+ - **Desktop notifications** - A per-browser switch pops each new notification up on the desktop, even when the admin is in a background tab. Choose all, warning and critical, or critical only. There is a Send a test button, and critical ones stay until closed.
1045
+ Where: Notifications settings cog, `admin/js/lib/desktop-notify.js` - Since: 0.80.0.
1046
+ - **Sample notifications** - Adds four made-up notifications, one per severity, visible only to the person who added them, for trying out the bell, the badge and desktop pop-ups.
1047
+ Where: Notifications settings cog > Add samples - Since: 0.80.0 (private to the requester since 0.86.0).
1048
+ - **Manager push** - The fleet manager can push notifications to a site.
1049
+ Where: `POST /api/system/notifications` (`MANAGER_SECRET`, `server/middleware/managerAuth.js`).
1050
+ - **Plugin API** - Plugins can raise notifications, resolve them and register their own sources through hooks: `notify()`, `resolveNotification()` and `registerNotificationSource()`, with `every: boot | 15m | hourly | daily` (daily runs at 08:00).
1051
+ Since: 0.80.0.
1052
+
1053
+ ### In-admin documentation
1054
+
1055
+ - **Documentation menu** - The admin carries its own handbook under **Documentation**:
1056
+ - **Usage**: Pages, Media, Navigation, DConfig, Shortcodes, Site Settings, Plugins, Users & Roles, Views, Actions, CTA Shortcode, **Collections & Forms** (`#/docs/usage/collections`), **Built-in Tools** (`#/docs/usage/tools`), **Editions & Licences** (`#/docs/usage/editions`: free vs paid vs Pro, rental and perpetual licences, managed-only plugins; the same text as `docs/editions-and-licences.md`).
1057
+ - **Tutorials**: Building a CRUD App (with a live recipe scaffolder on the page), Writing a Plugin, Form Follow-Up.
1058
+ - **Components**: Reference, How-To, Walkthrough, Rules.
1059
+ - **API Reference**: Authentication, Pages, Settings, Layouts, Navigation, Media, Users, Plugins, Collections, Views, Actions, and **Forms**, **Scaffold**, **External API & Tokens** (`/api/v1`, token mode) and **API Builder** (`/api/x`) (`#/docs/api/forms|scaffold|external|builder`).
1060
+ - **Plugins**: the user guides of the enabled Marketplace plugins (below).
1061
+
1062
+ The newer pages (Editions & Licences and the four API pages) are added to existing sites' sidebars at boot with `registerSidebarItem`, because the sidebar menu is seeded only once. Since: unreleased (after 0.93.0).
1063
+
1064
+ Where: `#/documentation`, `#/tutorials`, `#/docs/...`, `#/api-reference`; templates in `admin/js/templates/docs/`; `admin/js/views/doc-pages.js`.
1065
+ - **Plugin guides** - Each enabled Marketplace plugin can ship user guides as `plugins/<name>/docs/*.md`. They appear in the admin under **Documentation > Plugins** (`#/docs/plugins` lists them, `#/docs/plugins/<slug>` opens one, one tab per file), shown to users who hold the plugin's sidebar permission, or the manifest's `docs.permission` when it sets one (anyone signed in to the admin when neither names a permission). A guide is plain Markdown with optional frontmatter `title` (the tab label) and `order` (lowest first; `guide.md` comes first by default); shortcodes are not run, so a `[collection ...]` example shows literally, and the HTML is sanitised like a page body. Only plugins that loaded (enabled, licensed, not superseded) are listed.
1066
+ Where: `server/services/pluginGuides.js`, `GET /api/documentation/plugins`, `/api/documentation/plugins/:name[/:page]` (403 for a user who cannot use the plugin), `admin/js/views/plugin-guides.js`; the plugin template ships a starter `docs/guide.md`. Since: unreleased (after 0.93.0).
1067
+ - **Pro docs** - A hidden route with reference material for the Pro features: Views, Actions and the CTA shortcode.
1068
+ Where: `#/pro/docs` (`admin/js/views/pro-docs.js`). It is not in the sidebar.
1069
+ - **CRUD tutorial slideover** - A guided walkthrough, opened from the page editor, for building a create/read/update/delete app from Collections, Forms and Actions, with copy-ready snippets.
1070
+ Where: `admin/js/lib/crud-tutorial.js`.
1071
+ - **Public handbook** - The same doc templates can be published as public, themed pages at `/domma-docs/...`, rendered at request time so they update whenever the CMS does. Switch this on by enabling the built-in `domma-docs` project, which is off by default and cannot be deleted. While it is off, the whole path returns 404.
1072
+ Where: `server/services/docs.js`, `server/routes/docs-public.js` - Since: 0.30.0-0.31.0.
1073
+
1074
+ ### Site Settings
1075
+
1076
+ - **Site Settings** - One screen with one Save covering nine tabs:
1077
+ - General: title, tagline, Site URL, admin home, default spacer size.
1078
+ - SEO defaults: default title, separator, meta description.
1079
+ - Footer: copyright, a "Reduce motion" switch, social icons for X, Facebook, Instagram, LinkedIn, GitHub and YouTube.
1080
+ - Email: SMTP host, port, user, password and TLS; From address and name; send a test email.
1081
+ - Back to top: on/off, label, threshold, position, offsets, smooth scrolling.
1082
+ - Cookie consent: message, position, layout, colours, categories, policy links, button text, consent version.
1083
+ - Breadcrumbs: on/off, home label, corner, offsets.
1084
+ - Custom CSS.
1085
+ - Cache: status, on/off, clear, and cached pages as rows.
1086
+
1087
+ Each tab shows a dot while it has unsaved changes.
1088
+ Where: **System > Site Settings** (`#/settings`), `config/site.json`, `server/routes/api/settings.js` - Since: 0.76.0 as a Tool.
1089
+ - **Needs attention** - The screen and its sidebar badge flag settings that are quietly wrong: no SMTP host (mail goes to a test inbox and reaches no one), no From address, or a cookie banner with no privacy policy link.
1090
+ Where: `GET /api/settings/_count` - Since: 0.55.0 (SMTP warnings), 0.76.0.
1091
+ - **Site URL** - The site's own address (`site.baseUrl`), used for reset links, canonical links and the sitemap. It is checked as you type.
1092
+ Since: 0.88.0.
1093
+
1094
+ ### Built-in Tools
1095
+
1096
+ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They are on by default and appear in the **Tools > Core** sidebar group. SEO joined them in 0.90.0 and is covered by the SEO section.
1097
+
1098
+ - **Analytics** - Privacy-friendly page-view counting on your own server, with no third parties. A browser beacon posts to `/api/analytics/hit`, so bots, blocked scripts and Do-Not-Track visitors are left out. Admin pages and `/api` are never counted, and preview renders are not counted either (since 0.52.0).
1099
+ - Figures: views, visitors, sessions and bounce rate for today, 7, 30 or 90 days, all time or a custom range.
1100
+ - A trend chart (line or bar), top pages with share bars, and "Active now" (the last 5 minutes, refreshing itself).
1101
+ - A Journeys tab (entry pages, exit pages, common paths) and a Spikes tab (pages well above their usual rate).
1102
+ - CSV export and a counter reset.
1103
+ - The settings cog sets the opening range, rows shown, chart type, live refresh and reset confirmation.
1104
+
1105
+ Counters live in `content/analytics/`.
1106
+ Where: **Tools > Analytics** (`#/analytics`), `server/routes/api/analytics.js`, `public/js/analytics.js`; permissions `analytics.read` and `analytics.delete` - Since: 0.67.0 core, 0.69.0 rebuilt.
1107
+ - **Contacts** - A personal address book for each user.
1108
+ - Fields: name, email, phone, company, job title, website, notes, VAT and company numbers, reference, billing email, currency, bank details (account name, bank, sort code, account number, IBAN, BIC), and several addresses, emails and phones, with one address marked for billing.
1109
+ - Groups, which are shared by everyone who uses Contacts, and favourites (starred). Anyone can file their own contacts in any group.
1110
+ - **Who may rename or delete a group** (0.93.0): only its creator, or a holder of **Contacts > Manage groups** (`contacts.manageGroups`), because a rename or delete changes every user's contacts. New groups record who created them; groups from before 0.93.0 have no recorded creator, so only holders of the permission may change them. The level-0 role holds it, and `admin` is granted it once (removed by hand, it stays removed). In **Manage groups** the Rename and Delete buttons are greyed out where the server would refuse, with a help popover saying why. `GET /api/contacts/groups/access` answers who may do what (`{manageAll, groups: [{name, mine, canManage}]}`); `GET /api/contacts/groups` is still a plain list of names. Contacts Pro 0.7.4 applies the same rule to restyling a group.
1111
+ - Search across names, emails, phones, company, notes and groups.
1112
+ - Each contact's menu offers edit, email, call, copy, star, add to or remove from a group, duplicate, select and delete. Bulk add-to-group and bulk delete work on a selection.
1113
+ - JSON export and import.
1114
+ - The settings cog sets sort, starred first, density and columns.
1115
+
1116
+ Where: **Tools > Contacts** (`#/contacts`), `/api/contacts`; collections `contacts-contacts` and `contacts-groups` (file storage by default; they can be moved to MongoDB with the Storage move - see Preset collections) - Since: 0.67.0 core, 0.69.0 rebuilt, 0.93.0 group rule.
1117
+ - **Notes** - Private notes for each user, with a title, content and categories.
1118
+ - Card or dense list layouts, search that also matches categories, and a category filter.
1119
+ - Each note's menu offers edit, duplicate, copy, filter by its category and delete.
1120
+ - Notes open in a resizable panel with a word count and Ctrl+Enter to save. Press `/` to search and `N` for a new note.
1121
+ - Settings (per browser): layout, sort, preview length, category chips, delete confirmation, default categories.
1122
+
1123
+ Where: **Tools > Notes** (`#/notes`), `/api/notes`; collection `notes-notes`, which declares MongoDB and uses it where the site has a working connection (files otherwise; see Preset collections) - Since: 0.67.0 core, 0.69.0 rebuilt, 0.93.0 MongoDB honoured.
1124
+ - **Todo** - To-do lists for each user.
1125
+ - Type a task and press Enter, with optional priority (highest to lowest) and due date.
1126
+ - Statuses are not started, in progress and completed. Filters are Open, All, each status and Overdue, plus search.
1127
+ - Due dates read "Today", "Tomorrow" or "3 days ago". Completed tasks sink to the bottom.
1128
+ - Every change is on the task's context menu.
1129
+ - A daily notice lists your overdue and due-today to-dos.
1130
+ - The settings cog sets the opening filter, sort, default priority, relative dates and delete confirmation.
1131
+
1132
+ Where: **Tools > Todo** (`#/todo`), `/api/todo`; collection `todo-items`, which declares MongoDB and uses it where the site has a working connection (files otherwise) - Since: 0.67.0 core, 0.69.0 rebuilt, 0.87.0 notices, 0.93.0 MongoDB honoured.
1133
+ - **Tool switches** - Contacts, Notes, Todo, Analytics and SEO can each be switched off. A Tool that is off keeps its data, but it disappears from the sidebar, its API answers 404, and anything it feeds stops, including notification sources, calendar layers, related items and the analytics beacon. A switch takes effect after a restart.
1134
+ Where: **Marketplace** (`#/plugins`) > "Built-in Tools"; `config/tools.json` (`{"notes": {"enabled": false, "locked": true}}`); `PUT /api/tools/:name`; `server/services/tools.js` - Since: 0.88.0.
1135
+ - **Dependencies and cascades** - A plugin's `plugin.json` can declare dependencies:
1136
+ - `requires` - the plugin cannot work without it. Contacts Pro requires Contacts.
1137
+ - `uses` - the plugin loses a named feature without it. Calendar uses Contacts to invite contact groups.
1138
+
1139
+ Before a switch changes anything, the admin is shown what else it affects. Switching off a requirement switches off the plugins that need it. Switching on a plugin switches on what it requires. Soft dependants stay on, and the feature each one loses is listed. The loader never starts a plugin whose requirement is off.
1140
+ Since: 0.88.0.
1141
+ - **Locks** - `locked: true` on a Tool or plugin switch means only the site's Manager can change it. The site's own admin sees "Locked" and gets a 423 response.
1142
+ Since: 0.88.0.
1143
+ - **Plugin takeover of a core Tool** - A plugin that `supersedes` a core Tool (for example Contacts Pro) hides the core sidebar entry and takes over its route. If the plugin's licence lapses, the core Tool comes back.
1144
+ Since: 0.72.0.
1145
+ - **Calendar sources and related items** - Two registries let plugins build on the core Tools without core depending on them:
1146
+ - `registerCalendarSource` - Todo supplies a "To-dos" layer of your open to-dos on their due days.
1147
+ - `registerRelatedItems` - lets a plugin such as Calendar show "Events" beside a contact or note and add "Schedule with..." and "Add to calendar..." menu items.
1148
+
1149
+ Before any provider is asked, core checks that the viewer owns the record. A provider that fails or is slow is named and left out.
1150
+ Where: `server/services/hooks.js`, `GET /api/related`, `GET /api/related/providers`, `admin/js/lib/related.js`; `contact:deleted` and `note:deleted` events - Since: 0.79.0.
1151
+ - **Instant calls - "Call Sam now…" (Pro)** - A video call on the site with one person, starting now, that is not a booking or an event. Core code, but only a paid Tool with calls switches it on (`hooks.enableInstantCalls()`, at boot - Booking Pro 0.4, Contacts Pro 0.8, Calendar 1.12; bound to the plugin's name and manifest; refused unless it is `tier: "paid"` AND `closedSource: true`, so only a licensed plugin can). Until one does, `/api/calls/*` answer 404, `/_call/*` is not claimed (it falls through to the pages) and nothing is offered anywhere - a free site never has instant calls. Disabling the last such Tool switches them off at once. While an older Booking Pro 0.3.x still registers its own (related provider `booking-pro-call`), core's actions and list stand aside, checked per request.
1152
+ - Related items: "Call {name} now…" on every contact's menu (core Contacts and Contacts Pro) and every email's menu (Email / Mail Reader), plus a "Calls" list on the contact card (the caller's own recent calls with that contact, "open now" while it lasts).
1153
+ - The Call now screen `#/calls/new` (`?contact=<id>` or `?email=&name=`; not in the sidebar; domma-reactive): pick a contact (own, or shared with you through Contacts Pro - `registerContactAccess`, marked "shared with you") or type a name and email, "Email them the link", Start the call - the host tab opens on the click and is pointed at the call, then their link with Copy / Email it…. Booking's banner Call now opens it too.
1154
+ - A call: a record `{id, name, email, contactId, hostId, hostName, token, hostKey, createdAt, expiresAt}` open 3 hours, kept in `content/sessions/instant-calls.json` (runtime state, never committed; at most 2000, forgotten 30 days after they end). The page is `/_call/<id>?t=` (guest) or `?h=` (host) in the site's shell but WITHOUT the site's/plugins' injected snippets or the analytics beacon (no-referrer, no-store, noindex; 404 "This call link is not valid", 410 "This call has ended"); the call page script takes the credentials out of the address bar (every Tool's call page), and a reload of the bare address on `/_call` comes back to them; the call engine (`_lib/call`) is mounted at `/api/calls/room`, guests knock and the host lets them in, no recording.
1155
+ - The email ("<host> would like to talk now", Join the call) goes through the site's SMTP only - with no SMTP host it is not sent, and the screen says so. Limits: 20 starts a minute per address, 10 per user (429), and 3 call emails an hour per recipient (past that the call is made, not emailed: `emailLimited`).
1156
+ - Permission `calls` (`read`, `create`) in the Tools group; Admin is granted it once, the first time a Tool with calls runs.
1157
+ - The call's chat is saved against the call (unreleased): the record's `chat` (the engine's items - typed lines and joined/left, the last 500, written through the service's queued writes), so the call reopened while it lasts (or after a restart) shows it. The contact card's Calls list shows "N messages" on a call with a chat; clicking it opens a read-only transcript slideover (`plugins/_lib/admin/ui/transcript.js`, domma-reactive, text bindings only). `GET /api/calls/:id/chat` (calls.read, AND the host who made the call or an admin - anyone else 404) answers `{messages, items}` with the host's lines marked `mine`, never participant ids or link keys; `GET /api/calls?contact=` carries `messages`.
1158
+ - Add people (unreleased): the host, in the call, clicks **Add people** (the control bar's `data-slot="invite"`; shown only when the join answers `canInvite`) - search the host user's readable contacts and the team (active users at the base user level or above), or type a name and email; tick Notification / Message / Email; Invite; or Copy link. Each invitee gets their OWN link `/_call/<id>?g=<pass>` (a pass signed with the call's `inviteKey`, made on first use - never the guest's token, which would take the guest's place), names themselves and knocks. Team members also get a notification ("<host> is calling you - Join", source `core:call-invites`, to them only) and a direct message from the host through Messages when it is on. Engine: `mountCallRoutes({invite})` -> `GET :room/people?q=` / `POST :room/invite` (host only by `X-Call-Id`/`X-Call-Secret`; 10 people a request, 20 a minute per caller, 30 Join emails an hour per site; contacts' emails masked and matched whole; `inviteOptions` search/typed/copyLink enforced by the engine; resolve's `history: false` hides the earlier chat); `_lib/call/invite.js`; `server/services/callInvites.js`; `hooks.registerDirectMessenger` / `sendDirectMessage` (hooks.js 7c). Booking 1.6, Calendar 1.14 and Messages 1.7 use the same. Tests `tests/plugins/call-invite.test.js`.
1159
+ - Background blur (unreleased): **Blur** on the join screen and in the control bar of every call (instant calls, Booking, Calendar, Messages - host and guests alike). `plugins/_lib/public/call/blur.js`, imported on first use: MediaPipe's selfie segmenter (vendored in `_lib/public/call/vendor/mediapipe/`, Apache 2.0, its usage logging patched out - see the README there) marks the person; a canvas draws the frame with the background blurred at 1/6 size and the person sharp on top; `captureStream` makes it a track that replaces the camera's on every connection (`replaceTrack`, like screen share) and in the self-view. Timer-driven (keeps going in a background tab), 30 fps, 15 when slow; stops itself after repeated failures and puts the camera back. Offered only with WebAssembly, canvas `captureStream` and WebGL2. Remembered per browser (`localStorage 'dm-call:blur'`). Needs `'wasm-unsafe-eval'` in the site CSP (server.js). About 12 MB, brotli in transit, loaded only when turned on. Tests `tests/plugins/call-blur.test.js`.
1160
+ Where: `server/services/instantCalls.js`, `server/routes/api/calls.js` (`POST /api/calls`, `GET /api/calls?contact=`, `GET /api/calls/status`, `GET /api/calls/contacts?q=`, `GET /api/calls/contact/:id`, `/api/calls/room/:id/*`), `server/routes/call-public.js`, `admin/js/views/call-new.js`; tests `tests/services/instant-calls.test.js`, `tests/routes/calls.test.js`.
1161
+
1162
+ ## Users, Roles & Security
1163
+
1164
+ ### Users
1165
+
1166
+ - **Users list** - One row per user showing avatar or initials, email, last sign-in, role and extra roles, project access, Inactive, and "Reset link open". You can filter by All, Active or Inactive and by role, and search or sort by name or last sign-in. Each user's menu offers Edit, Copy email, Send password reset email, Copy a reset link, Withdraw the reset link, Make active or inactive, and Delete. Actions you cannot take are shown greyed out with the reason.
1167
+ Where: **System > Users** (`#/users`, and `#/projects/<slug>/users` for one project), `/api/users` - Since: 0.76.0 as a Tool.
1168
+ - **User editor** - One form covers the account (name, email, password, active), the primary role plus any additional roles, project access (`projects: []`), project ownership (`meta.project`) and the profile fields. A new user is created with everything in one request, so a failure cannot leave a half-made account. An email that is already taken is flagged on the field.
1169
+ Where: `#/users/new`, `#/users/edit/:id` - Since: 0.77.0.
1170
+ - **Management hierarchy** - You can edit yourself but not deactivate or delete yourself. You can manage other users only if your role is more senior (a lower level number) than theirs. This rule applies to editing, deleting and resetting passwords.
1171
+ Where: `canManageUser` in the users routes and `admin/js/lib/users-arrange.js`.
1172
+ - **Multi-role users** - A user has one primary role and optional `additionalRoles`. A permission check passes if any of their roles grants it. Level checks use their most senior role.
1173
+ Where: `server/services/userRoles.js`.
1174
+ - **Custom profile fields** - Administrators add fields to the `user-profiles` preset collection. The fields then appear in the user editor and on My Profile, and count towards the profile badge.
1175
+ Where: `server/services/userProfiles.js`.
1176
+ - **Admin-initiated password reset** - An admin can email a reset link or copy one to hand over (shown once), and can withdraw an open link. These resets do not count towards the per-account email limit.
1177
+ Since: 0.88.0.
1178
+
1179
+ ### Roles & permissions
1180
+
1181
+ - **Dynamic roles** - Roles are data, stored in the `roles` preset collection. Three base roles are seeded and cannot be deleted:
1182
+ - Super Admin (level 0) - holds every permission, including those added by plugins.
1183
+ - Admin (level 1) - also granted the built-in Tools (`notes`, `todo`, `contacts`, `analytics`, `seo`) and `contacts.manageGroups` once at boot (`grantAdminToolDefaults()`, marker `core:<permission>` in `grantedBy`), so one removed by hand stays removed.
1184
+ - User (level 2) - notifications only.
1185
+
1186
+ Newly shipped permission families are added to the base roles at boot, and nothing an admin added is ever removed.
1187
+ Where: `server/services/roles.js` - Since: 0.5.0. The current base roles replaced the old admin, manager, editor and subscriber set.
1188
+ - **Roles screen** - One row per role, most senior first, showing its badge, permissions, level, how many users hold it, the plugin that owns it and its admin scope. Filters: All, Built in, Yours, Plugins. New role opens in a slideover. The sidebar badge turns red while a user holds a role that no longer exists.
1189
+ Where: **System > Roles** (`#/roles`), `GET /api/roles/_holders`, `GET /api/roles/_count` - Since: 0.76.0.
1190
+ - **Role editor** - A permission matrix grouped by area. You can search it, filter to Granted or Not granted, toggle single actions or grant Full access, set View only or None from an area's context menu, and grant or clear a whole group. "What it can do" reads the ticks back in words. Before you save a role you hold, it warns if the change would make you less senior or remove what you need to undo it.
1191
+ Where: `#/roles/edit/:id` - Since: 0.77.0.
1192
+ - **Permission registry** - A single list of every resource and its actions. Most resources use read, create, update and delete. The exceptions are `theme` (read, update), `analytics` (read, delete), `seo` (read, manage), `plugins` (adds develop), `notifications` (read, update, delete) and `contacts` (adds `manageGroups`, 0.93.0).
1193
+ - Content: pages, media, blocks, components.
1194
+ - Structure: navigation, menus, context-menus, projects, layouts.
1195
+ - Data: collections, views, actions.
1196
+ - Configuration: users, theme, settings, plugins, notifications, api-tokens, api-endpoints.
1197
+ - Tools: contacts, analytics, seo, notes, todo.
1198
+
1199
+ Plugins contribute their own resources on top. The role editor lists the groups in that order, Tools before Plugins (fixed in 0.93.0).
1200
+ Where: `server/services/permissionRegistry.js`, `GET /api/auth/permissions-registry`.
1201
+ - **Admin scope (confinement)** - A role can confine its holders to certain admin screens with `admin: {home, allow: [...]}`. A user is confined only if every role they hold is confined, and Super Admin is never confined. The server applies the scope to plugin APIs as well (fixed in 0.86.0).
1202
+ Where: Role editor > Admin area; `normaliseAdminScope` / `getAdminScope` in `roles.js`; `admin/js/lib/admin-scope.js` - Since: 0.71.0.
1203
+ - **Plugin roles and permissions** - Plugins declare `permissions[]` in `plugin.json`, with `grant` to give them to existing roles, and ship `roles/*.json`. They can also create and remove roles from code with `hooks.registerRole()` and `unregisterRole()`. A plugin role is created once and then belongs to the admin. Disabling the plugin keeps the role, so user assignments survive, and uninstalling removes it.
1204
+ Since: 0.71.0.
1205
+ - **Role-aware public site** - A signed-in visitor sees the navbar, footer and menu items meant for their role. Visibility supports both "this role and above" and `=role` ("this role only").
1206
+ Since: 0.71.0.
1207
+
1208
+ ### Authentication & sessions
1209
+
1210
+ - **First-run setup in the browser** - While no users exist, `/admin` shows a setup form to create the first Super Admin, followed by an onboarding wizard: name the site, pick a theme, done. `POST /api/auth/setup` is refused once any user exists.
1211
+ Where: `admin/js/views/login.js`, `/api/auth/setup-status` - Since: fixed in 0.88.1 (the site name goes to the navbar brand and the theme to theme settings).
1212
+ - **JWT sign-in** - Short-lived access tokens (15 minutes) and refresh tokens (7 days), with bcrypt hashing (10 rounds). All of these are set in `config/auth.json`. The server will not start if `JWT_SECRET` is missing, is `CHANGE_ME`, or is shorter than 32 characters.
1213
+ Where: `server/routes/api/auth.js`, `server/middleware/auth.js`.
1214
+ - **Sessions that stay signed out** - Each sign-in creates a session stored on disk, so signing out holds even across a restart. A new password ends your other sessions. An admin changing someone's password or deactivating the account ends that person's sessions.
1215
+ Where: `content/sessions/` (never committed), `server/services/sessions.js`; `GET /api/auth/sessions`, `DELETE /api/auth/sessions/:sid`, `POST /api/auth/sessions/revoke-others` - Since: 0.83.0.
1216
+ - **Viewer session cookie** - Sign-in also sets an `dm_session` cookie. Only the public page renderer reads it, so role-gated pages and draft previews work when a page is opened directly by URL. The API itself accepts bearer tokens only, which keeps cross-site request forgery exposure at zero.
1217
+ Where: `server/services/viewerSession.js` - Since: 0.50.0.
1218
+ - **Password reset** - "Forgot your password?" answers immediately and the same way for every address, so it does not reveal which accounts exist. Each account gets at most 3 reset emails an hour. Links only ever point to a trusted origin (`site.baseUrl`, or an address someone has signed in from), never to the host a request claims.
1219
+ The reset screen checks the link as soon as it opens. It names the account, shows how long the link has left, and checks the new password as you type. Without a mail server, the login screen says a link cannot be sent and the admins are notified. The expiry is `resetTokenExpiry` in `config/auth.json`.
1220
+ Where: `server/services/passwordReset.js` - Since: 0.5.0, overhauled in 0.88.0.
1221
+ - **"Your password was changed" email** - After any password change, whether by reset, My Profile or an administrator, the account holder gets an email saying how and when it happened.
1222
+ Since: 0.88.0.
1223
+ - **Sign-in screen comforts** - Password boxes have show/hide and a Caps Lock warning. The email you typed carries over to Forgot password.
1224
+ Since: 0.88.0.
1225
+ - **Plugin sign-in checks and two-factor authentication** - Plugins can add checks to sign-in:
1226
+ - `registerLoginCheck` with a `before` phase (for example a lockout, applied before the password is checked) or an `after` phase (for example two-factor).
1227
+ - `registerPasswordCheck` - rules applied wherever a password is set.
1228
+
1229
+ An `after` check can ask one more question on a "One more step" screen. That screen supports a recovery code, "Trust this browser for 30 days", and a QR code for setting up an authenticator app during sign-in (0.84.0).
1230
+ Where: `server/services/authChecks.js`, `POST /api/auth/login/verify` - Since: 0.83.0.
1231
+ - **Auth events** - `auth:loginFailed`, `auth:loginSucceeded`, `auth:loggedOut`, `auth:passwordChanged`, `auth:rateLimited`, `auth:resetRequested`, `auth:resetIssued` and `user:loggedIn`.
1232
+
1233
+ ### Audit, security posture and hardening
1234
+
1235
+ - **Audit events** - Every successful POST, PUT, PATCH or DELETE under `/api/` by a signed-in user raises an `audit` event. It records the actor, the action (for example "pages update"), the route, the target, the IP address and the names of the fields sent, never their values. Core stores nothing itself. The event is for plugins such as Security Pro to keep an audit trail.
1236
+ Where: `server/services/audit.js`, `onResponse` hook in `server.js` - Since: 0.83.0.
1237
+ - **Security (built-in plugin, on by default)** - Adds:
1238
+ - a scored health check covering unused admin accounts, CMS version, HTTPS certificate, security headers, private files being served, proxy addresses, email for resets and licences;
1239
+ - sign-in lockout that escalates with repeated failures;
1240
+ - password rules (at least 10 characters, not among the 10,000 most common, no name or email);
1241
+ - a sign-in log with CSV export;
1242
+ - scanner blocking (WordPress, `.env`, `.git` and database-dump probes), plus manual IP blocks.
1243
+
1244
+ If you lock yourself out, run `node plugins/security/bin/unlock.js --all`.
1245
+ Where: **Tools > Security** (`#/plugins/security`), `plugins/security/` - Since: 0.83.0.
1246
+ - **Rate limiting** - Every route is limited to 100 requests a minute. Tighter limits apply to:
1247
+ - login: 5 a minute
1248
+ - forgot password: 3 an hour
1249
+ - reset password: 5 a minute
1250
+ - two-factor verify: 10 a minute
1251
+ - admin password-reset actions: 20 per 10 minutes
1252
+ - avatar upload: 20 per 10 minutes
1253
+
1254
+ Only genuinely local calls are exempt. Before 0.83.0, every loopback request was exempt, so a site behind a proxy had no effective limits.
1255
+ Where: `server/services/network.js` - Since: 0.83.0.
1256
+ - **Headers and input hygiene** - Helmet with a Content-Security-Policy, CORS off by default, sanitising of HTML output, path-traversal guards, and UUID checks on user IDs.
1257
+ Where: `SECURITY.md`.
1258
+ - **Pen test (3 April 2026)** - An internal black-box and grey-box test found:
1259
+ - High: stored XSS in page titles, and no rate limit on login. Titles are now escaped in the renderer and login is rate-limited.
1260
+ - Medium: an HTML file uploaded as a JPEG was stored and served from the media path.
1261
+ - Low: an internal path was disclosed in a 500 error.
1262
+
1263
+ About 20 other checks passed, including alg:none JWTs, refresh token used as an access token, path traversal, NoSQL injection, prototype pollution, CORS reflection and SSTI.
1264
+ Where: `docs/pentest-report-2026-04-03.md`.
1265
+
1266
+ ## Operations & Deployment
1267
+
1268
+ ### CLI and scripts
1269
+
1270
+ - **Node 24 required** - `package.json` `engines` says `"node": ">=24"`. `npx domma-cms` (scaffold and `update`) reads the minimum from `engines` and stops on an older Node, naming the version it found. 0.93.0 only warned, because the fleet still ran Node 22; the fleet has since moved (the manager and all nine site processes run Node 24.21.0 under pm2), so the check stops again (unreleased, commit `02dac74`). Before 0.93.0 the CLI checked for Node 18.
1271
+ Where: `bin/cli.js` (`NODE_RANGE`), `package.json` `engines`.
1272
+ - **`npx domma-cms <project-name>`** - Scaffolds a new site in one go: copies server, admin, public, scripts, config and plugins, resets plugin data, writes a default `site.json` (with the SMTP block filled in), `navigation.json`, `plugins.json`, `package.json`, `.env`, and a content-only `.gitignore`. It then runs `npm install`, the setup wizard and the seed, and writes a `.domma` version marker. Options: `--no-install`, `--no-setup`, `--no-seed`, `--help`. Bundled plugins start switched off (the dead "enabled by default" list was removed in 0.93.0).
1273
+ Where: `bin/cli.js`.
1274
+ - **`npx domma-cms update`** - Updates an existing site in place:
1275
+ - Checks the folder is a real, private project.
1276
+ - Compares the `.domma` version with the incoming one.
1277
+ - Backs up to `.domma-backups/<timestamp>/`.
1278
+ - Replaces `server/`, `admin/` and `scripts/`, and only the entries in `public/` that the CMS ships, keeping site-added files and symlinks.
1279
+ - Updates the bundled plugins while keeping their `data/`, `contentPaths` and reset files. A newer copy of a plugin on the site is never downgraded (0.69.1).
1280
+ - Registers new plugins, disabled except the protective ones such as Security.
1281
+ - Adds only new keys to `server.json`, `auth.json`, `content.json` and `presets.json`.
1282
+ - Swaps the CMS dependencies but keeps dependencies you added, self-heals `.gitignore`, updates the marker and runs `npm install`.
1283
+
1284
+ Options: `--yes`, `--no-backup`, `--no-install`, `--dry-run` (preview only), `--force` (reinstall the same version).
1285
+ Where: `bin/update.js`, `bin/lib/config-merge.js`, `bin/lib/plugin-version.js` - Since: 0.6.5.
1286
+ - **`npm run setup`** - The terminal setup wizard:
1287
+ 1. Generates `JWT_SECRET` and `INSTANCE_SECRET`.
1288
+ 2. Creates the first admin if there are no users (password of at least 8 characters).
1289
+ 3. Sets the site title and tagline (and renames the SMTP From name to match).
1290
+ 4. Picks a theme.
1291
+ 5. Sets the server port.
1292
+
1293
+ Where: `scripts/setup.js`.
1294
+ - **`npm run users -- <command>`** - User management from the command line, useful when you are locked out: `list`, `show <email>`, `reset-password <email>`, `set-role <email> <role>`, `activate`, `deactivate`, `create <email>` (with `--password=`, `--generate`, `--name=`, `--role=`) and `delete` (with `--yes`).
1295
+ Where: `scripts/users.js`.
1296
+ - **Other scripts**:
1297
+ - `npm run seed` - demo pages and navigation.
1298
+ - `npm run reset` - factory reset that wipes content and users and resets config.
1299
+ - `npm run fresh` - reset then setup.
1300
+ - `npm run pro enable|disable|status` - the MongoDB storage adapter.
1301
+ - `npm run dev` / `start` / `prod`.
1302
+ - `npm run pm2:start|stop|restart|logs` - using `ecosystem.config.cjs`.
1303
+ - `node scripts/gen-instance-secret.js`.
1304
+
1305
+ ### Fleet management (domma-cms-manager)
1306
+
1307
+ - **Managed sites** - A site forked by the manager gets `MANAGER_URL`, `SITE_SLUG` and a per-fork `FLEET_TOKEN` in its environment. It then:
1308
+ - browses the fleet catalogue under Marketplace > Browse, with prices, licence state and updates;
1309
+ - installs paid plugins with an entitlement issued for that site;
1310
+ - receives pushed notifications (`MANAGER_SECRET`).
1311
+
1312
+ The manager also writes `tools.json` and `plugins.json` directly to set switches and locks. Every call to the manager fails gracefully, so a manager that is down never stops the site serving pages. Unmanaged sites install plugins from a `.dcmsplugin` file instead.
1313
+ Where: `server/services/managerClient.js`, `server/routes/api/plugin-marketplace.js`, `server/middleware/managerAuth.js` - Since: 0.69.0 (Browse and install), 0.88.0 (locks).
1314
+ - **Backups (Site Manager > Backups)** - every managed site is backed up by the `domma-backup` service, a separate pm2 process driving restic (encrypted, de-duplicated, one store for the fleet; a site's own MongoDB database dumped with its own scoped login). The dcm screens:
1315
+ - **Overview** - one row per site, worst first (Failing, No backup yet, Overdue after 26 hours, Paused, Backed up, Unknown), with last good backup, size, next nightly run, restore-test and off-site chips; the service's health line; the fleet-wide **Nightly backups** switch and **Back up all now**. Row menu: back up now, restore, run a restore test, schedule and retention, history, open site. Sidebar badge from `/backups/badge` (sites with an open critical alert).
1316
+ - **A site's slideover** - Backups (facts and every backup kept, with who took it; click one to see what it holds: versions, collections and their store, pages, media, MongoDB), Restore (same site, a new folder under `_restored/`, or a download; whole site or chosen collections, pages or media; a reason of 10+ characters and a confirmation that the site's admins will be told; a safety backup first), Settings (nightly time UTC, restore-test day, daily/weekly/monthly retention, pause writes during the backup), History (the site's hash-chained audit log with an intact/broken check), Jobs.
1317
+ - **Activity** - open alerts, jobs across the fleet, fleet history (nightly switch, off-site, store checks). **Settings** - off-site copy (B2, S3, SFTP, restic REST; write-only credentials), quick and full store checks, the password-custody reminder, what site owners are told.
1318
+ - **Site owners always know** - every backup, failure, restore (before and after, with the admin's name and reason), download, restore test and settings change reaches the site's admins as a notification and the site's permanent backup history.
1319
+ Where: domma-cms-manager `plugins/site-manager/admin/views/panel-backups.js`, `backups-overview.js`, `backups-activity.js`, `backups-settings.js`, `admin/lib/backup-site.js`, pure part `admin/lib/backups.js` (+ test); guide `plugins/site-manager/docs/backups.md` (Documentation > Plugins > Site Manager > Backups); routes `services/backupRoutes.js`; the service `tools/domma-backup/`; plan `dcms-marketplace/docs/backup-pro-plan.md`. Since: manager 2026-09-28.
1320
+ - **Multi-tenant** - Multi-tenancy is not built in. `docs/MultiTenant.md` is a design note that weighs three approaches: one process per tenant, directory-scoped tenants in a single process, and MongoDB-native tenancy. It recommends one process per tenant, which is how the manager fleet runs today, with one CMS process per site behind a proxy.
1321
+
1322
+ ### Networking, email and CORS
1323
+
1324
+ - **`TRUST_PROXY`** - Takes Fastify's own values: `loopback` (recommended behind Apache or the manager, and the default for new sites the manager creates), a list of addresses or CIDR ranges, or a hop count. `1` or `true` trusts every hop, which lets a visitor spoof their IP address.
1325
+ Where: `server/services/network.js` - Since: 0.83.0.
1326
+ - **Reading other sites from a page** - `csp.connectHosts` in `config/server.json` lets public pages fetch from the listed hosts (added to the CSP's `connect-src`, https only); domma-cms.com's Pricing page reads the Pro Tool prices from dcm this way.
1327
+ - **Public asset CORS** - `cors.publicPrefixes` in `config/server.json` adds `Access-Control-Allow-Origin: *` to GET and HEAD requests under the listed paths only, so other sites can import scripts or assets from them.
1328
+ Where: `server/services/publicCors.js`.
1329
+ - **Email service** - A Nodemailer transport built from the SMTP settings in `site.json`, used for form emails, password resets and notices. With no SMTP host set, mail goes to a throwaway Ethereal test inbox, which reports success but delivers to nobody. The CMS makes this visible in Settings, in health checks and with an "Email not set up" notification. New sites ship with an SMTP host, defaulting to a local mail server on port 25 or taken from the environment.
1330
+ Where: `server/services/email.js`, `bin/lib/smtp-defaults.js`, `POST /api/settings/test-email` - Since: 0.55.0.
1331
+
1332
+ ---
1333
+
1334
+ ## Plugins & the Marketplace
1335
+
1336
+ Domma CMS is extended by plugins: self-contained folders under `plugins/<slug>/` that add server routes, admin screens, public pages, shortcodes, themes, roles and more. The **Marketplace** (System › Plugins, `#/plugins`) is where a site installs, switches on and off, updates, configures, exports and removes them. Every Domma plugin, free and paid, is written in one repository (`dcms-marketplace`), published as a signed catalogue, and delivered by one of three paths: fleet install from a site's manager, a single `.dcmsplugin` file for self-hosted customers, or plain export/import between any two sites.
1337
+
1338
+ ### The Marketplace screen
1339
+
1340
+ - **Installed tab** - One row per plugin with version, author, release date (dd.mm.yyyy), a status chip (On / Off / Built in / Not running / Replaced / Needs <x>), licence chip (Licensed, Renew, Support ended, Expired, Unlicensed), Locked chip and "Update to x.y.z" when the catalogue is ahead. Filters: All / On / Off / Needs attention, plus search; anything needing attention sorts first. The filter is remembered per viewer.
1341
+ - Where: `admin/js/views/plugins.js`, `admin/js/lib/plugins-arrange.js`, `GET /api/plugins` · Since: 0.67.0 (renamed to Marketplace), rebuilt as a Tool in 0.76.0
1342
+ - **Row actions** - Click a row for its details and settings; right-click, the ⋮ button or Shift+F10 opens: Details and settings, Switch on/off, Update to vX, View source, Export `.dcmsplugin`, Uninstall. View source is disabled for licensed plugins; Export is disabled for `closedSource` plugins.
1343
+ - Where: `installedItems()` in `plugins.js` · Since: 0.76.0
1344
+ - **Built-in Tools block** - Contacts, Notes, Todo, Analytics and SEO appear at the top with their own switches, listing which plugins require or use each one.
1345
+ - Where: `CORE_TOOLS` in `server/services/tools.js` · Since: 0.88.0 (SEO added 0.90.0)
1346
+ - **Switching on/off restarts the server** - A plugin's routes, hooks and screens register only at boot, so a toggle asks for confirmation, saves, then exits cleanly for the supervisor (fleet manager, pm2) to bring the process back; the screen waits out the restart. Only happens when a supervisor is detected and the flag actually changed; several toggles in a row cost one restart (debounced). Opt out with `restartOnPluginToggle: false` in `config/server.json`.
1347
+ - Where: `PUT /api/plugins/:name`, `scheduleRestart()`/`hasSupervisor()` in `server/routes/api/plugins.js` · Since: 0.41.1
1348
+ - **Dependency cascade confirmation** - If a switch drags others with it (switching off a Tool that a plugin requires; switching on a plugin whose requirement is off), the API answers `409 {needsConfirm: true, plan}` and the screen lists "Switching X off also means: ..." including soft-dependency losses ("Calendar loses: Invite contacts and contact groups to events"). Confirming re-sends with `cascade: true`. A locked switch answers 423.
1349
+ - Where: `planSwitch()` in `server/services/tools.js`, `planLines()` in `plugins-arrange.js` · Since: 0.88.0
1350
+ - **Needs / Works better with** - A plugin's slideover on the Installed tab lists its `requires` as **Needs** (display names, each On, Off or Not installed here) and its `uses` as **Works better with** (with what each adds), with help popovers. The Site Manager shows the same in the Plugin Store slideover, a site's Plugins tab and under each Tool in Sites > Tools.
1351
+ - Where: `dependencyRows()` in `admin/js/lib/plugins-arrange.js` (manager: `admin/lib/tool-deps.js`) · Since: Unreleased
1352
+ - **Requirements switched on at grant** - When the Site Manager grants a plugin, turns Pro on, or re-pushes an enabled plugin, it switches on what the plugin `requires` through the engine's `planSwitch()` (transitively; a locked-off requirement is refused, not overridden). A free Tool the engine ships but the site lacks is installed. A paid requirement the site has no licence for is never granted: the result warns "Cards Pro needs Cards, which this site has no licence for." Idempotent.
1353
+ - Where: manager `plugins/site-manager/services/requirements.js` · Since: manager, 2026-10-13
1354
+ - **Configure (settings form)** - Every plugin with a `config.js` is configurable from its details panel. The form comes from `settingsSchema` in `plugin.json` if present (types `text`, `number`, `select`, `boolean`; `label`, `help`, `min`, `max`, `options`, `secret`), otherwise it is inferred from the effective settings (boolean → switch, number → number box, object → JSON). Only values that differ from `config.js` are saved.
1355
+ - Where: `settingsFields()`/`readSettings()` in `plugins-arrange.js`; stored in `config/plugins.json` · Since: 0.76.0
1356
+ - **Secret masking** - Settings whose key looks secret (`password`, `secret`, `token`, `apiKey`, `private`, `credential`, `...Uri`, `dsn`) or marked `"secret": true` are sent to the browser as a bullet mask; saving an untouched mask keeps the stored value (arrays of secrets included).
1357
+ - Where: `maskSettings()`/`restoreMasked()` in `server/routes/api/plugins.js` · Since: 0.76.0
1358
+ - **Drafts** - A half-filled New plugin form or settings form is kept when you leave and offered back on the next visit.
1359
+ - Where: `DRAFT_KEY` in `plugins.js` · Since: 0.76.0
1360
+ - **Sidebar badge** - The Plugins sidebar entry shows how many are installed; turns red when a licensed plugin that is switched on is not running, amber for a pending update or a licence in grace. Hover for a popover (switched on/off, updates, what needs doing).
1361
+ - Where: `GET /api/plugins/_count`, `pluginsBadge()` · Since: 0.76.0
1362
+ - **Browse tab (managed sites)** - A site started by a fleet manager lists the manager's catalogue: price ("Free", "£12.00/mo", or "Paid"), Licensed/Renew chip, and an action of Install, Update, Installed, "Needs newer CMS" or "Not licensed" / "Licence expired". A plugin whose `minCmsVersion` is newer than the site shows "Needs newer CMS" and "needs Domma CMS X or later" on its card instead of Install or Update (clicking it says both versions; it is left out of Available). Filters: All / Available / Installed / Updates, plus search. "Licensed to <site>" is shown.
1363
+ - Where: `GET /api/plugins/marketplace`, `server/routes/api/plugin-marketplace.js`, `server/services/managerClient.js` · Since: 0.69.0
1364
+ - **Browse tab (self-hosted sites)** - An unmanaged site (no `MANAGER_URL`/`FLEET_TOKEN`) is told it installs from a file: buy a plugin, receive one `.dcmsplugin` file, use Install from file.
1365
+ - Where: `browseState` in `plugins.js` · Since: 0.69.0
1366
+ - **Banner actions** - Install from file (upload icon) and New plugin (plus icon) sit in the Tool banner.
1367
+ - Where: `addBannerAction()` calls in `plugins.js` · Since: 0.68.0 / 0.76.0
1368
+
1369
+ ### Installing, updating and removing
1370
+
1371
+ - **Install from a managed site's catalogue** - The site fetches the bundle from its manager over a fork-time fleet token; the manager mints an entitlement for that site and it travels with the bytes. A manager refusal ("no licence for invoice", "catalogue stale") is shown verbatim; an unreachable manager answers 503. A plugin that needs a newer CMS is refused with the same sentence Install from file shows ("X needs Domma CMS 0.93.0 or later; this site runs 0.92.2. Update the CMS first."); `GET /api/plugins/marketplace` carries it ahead of time as each entry's `needsCms`, with the site's `cmsVersion`.
1372
+ - Where: `POST /api/plugins/marketplace/install {slug, version, update}` · Since: 0.69.0
1373
+ - **Install from file (`.dcmsplugin`)** - Upload one envelope; the slug is read from the manifest inside, never from the filename. Each risky step becomes an explicit question, and the answer goes back as a form flag:
1374
+ - Already installed → `409 {code: 'already-installed', installedVersion, incomingVersion}` → "Update it to x?" (`confirmUpgrade`).
1375
+ - Older than installed → "Refusing to downgrade ... Install it anyway?" (`confirmDowngrade`).
1376
+ - No signature → "Unsigned plugin ... runs on this server with full access. Install anyway?" (`confirmUnsigned`).
1377
+ - A signature that is present but does not verify is always refused - broken means tampered, never "unsigned".
1378
+ - Where: `POST /api/plugins/install-upload`, `installFromFile()` in `plugins.js` · Since: 0.67.0 (update offer 0.68.0)
1379
+ - **Safe upgrade with rollback** - The new version is extracted to `plugins/.<slug>.incoming` and validated before the running one is touched; `data/` and `.entitlement.json` are carried across; the old folder is renamed to `.<slug>.backup` and restored if the swap fails. "The one state this must never produce is neither version installed."
1380
+ - Where: `upgradePlugin()` in `server/services/pluginInstaller.js` · Since: 0.68.0
1381
+ - **`minCmsVersion` enforced** - Installing or updating a plugin whose manifest names a newer `minCmsVersion` than the site runs is refused with both numbers ("Contacts Pro needs Domma CMS 0.93.0 or later; this site runs 0.92.1. Update the CMS first."), and an update leaves the installed version in place. A site whose CMS version cannot be read is not locked out.
1382
+ - Where: `cmsVersionShortfall()` in `server/services/pluginInstaller.js` (install and upgrade) · Since: 0.93.0
1383
+ - **No `onEnable` at install** - A plugin is installed switched off, so its `onEnable` runs once, when it is enabled, with `{fastify, services, hooks}`. Before 0.93.0 install also called it with nothing passed in, so it ran twice and a plugin that read its argument failed at install.
1384
+ - Since: 0.93.0
1385
+ - **Downgrade protection** - Version comparison is numeric (1.10.0 > 1.2.0); an older version is refused unless explicitly confirmed.
1386
+ - Where: `compareVersions()` in `pluginInstaller.js` · Since: 0.68.0
1387
+ - **Signed-only sites** - `requireSignedPlugins: true` in `config/server.json` refuses every unsigned bundle, even with confirmation. Default is `false` because the CMS's own scaffolder produces unsigned plugins; fleet sites should set it.
1388
+ - Where: `requireSignedPlugins()` in `pluginInstaller.js` · Since: 0.67.0
1389
+ - **Install safety** - Slug pattern check, path-traversal guard during extraction, symlinks and hard links ignored, manifest validated after extraction (partial installs cleaned up), new plugins registered disabled in `config/plugins.json`, every install/upgrade/uninstall appended to `content/instances_log.json` (unsigned ones recorded as `install:unsigned` / `upgrade:unsigned`).
1390
+ - Where: `installPlugin()`/`extractTar()` in `pluginInstaller.js`
1391
+ - **Uninstall** - Runs the plugin's `onDisable({uninstall: true})`, removes every role the plugin still owns (their users move to `user`), deletes `plugins/<slug>/` including its `data/`, removes its `plugins.json` entry, logs it. Restart to finish.
1392
+ - Where: `DELETE /api/plugins/marketplace/:slug`, `uninstallPlugin()` · Since: 0.71.0 (role clean-up)
1393
+ - **Export** - Any non-licensed plugin downloads as `<slug>-<version>.dcmsplugin`. `data/`, `backups/`, `node_modules/`, `CLAUDE.md` never travel; `?includeData=1` opts `data/` back in. Exports are deterministic (sorted, fixed mtime) so two exports of the same plugin compare equal. Licensed plugins answer 403. Needs `plugins.develop`.
1394
+ - Where: `GET /api/plugins/:name/export`, `tarDirectory()` in `pluginEnvelope.js` · Since: 0.67.0 (corrupt-download fix 0.69.0)
1395
+ - **Site git tracking** - A plugin scaffolded in the admin is allow-listed in the site's `.gitignore` so it is committed with the site; distributed plugins stay ignored, but folders a plugin declares in `contentPaths` (e.g. `["data"]`) are tracked so user-authored work is not lost. Reserved slugs count as upstream.
1396
+ - Where: `server/services/siteGitignore.js`, `trackPluginContent()` · Since: 0.69.2 (reserved-slug rule 0.69.4)
1397
+
1398
+ ### Tiers, licences and entitlements
1399
+
1400
+ - **Tiers** - Each marketplace manifest carries `tier: "free" | "paid"`. Free plugins also ship inside the CMS; paid ones are `closedSource: true` and need an entitlement to run. The CMS itself never reads `tier` - it drives the catalogue build, free-tier sync and the Browse price chip.
1401
+ - Where: `dcms-marketplace/plugins/*/plugin.json`, `scripts/verify.js`
1402
+ - **Current register (marketplace, 24 plugins)** - versions in the [catalogue table](#marketplace-catalogue).
1403
+ - Free (6, shipped with the engine): `blog`, `booking`, `feedback`, `mail-reader`, `security`, `shopping-cart`.
1404
+ - Paid (21): `analytics-pro`, `blog-pro`, `booking-pro`, `calendar`, `contacts-pro`, `data-transfer`, `docs`, `docs-pro`, `email-pro`, `garage`, `invoice`, `job-board`, `messages`, `mongodb-adapter`, `security-pro`, `seo-pro`, `shopping-cart-pro`, `social-media-pro`, `surveys`, `theme-roller`, `waypoint-pro`.
1405
+ - **Free-and-Pro pairs** - A Pro edition declares `supersedes` on its free sibling, so only one runs: Blog Pro → Blog, Email Pro → Mail Reader, Security Pro → Security, Contacts Pro → Contacts (built-in Tool takeover). Shopping Cart Pro and SEO Pro instead `requires` their free base (`shopping-cart`, the `seo` Tool).
1406
+ - **MongoDB-dependent plugins** - Email Pro ("Requires MongoDB" in its description), Data Transfer (moves collections between file and MongoDB storage) and MongoDB Adapter (managed-only: reads a `config/connections.json` the manager writes during provisioning). There is no manifest field for a Mongo requirement - it is stated only in the description text. Core already supports Mongo storage for free via `storage: {adapter: "mongodb"}` in a schema.
1407
+ - Where: design spec "Managed-only plugins"
1408
+ - **Entitlements (licences)** - A signed JSON document saying who may run which paid plugin. Two commercial models:
1409
+ - `rental` (managed sites, monthly): valid until `expiresAt`, then `grace` for `gracePeriodDays` (default 7), then `expired` - stops loading.
1410
+ - `perpetual` (self-hosted, bought outright): never stops. After `supportExpiresAt` it is `support-ended` - still runs, updates lapse.
1411
+ - Absent, wrong slug, unknown key or bad signature → `unlicensed` - does not load.
1412
+ - Where: `server/services/pluginEntitlement.js` · Since: 0.68.0
1413
+ - **Only our paid plugins are ever checked** - The gate keys on `closedSource: true`; a plugin you wrote yourself is never asked for an entitlement.
1414
+ - **Where entitlements live** - `plugins/<slug>/.entitlement.json` (arrives in the envelope, survives upgrades), or instance-wide in `config/entitlements/*.json` (e.g. an owner wildcard `slug: "*"` covering every plugin, used by the manager and cms.dommajs.org). Per-plugin wins.
1415
+ - **Offline, no phone-home** - Verification is Ed25519 over canonical JSON (keys sorted, no whitespace) against keys compiled into the CMS release.
1416
+ - **Lapses are never written to config** - An unentitled plugin is simply not loaded this boot; renewing (dropping in a new entitlement) and restarting restores it. Entitlement is settled before supersession, so a lapsed Email Pro does not also take Mail Reader away.
1417
+ - Where: `registerPlugins()` in `server/services/plugins.js`
1418
+ - **Licence line and expiry chip on the plugin banner** - Every plugin screen's banner shows version, release date and licence ("Commercial licence", "Included with Domma CMS" for the plugins the engine ships, "Your own Tool" for one the site made, "Built in", or the manifest's own `licence`); a paid plugin adds "Expires <date>" (rental) or "Support to <date>" (perpetual), amber inside 30 days, red once lapsed.
1419
+ - Where: `pluginLicence()`/`pluginExpiry()` in `plugins.js`, `admin/js/lib/plugin-chrome.js` · Since: 0.69.0
1420
+ - **Closed-source guards** - `closedSource: true` makes the code editor refuse the plugin (403) except for a super-admin (level 0 - on the fleet, only the manager), hides View source, and blocks export.
1421
+ - Where: `pluginFiles.js`, `mayEditLicensed` in `routes/api/plugins.js` · Since: 0.64.2 (source guard), 0.67.0 (export)
1422
+
1423
+ ### The `.dcmsplugin` envelope and signing
1424
+
1425
+ - **One file format for every delivery path** - A gzipped tar with three root members: `plugin.tar.gz` (the plugin folder, same bytes as the catalogue bundle), optional `plugin.sig` (detached Ed25519 signature, base64) and optional `entitlement.json` (present on a paid download). Anything else is refused as "Not a .dcmsplugin file".
1426
+ - Where: `server/services/pluginEnvelope.js` · Since: 0.67.0
1427
+ - **Signing keys** - The CMS trusts only keys compiled into its release (`TRUSTED_KEYS`, currently `marketplace-1`); a bundle naming any other `keyId` is refused before its signature is checked. The private key is held offline and never on the manager host; the old `manager-2026` key was removed because its private half could not be accounted for.
1428
+ - Where: `pluginInstaller.js`, `dcms-marketplace/keys/marketplace-1.pub`
1429
+ - **Exports are unsigned by design** - The marketplace key never sits on a customer host, so an export from a running CMS cannot be signed; the importing site decides (confirm, or refuse via `requireSignedPlugins`).
1430
+
1431
+ ### The catalogue (dcms-marketplace)
1432
+
1433
+ - **One repository for every plugin** - `dcms-marketplace/plugins/<slug>/`, one slug each, with history. `_lib` and `_template` deliberately stay in domma-cms so `npm install domma-cms` is a complete, offline plugin-development kit.
1434
+ - **Signed, reproducible catalogue build** - `make catalogue KEY=/path/to/marketplace-1.pem` writes `dist/catalogue.json`, `dist/catalogue.json.sig` (signature over canonical bytes), and per plugin `bundles/<slug>-<version>.tar.gz` + `.sig`. Builds are byte-reproducible (sorted tar, fixed mtime, `gzip -n`). The script verifies each signature against its own key before emitting.
1435
+ - Where: `scripts/build-catalogue.js`, `Makefile`
1436
+ - **Validated before anything is written** - A reserved slug, a manifest `name` that disagrees with its folder, or a missing `tier` aborts with `dist/` untouched.
1437
+ - **What never ships** - `data/`, `backups/`, `node_modules/`, `CLAUDE.md`, `.DS_Store`.
1438
+ - **Catalogue entries carry artifact facts only** - slug, version, displayName, description, tier, icon, supersedes, closedSource, bundle path, bytes (plus `npmDependencies`, `minCmsVersion` and the manifest's Tool dependencies `requires` / `uses` when declared - `npmDependencies` is carried under its own name since the fix in dcms-marketplace `5772505`; it used to be written as `requires`, clashing with the manifest's Tool/plugin `requires`). Prices live separately (`terms.json` / Site Manager's Plugin Terms screen) so a price change is never a release.
1439
+ - **Repo invariants** - `make verify` checks required manifest fields, a declared tier, no reserved-slug folders, paid ⇒ `closedSource` (or a stated reason), and no committed runtime state.
1440
+ - Where: `scripts/verify.js`
1441
+ - **Plugin tests run against the real CMS** - `make test` symlinks a sibling `domma-cms` checkout (`cms`, `node_modules`, `server`, `public`, `plugins/_lib`) and runs each `plugins/<slug>/tests/*.test.js` with the CMS as working directory.
1442
+ - **Issuing entitlements** - `scripts/issue-entitlement.js --key ... --slug <slug|*> --model rental|perpetual [--rental-days n | --support-months n | --no-support] --purchaser-name ...` writes a signed entitlement (licence key `DOMMA-XXXX-XXXX-XXXX` generated if not given, grace 7 days default). It verifies what it signed before writing; run only where the private key lives.
1443
+ - Where: `scripts/issue-entitlement.js`, `scripts/entitlement.js` (signing core; `canonicalise()` is copied verbatim into the CMS verifier)
1444
+ - **Free-tier sync** - `make sync-free-tier` copies the free plugins byte-for-byte into `domma-cms/plugins/` and records per-file SHA-256 hashes in `domma-cms/plugins/free-tier.lock.json`. It refuses (copying nothing) if the CMS copy has uncommitted changes or was edited since the last sync; `CHECK=1` reports drift, `ADOPT=1` is first-sync only. The engine build ships those folders untouched, so a Blog updated by the engine is the same bytes as the catalogue's.
1445
+ - Where: `scripts/sync-free-tier.js`, `domma-cms/scripts/build.js` (`FREE_TIER`) · Since: 0.69.1
1446
+ - **Reserved slugs** - `reserved.json` holds names that must never be published: `analytics`, `contacts`, `notes`, `todo`, `demo-viewer`, `theme-switcher`, `seo` (promoted into core), `form-builder`, `site-search` (retired). Prevents a third party wearing a built-in's name.
1447
+ - **Provenance** - `PROVENANCE.md` records where every plugin was imported from on 2026-09-22 (verified by per-file hashing across all site copies), including the stale `paid-plugins/surveys` fork that would have silently rolled Surveys back three months. Each import is a single verbatim commit; `tier`/`closedSource` were applied in a separate commit.
1448
+ - **Submitting a plugin** - Per the design, a third-party author builds with the free builder, exports a `.dcmsplugin` and sends it; Domma reviews, signs and adds it. Author terms (revenue share, paid third-party plugins) are an open question.
1449
+
1450
+ ### Plugins that became core (promotion history)
1451
+
1452
+ - **Promoted-plugin migration** - On start-up, a plugin now built into the CMS has its `plugins.json` entry removed and its folder deleted (only if the manifest's `author` matches ours - someone else's folder of the same name is left alone). Tuned settings are reported or lifted to their new home; Analytics' counters are copied to `content/analytics/` and verified byte-for-byte before the folder goes.
1453
+ - Where: `server/services/promoted-plugins-migration.js` · Since: 0.67.0
1454
+ - **Timeline** - `site-search` → core search (0.64.2); `analytics`, `contacts`, `notes`, `todo` → built-in Tools, `demo-viewer` → `[demo]` shortcode, `theme-switcher` → theme setting (0.67.0); `seo` → built-in SEO Tool (0.90.0, via `RETIRED_PLUGINS`: the old folder is skipped and its data moves to `content/seo/` and `config/seo.json`). `invoice` and `surveys` moved out of the engine to the Marketplace as paid plugins (0.67.0).
1455
+ - **Engine updates never downgrade plugins** - The updater replaces each bundled plugin with the engine's copy unless the site's copy is newer (e.g. installed from the catalogue), in which case it is kept and logged.
1456
+ - Where: `bin/lib/plugin-version.js` (`shouldReplacePlugin()`), `bin/update.js` · Since: 0.69.1
1457
+
1458
+ ### How a plugin loads (lifecycle)
1459
+
1460
+ - **Discovery** - Every folder in `plugins/` not starting with `_` and not retired, with a valid `plugin.json` holding the seven required fields plus `plugin.js` and `config.js`. A failure is recorded (and raises a notification), never crashes the server.
1461
+ - **Boot order** - (1) classify entitlements of `closedSource` plugins; (2) work out which enabled plugins would load; (3) resolve `supersedes`; (4) block any whose `requires` is not running (`blockedBy`); (5) import `plugin.js` and register it under `/api/plugins/<slug>/`; (6) apply built-in Tool takeovers; (7) register `admin.sidebar` entries; (8) create collections from `collections/*/schema.json` if missing; (9) register permissions and roles; (10) detect `plugin.public.js` for root-level public routes.
1462
+ - Where: `registerPlugins()` in `server/services/plugins.js`
1463
+ - **Enable/disable hooks** - Switching on runs `setupPlugin()` (creates missing collections, forms from `forms/*.json`, pages from `pages/**/*.md`, roles) then the plugin's exported `onEnable({fastify, services, hooks})`. Switching off removes the plugin's pages and forms (collections and roles are kept), unregisters its permissions, then calls `onDisable(...)`. `services` holds collections, forms, blocks, content, config, roles, users, actions, views.
1464
+ - **Supersession** - `"supersedes": ["other"]` makes the other plugin register nothing while this one runs; computed every boot, never written to config, so the superseded plugin returns if the replacement stops. Naming a built-in Tool (`notes`, `todo`, `analytics`, `contacts`, `seo`) takes it over: its sidebar entry is hidden and `#/<tool>` goes to the plugin's first admin route, while the Tool's API stays up.
1465
+ - Since: 0.60.1 (supersedes), 0.72.0 (Tool takeover)
1466
+ - **Dependencies** - `requires: ["contacts"]` = cannot run without it; `uses: {"contacts": "Invite a contact group"}` = loses that feature without it. Either can name a built-in Tool or another plugin. Check `uses` at request time with `isToolEnabled(name)` or `GET /api/tools/enabled`.
1467
+ - Since: 0.88.0
1468
+ - **Locks** - `locked: true` on a `plugins.json` or `tools.json` entry means only the site's manager can change that switch.
1469
+ - Since: 0.88.0
1470
+ - **Plugin API scope** - A user whose roles confine them to some admin screens (`role.admin.allow`) can call a plugin's authenticated routes only if one of that plugin's screens is in their scope; others get 403 "Your account cannot use <plugin>".
1471
+ - Where: `server/services/pluginScope.js` · Since: 0.86.0
1472
+ - **Static files** - Only `plugins/<slug>/admin/**` and `plugins/<slug>/public/**` are served (at `/plugins/<slug>/...`); `plugin.js`, `config.js`, `data/` are never reachable.
1473
+ - **Cache-busting** - Admin view entries and `admin.css` are stamped with the newest mtime of the plugin's `admin/` tree and `_lib/admin`, so an update reaches the browser without a hard refresh.
1474
+
1475
+ ## Building Plugins
1476
+
1477
+ ### Plugin anatomy
1478
+
1479
+ ```
1480
+ plugins/my-plugin/
1481
+ plugin.json manifest (required)
1482
+ plugin.js default export: async Fastify plugin (required)
1483
+ config.js default export: settings defaults (required)
1484
+ plugin.public.js optional: root-level public routes (/shop, /blog/...)
1485
+ admin/views|templates|css|lib/ admin SPA screens (served)
1486
+ public/ browser assets and inject snippets (served)
1487
+ collections/<slug>/schema.json created on first enable
1488
+ forms/*.json pages/**/*.md created on enable, removed on disable
1489
+ roles/*.json plugin roles, created once
1490
+ data/ runtime state (never exported or shipped)
1491
+ tests/ node:test files (run by `make test` in the marketplace)
1492
+ docs/*.md user guides for Documentation > Plugins (optional frontmatter title, order)
1493
+ CLAUDE.md working notes (never shipped)
1494
+ ```
1495
+
1496
+ - **Five silent mistakes to avoid** (from `plugins/CLAUDE.md`): collection schemas use `title` and field `name` (not `label`/`key`); read route params from `location.hash`, not `R.current`; use Domma class names (`form-input`, `flex`, `justify-between`), not Bootstrap; `F.create(schema, data, options).renderTo(el)`; read responses as `res.data ?? (Array.isArray(res) ? res : [])`. Also: always `[authenticate, requireAdmin]`, never `requireAdmin` alone; don't use `type: 'date'` in `F.create` (use `E.datePicker`).
1497
+ - **Don't draw your own banner** - The admin wraps every plugin view in `.dm-plugin-shell` with name, version, licence and expiry. Put screen-level controls (the Settings cog especially) into it with `addBannerAction(container, {icon, title, onClick, id})`, imported dynamically so an older admin does not break your view.
1498
+ - Where: `admin/js/lib/plugin-chrome.js`
1499
+
1500
+ ### Manifest (`plugin.json`) fields
1501
+
1502
+ | Field | Purpose |
1503
+ |---|---|
1504
+ | `name`, `displayName`, `version`, `description`, `author`, `date`, `icon` | Required. `name` must equal the folder name. |
1505
+ | `admin.sidebar[]` | Sidebar entries `{text, url, icon, folder?, group?, permission?, visibility?, countUrl?, items?, section?}`. `folder` = default placement (`tools` unless `overview`, `projects`, `content`, `data`, `system`, `documentation`). `group` = an optional sub-folder of it (`Core` -> Tools > Core). |
1506
+ | `admin.routes[]`, `admin.views{}` | SPA routes and view modules `{entry, exportName}`. |
1507
+ | `admin.css[]` | Admin stylesheets (may include `../_lib/admin/ui/help.css`). |
1508
+ | `inject.head`, `inject.headLate`, `inject.bodyEnd` | HTML snippets added to every public page (paths confined to the plugin folder). |
1509
+ | `permissions[]` | Permission resources `{key, label, actions?, group?, icon?, grant?: ["admin"]}` - `grant` given once. |
1510
+ | `settingsSchema[]` | Configure form: `{key, label, type: text|number|select|boolean, options?, min?, max?, help?, secret?}`. |
1511
+ | `requires[]` / `uses{}` | Hard / soft dependencies on Tools or plugins (0.88.0). |
1512
+ | `supersedes[]` | Plugins or built-in Tools this one replaces. |
1513
+ | `tier` | `free` / `paid` (marketplace only). |
1514
+ | `closedSource` | Licensed plugin: entitlement required, source editor and export refused. |
1515
+ | `licence` | Override the licence line on the banner. |
1516
+ | `contentPaths[]` | Folders holding user-authored work, tracked in the site's git (0.69.2). |
1517
+ | `scaffold.reset[]` | Files `{path, content}` reset by `npm run reset`; the updater also preserves them. |
1518
+ | `minCmsVersion` | The oldest Domma CMS the plugin runs on; install and update refuse a newer requirement (0.93.0). Carried into the catalogue. |
1519
+ | `npmDependencies{}` | npm packages the plugin needs; carried into the catalogue as `npmDependencies`. Not checked by the CMS at install. |
1520
+ | `docs.permission` | Who may read the plugin's user guides (`docs/*.md`, shown under Documentation > Plugins); defaults to the permission on its first `admin.sidebar` entry that names one, else anyone signed in to the admin (unreleased, after 0.93.0). |
1521
+
1522
+ ```json
1523
+ {
1524
+ "name": "job-desk", "displayName": "Job Desk", "version": "1.0.0",
1525
+ "description": "Vacancies and applications.", "author": "You", "date": "2026-09-27", "icon": "briefcase",
1526
+ "requires": ["contacts"],
1527
+ "uses": {"calendar": "Interview slots on the calendar"},
1528
+ "permissions": [{"key": "vacancies", "label": "Vacancies", "grant": ["admin"]}],
1529
+ "settingsSchema": [{"key": "apiKey", "label": "Feed API key", "type": "text", "secret": true}],
1530
+ "admin": {
1531
+ "css": ["admin/css/index.css"],
1532
+ "sidebar": [{"text": "Job Desk", "url": "#/plugins/job-desk", "icon": "briefcase",
1533
+ "folder": "data", "permission": "vacancies", "countUrl": "/api/plugins/job-desk/_count"}],
1534
+ "routes": [{"path": "/plugins/job-desk", "view": "plugin-job-desk"}],
1535
+ "views": {"plugin-job-desk": {"entry": "job-desk/admin/views/index.js", "exportName": "indexView"}}
1536
+ }
1537
+ }
1538
+ ```
1539
+
1540
+ ### Server entry, auth helpers and roles
1541
+
1542
+ - **`plugin.js`** - `export default async function (fastify, options)`; routes are prefixed `/api/plugins/<slug>/`. `options` carries `auth`, `hooks`, `settings` (merged `config.js` + saved overrides).
1543
+ - **`options.auth`** - `authenticate` (Bearer JWT, plus the admin-scope rule), `requireAdmin`, `requirePermission(resource, action?)`, `requireRole(roles)`, `requireVisibility(visibility)` (supports `=role`).
1544
+ - **Roles** - `roles/*.json` `{name, label, level ≥ 1, permissions, badgeClass?, resources?, admin?: {home, allow}}`; created once, then owned by the admin (edits survive). `admin` confines holders to the plugin's screens. Disabling keeps roles; uninstall removes them.
1545
+ - Since: 0.71.0
1546
+ - **`plugin.public.js`** - Registered at the site root (before the catch-all) with `settings`, `auth` (authenticate/requireAdmin/requireVisibility), `config` and notification hooks.
1547
+ - **Sidebar badges** - `countUrl` on a sidebar entry answering `{count, label, tone, details}`; dispatch `dm:sidebar-count` to update it live.
1548
+ - Since: 0.65.0
1549
+ - **Settings** - Read with `options.settings` at registration or `getPluginSettings(name)` at request time (required for secrets, so a new value works without restart); write with `savePluginState(name, {settings})`.
1550
+
1551
+ ### Hooks available to plugins
1552
+
1553
+ Handed to `plugin.js` as `options.hooks` (bound to the plugin's name where it matters). `onEnable`/`onDisable` receive `{fastify, services, hooks}`, where `hooks` holds only the role and notification hooks; `plugin.public.js` only the notification hooks.
1554
+
1555
+ | Hook | Purpose |
1556
+ |---|---|
1557
+ | `on(event, fn)` | Subscribe to the event bus (see events below). |
1558
+ | `registerShortcode(name, handler, {priority})` | Add a `[name]` shortcode; handler `(attrStr, body, context) => html`, synchronous. |
1559
+ | `registerSanitizeRules({tags, attributes})` | Let custom tags/attributes through the HTML sanitiser. |
1560
+ | `registerTransform(hookName, fn, {priority})` | Modify values at `markdown:beforeParse`, `markdown:afterParse`, `render:beforeRender`, `render:afterRender`, `seo:meta`, `sitemap:entries`, `robots:lines`. |
1561
+ | `registerMenuLocation(slot, {label, description, maxDepth})` | Add a menu slot to Menus › Locations and `[menu location=...]`. |
1562
+ | `registerSidebarItem({folder?, group?, parent?, item})` | Add an admin sidebar Tool at runtime (not persisted; admins can move it). `group` puts it in a sub-folder of `folder`. |
1563
+ | `registerThemes(provider)` | Offer custom themes `{id, label, baseTheme, tokens}` everywhere a theme is chosen (0.73.0). |
1564
+ | `registerRole(def)` / `unregisterRole(name)` | Create/remove roles from code - only the plugin's own (0.71.0). |
1565
+ | `registerCalendarSource({id, label, icon, tone, list})` | Dated items shown on the Calendar plugin, per viewer and range (0.79.0). |
1566
+ | `getCalendarSources()` | Read registered calendar sources (used by the Calendar plugin). |
1567
+ | `registerRelatedItems(kind, provider)` | Show items beside a core contact or note, with menu actions (0.79.0). Kind `message` (0.103.0): actions on an email's menu in the mail reader, `{email}`/`{name}`/`{subject}` in the url; any action can carry `permission: {resource, action}`. |
1568
+ | `registerSitemapSource({id, label, list})` / `sitemapChanged()` | Add public URLs to `/sitemap.xml`; refresh it on change (0.89.0). |
1569
+ | `notify({...})` / `resolveNotification(key)` | Raise or clear an admin notification (source ids `<plugin>:<id>`) (0.80.0). |
1570
+ | `registerNotificationSource({id, label, severity, defaultOn, every, atBoot, check, audience})` | A switchable source, optionally scheduled (`boot`, `15m`, `hourly`, `daily`) (0.80.0). |
1571
+ | `registerLoginCheck({id, phase: before|after, check, verify?})` | Lockouts (before) or two-factor challenges (after) at sign-in (0.83.0). |
1572
+ | `registerPasswordCheck({id, check})` | Password policy wherever a password is set (0.83.0). |
1573
+ | `registerComponent({name, source})` | Add a Domma component from a `.dmc` source, used on pages as `<dm-name>` or `[component]` and listed read-only in Components; refreshes the page sanitiser's component tag list (0.93.0). |
1574
+
1575
+ Also exported from `server/services/hooks.js` but **not** handed over in `options.hooks`: `hideSidebarUrl(url)`, `unregisterCalendarSource`, `unregisterRelatedItems`, `unregisterSitemapSource`.
1576
+
1577
+ **Events on the bus:** `content:pageCreated`, `content:pageUpdated`, `content:pageDeleted`, `content:pageRenamed`, `content:pageViewed`, `content:mediaUploaded`, `content:mediaDeleted`, `collection:entryCreated`, `collection:entryUpdated`, `collection:entryDeleted`, `form:submitted`, `user:loggedIn`, `user:deleted`, `contact:deleted`, `note:deleted`, `auth:loginSucceeded`, `auth:loginFailed`, `auth:loggedOut`, `auth:rateLimited`, `auth:passwordChanged`, `auth:resetRequested`, `auth:resetIssued`, and `audit` (every successful POST/PUT/PATCH/DELETE under `/api/` by a signed-in user - `{actor, action, route, target, fields, status, ip}`; core stores nothing, Security Pro keeps the trail).
1578
+
1579
+ ```js
1580
+ export default async function plugin(fastify, options) {
1581
+ const {authenticate, requirePermission} = options.auth;
1582
+ const {registerShortcode, registerSitemapSource, notify, on} = options.hooks;
1583
+
1584
+ registerShortcode('vacancy-count', () => `<span class="vc">${count}</span>`);
1585
+ registerSitemapSource({id: 'vacancies', list: async () => [{urlPath: '/jobs'}]});
1586
+ on('form:submitted', ({slug}) => slug === 'apply' && notify({title: 'New application', severity: 'info'}));
1587
+
1588
+ fastify.get('/vacancies', {preHandler: [authenticate, requirePermission('vacancies', 'read')]}, async () => []);
1589
+ }
1590
+
1591
+ export async function onDisable({hooks, uninstall}) {
1592
+ if (uninstall) await hooks.unregisterRole('candidate');
1593
+ }
1594
+ ```
1595
+
1596
+ ### Shared helper library (`plugins/_lib`)
1597
+
1598
+ Not a plugin (skipped by discovery); ships in the npm package so plugins built against it work once installed (since 0.61.0).
1599
+
1600
+ - **`dataStore.js`** - `createStore(absPath)` → `read`, `write` (atomic via `.tmp` + rename), `readArray`, `appendItem` (uuid id), `updateItem`, `deleteItem`. For plugin-local JSON that does not need the collections API; single-process safe only.
1601
+ - **`schemaSync.js`** - `syncPluginSchemas(collectionsDir, {getCollection, updateCollection}, log)` adds fields a plugin's shipped schema has gained to already-installed collections (additive only, idempotent). Fixes "new field reaches fresh installs only".
1602
+ - **`admin/ui/help.js` + `help.css`** - Declarative inline help: `data-help="..."` on any element, then `scanHelp(container)`; also `helpTip`, `helpIcon`, `openHelpPanel({title, intro, sections})` for longer help.
1603
+ - **`admin/ui/dates.js`** - `toDate`, `fmtDate`, `fmtTime`, `fmtDateTime` (dd/mm/yyyy, 24-hour; `{utc: true}` option), usable in the browser and on the server, so plugins read dates the way core does. Needs core 0.93.0.
1604
+ - **`admin/ui/resizable.js`** - `makeResizable(slideover, {storageKey, defaultWidth, handleClass})`: drag handle on a slideover's leading edge, width remembered per panel (defers to the admin's central handle when present).
1605
+ - **`mail/` + `admin/mail/`** - The complete IMAP/SMTP mail core shared by Mail Reader (free) and Email Pro (paid): accounts with AES-256-GCM sealed passwords or a Microsoft/Google OAuth2 sign-in (`oauth.js`, `oauthRoutes.js`, `admin/mail/oauth-panel.js`; routes registered by Email Pro only), connection pool, sync, rules, send-later, snooze, templates, vacation replies, mbox export, three-pane reader, compose window, and `section-kit.js` (small DOM builders: `box`, `heading`, `note`, `formIn`, `itemCard`, `button`, `actionBar`, `richEditor`, `select`, `empty`). Mail Reader's `plugin.js` is 48 lines over it.
1606
+ - **Admin libraries plugins can import at run time** (core, under `/admin/js/lib/`): `plugin-chrome.js` (`addBannerAction`), `tool-kit.js` (`onceClosed`, `openMenuAt`, `settingsStore`, `viewLifetime`, `sidebarCount`), `help-popover.js` (`helpPopovers`), `server-restart.js`. Import dynamically and survive their absence - `admin/` is replaced wholesale by the updater.
1607
+
1608
+ ### Scaffolder and in-admin code editor
1609
+
1610
+ - **New plugin (admin)** - Marketplace banner › New plugin: display name, slug (made from the name as you type), description, author, icon, validated live, with a draft kept. Instantiates `plugins/_template/` (manifest, `plugin.js`, `config.js`, wired admin view, template, CSS, `CLAUDE.md`) with tokens `PLUGIN_SLUG`, `PLUGIN_DISPLAY_NAME`, `PLUGIN_DESCRIPTION`, `PLUGIN_AUTHOR`, `PLUGIN_DATE` replaced. Created pre-enabled; restart to activate. Free text may not contain `<`, `>`, `"`, backslash or `*/`.
1611
+ - Where: `POST /api/plugins/scaffold` (`plugins.create`), `server/services/pluginScaffold.js` · Since: 0.36.16
1612
+ - **New plugin (CLI)** - `npm run create-plugin <slug> [--author "Name"] [--desc "..."]`, registered disabled.
1613
+ - Where: `scripts/create-plugin.js`
1614
+ - **Plugin code editor** - Marketplace › View source (`#/plugins/code/<name>`): files grouped by folder with filter and unsaved markers; syntax-highlighted editor; right-click to save, revert, copy path, new file beside it, duplicate, rename, delete; Ctrl+S / Ctrl+Shift+S (save all); a Server file / Admin file chip says whether saving needs a restart, and a bar offers Restart server; unsaved files are kept as a per-plugin draft (flagging files changed on the server meanwhile).
1615
+ - Where: `admin/js/views/plugin-code.js`, `admin/js/lib/plugin-code-arrange.js` · Since: 0.36.17, rebuilt as a Tool 0.77.0
1616
+ - **Editor guard rails** - Needs `plugins.develop` (separate from `plugins.update`, because editing source is code execution); confined to `plugins/<name>/` after symlink resolution; editable types `.js .json .md .html .css .txt .svg`, 512 KB max; `.json` must parse and `plugin.json` must keep its `name`; `plugin.json`, `plugin.js`, `config.js` cannot be deleted; licensed plugins refused except for level-0 super-admins.
1617
+ - Where: `server/services/pluginFiles.js`, `GET/PUT/DELETE /api/plugins/:name/file(s)`, `POST /api/plugins/restart`
1618
+
1619
+ ### Permissions for plugin management
1620
+
1621
+ | Permission | Allows |
1622
+ |---|---|
1623
+ | `plugins.read` | See the Marketplace list and badge |
1624
+ | `plugins.create` | Scaffold a plugin; install from file |
1625
+ | `plugins.update` | Switch on/off, save settings |
1626
+ | `plugins.develop` | Code editor, export, restart server |
1627
+ | admin role (`requireAdmin`) | Managed-site catalogue: browse, install/update, uninstall |
1628
+
1629
+ ### Security plan (Security / Security Pro) - headline points
1630
+
1631
+ From `dcms-marketplace/docs/security-plan.md` (2026-09-26; all phases built and on the testbed, fleet roll-out pending):
1632
+
1633
+ - Two plugins on the Blog / Blog Pro pattern: **Security** (free, ships with the engine) and **Security Pro** (paid, `supersedes: ["security"]`). "Free = see where a site is weak and block the obvious. Pro = defend actively, and defend the fleet."
1634
+ - Core groundwork (0.83.0): correct proxy trust (`TRUST_PROXY=loopback`), loopback rate-limit exemption only for genuine local calls, persistent token revocation, auth events, `registerLoginCheck`/`registerPasswordCheck`, and an `audit` event for every change.
1635
+ - Security (free): health check with score and sidebar badge, progressive sign-in lockout, password policy (length, not name/email, top-10k list), 30-day sign-in log, probe-path blocking and IP blocks, emergency CLI unlock (`node plugins/security/bin/unlock.js --all`).
1636
+ - Security Pro: TOTP two-factor with recovery codes and per-role enforcement, firewall with escalating bans and country rules, session management, opt-in breached-password check (k-anonymity), then audit trail, file-integrity checks against the npm package and signed catalogue bundles, weekly summary email, and a fleet view with a shared block list.
1637
+
1638
+ ---
1639
+
1640
+ ## Marketplace Plugins
1641
+
1642
+ ### Analytics Pro (`analytics-pro`)
1643
+
1644
+ Tier: paid (closed source) · Version: 2.3.2 · Requires: no MongoDB (hits kept in `data/hits.jsonl`); no `minCmsVersion` declared; no `requires` · Uses: none declared · Permission: `analytics-pro` (View), granted to `admin` (2.3.0)
1645
+
1646
+ Cookieless visitor analytics built into the site itself. Every page view is recorded server-side (no tracking script, no cookie banner needed for it), grouped into visits, and broken down by where people came from, where they are, what they used and which pages they read in what order - with a live view, your own filters, several display styles, CSV/JSON export and a printable report. Location lookups run on the server against a downloaded DB-IP Lite database, so nothing is sent to a third party.
1647
+
1648
+ **Features**
1649
+ - Tracking
1650
+ - Global `onResponse` hook: counts GET requests answered 2xx or 304 with an HTML (or untyped) response; skips file extensions, prefetch/prerender requests, excluded path prefixes and ignored IPs. Redirects count where they land; 404s from scanners never count.
1651
+ - Visitor id = keyed HMAC of IP + user agent (optionally + the day), using a random per-site key in `data/visitor-key`. No cookies.
1652
+ - Visits split after `sessionGapMin` of inactivity, or when a new outside referrer/UTM source arrives.
1653
+ - Bot detection by user agent: bots are recorded but excluded unless "include bots" is on.
1654
+ - UTM capture (`utm_source`, `utm_medium`, `utm_campaign`); channel classification: Direct, Organic search, Social, Email, Paid, Referral, Campaign.
1655
+ - Device, browser and OS parsing (pure `ua.js`).
1656
+ - GeoIP: off / country (about 8 MB DB) / country, region and city (about 130 MB). DB-IP Lite downloaded to `data/geo/`, refreshed monthly by the daily job; admins can force "update now". DB-IP credited (CC BY 4.0) on the Locations tab and in the report.
1657
+ - Dashboard (`#/plugins/analytics-pro/dashboard[/<tab>]`), seven tabs, keys 1-7 switch tabs:
1658
+ - Overview - KPI tiles with sparklines and comparison against the same point of the previous period (visitors, visits, page views, bounce rate, average visit length, pages per visit, new vs returning); Traffic chart (Chart.js); top pages, channels, countries, devices; "Busiest times" weekday x hour heatmap (click a square, day or hour to filter).
1659
+ - Visitors - every visit, newest first, with its pages in order; search box; Timeline or Table style; paged.
1660
+ - Pages - all pages (views, visitors, entries, exits, average time), landing pages, exit pages.
1661
+ - Sources - channels, "Where from" (referring sites), campaigns.
1662
+ - Locations - countries, regions, towns and cities (regions/cities need the city DB).
1663
+ - Technology - devices, browsers, systems.
1664
+ - Live - visits active in the last 5 minutes, auto-refreshing while open and visible.
1665
+ - Period picker: Today, Yesterday, 7 days, 30 days, 90 days, 12 months, or a custom from/to range; capped at the retention period; days and hours in the site time zone.
1666
+ - Filters (2.1/2.2): a filter builder with conditions on Page, Day, Hour, Landing page, Exit page, Source, Channel, Campaign, Country, Region, City, Browser, System, Device, New/returning visitor, Visitor, IP address (admins only), Pages in the visit, Visit length (seconds). Operators: is, is not, contains, does not contain, starts with; more than / less than for numbers. Up to 20 conditions, all must pass. Suggestions from the period's data. "Exclude this" on any row menu. Named saved filter sets ("Saved" menu, per viewer).
1667
+ - Context menus on every row and card (right-click, the kebab button, Shift+F10): filter by it, exclude it, open the page on the site, copy the address.
1668
+ - Display preferences (per viewer): Traffic chart as Columns / Line / Area / Table; lists as List / Table / Tiles (with per-card overrides); visits as Timeline / Table; chart metric Visitors / Visits / Page views; Compact; Flags; Busiest times on/off.
1669
+ - Export and print (banner "Export and print" menu): printable report (PDF via the browser), every page view, every visit, summary figures, traffic by day/hour, any single breakdown - each as CSV (formula-defused) or JSON; "Everything on screen" as JSON.
1670
+ - Settings slideover (banner cog): Display section for everyone; Tracking section for admins only:
1671
+ - Count visits on/off; IP addresses Full / Masked (last octet dropped) / Not kept; Locations off/country/city; Returning visitors "across days" or "each day" (fingerprint resets at midnight); visit gap in minutes; time zone; keep data for 30/90/180/365/730 days; addresses not to count (single IPs, wildcards, CIDR, IPv6 prefixes, with a one-click "add my address"); path prefixes not to count.
1672
+ - Settings apply live (no restart), saved to `config/plugins.json`.
1673
+ - Privacy tools (admins): erase every hit for one visitor, or every hit from one IP address (`POST /erase`).
1674
+ - Who sees the figures (2.3.0): the dashboard and its routes (`/stats`, `/visits`, `/live`, `/hits`, `/count`) need the `analytics-pro` permission (View), granted to `admin` and grantable to other roles under System > Roles; the sidebar entry carries it too. Before 2.3.0 any signed-in account could read them - on a site with public sign-up, anyone who made an account. IP addresses, settings, erasing and the GeoIP update stay with admins (levels 0 and 1).
1675
+ - Daily job at `dailyRollupHour` (UTC): prunes data older than retention and refreshes a stale GeoIP DB.
1676
+ - Sidebar badge (`/active-count`, tone "live"): visitors on the site in the last 5 minutes - or, while nobody is, page views today as an inventory (tone "info", left out of the folder total); popover with on-site-now, page views today, page views last 7 days, and the top 5 pages being read now.
1677
+ - Access: any signed-in admin user sees the dashboard; IP addresses, the Tracking settings, erase and GeoIP update are for admins (level <= 1) only. No permission key is declared.
1678
+
1679
+ **Where:** Admin sidebar "Analytics Pro" (bar-chart icon) -> `#/plugins/analytics-pro/dashboard`. Banner actions: Refresh, Export and print, Settings.
1680
+
1681
+ ### Backup Pro (`backup-pro`)
1682
+
1683
+ Tier: paid (closed source) · Version: 0.1.1 · Requires: `minCmsVersion` 0.101.5; a site run by Domma (the manager's fleet token) - elsewhere it shows "This site is not run by Domma" · Uses: none declared
1684
+
1685
+ The site's end of Backup Pro (plan: `dcms-marketplace/docs/backup-pro-plan.md`). Domma's `domma-backup` daemon backs every managed site up nightly (restic, encrypted, deduplicated, per-site MongoDB dumps, 14 daily / 8 weekly / 12 monthly nightly backups kept, backups taken by hand kept 90 days or the last 20 - whichever keeps more, a monthly sandboxed test restore; a database it must not touch, such as one on the old 27017 server, is skipped and reported rather than failing the backup; switching nightly backups on never runs slots already passed that day) whether or not the Tool is installed; the Tool puts that in the site's own admin. Owners cannot stop Domma, but everything Domma does is shown and notified.
1686
+
1687
+ - Overview: green/red headline (red when the last backup failed or none is under 26 hours old), next backup, keep rules, last test restore, off-site copy, and "What Domma does with your backups" in plain words; Back up now (with an optional name).
1688
+ - Backups: this site's backups only (the manager resolves the fleet token to one site), with kind - Scheduled, Made by hand, Before an update, Safety backup - who made it and the size. Click to restore; right-click / ⋮ / Shift+F10 to download.
1689
+ - Restore: the whole site, or chosen collections, pages or media folders, picked from the backup's manifest; a plain summary of what will change; always a safety backup first; `.env`, database credentials and licences are never restored from the site. Progress is followed until it finishes.
1690
+ - Download: a `.tar.gz` without passwords, keys or licences.
1691
+ - Activity: the daemon's append-only, hash-chained audit log as sentences - "Restored by Domma (Darryl): the page prices.md from the backup of 27/09/2026 18:37", with the reason given. Worded in the browser, so times are in the reader's zone. Nothing on the screen can edit or hide an entry.
1692
+ - Notifications: sources `backup-pro:backups`, `failures`, `restores`, `changes`, `tests` (admins, all on). The manager delivers the daemon's outbox notices to the Tool's `POST /api/plugins/backup-pro/notices` (the site's MANAGER_SECRET) first and to core's manager notices when the Tool is absent.
1693
+ - Permission `backup-pro`: View, Back up, Restore (Restore also covers downloads); granted to admin. The screen asks the server what the user may do (`/status` answers `can`).
1694
+ - Sidebar badge (`/count`, cached 60 s): nothing when all is well, one red item when the last backup failed or is overdue.
1695
+
1696
+ **Where:** Admin sidebar "Backups" (archive icon) -> `#/plugins/backup-pro` (tabs `/backups`, `/activity`). Banner actions: Back up now, Refresh. API `/api/plugins/backup-pro/*` -> manager fleet routes `/api/fleet/backups*`. User guide: Documentation > Plugins > Backup Pro.
1697
+
1698
+ ### Blog (`blog`)
1699
+
1700
+ Tier: free (open source) · Version: 1.9.4 · Requires: no MongoDB (file storage by default); no `minCmsVersion` declared (sitemap integration needs core 0.89+, notifications need the core notifications hooks) · Uses: none declared
1701
+
1702
+ A complete public blog for a Domma CMS site: posts with categories, tags, scheduling and SEO fields, optional moderated comments, an RSS feed and a choice of post layouts and listing displays. Layouts are ordinary blocks, so a site can restyle them in the Blocks tool. Writers can draft; editors publish.
1703
+
1704
+ **Features**
1705
+ - Admin screens
1706
+ - Posts list: KPI tiles; status filter (All, Published, Drafts, Scheduled), category filter and search; row menu: Edit, View on site, Copy link, Publish now, Unpublish, Duplicate as draft, Delete. Kebab menu: View the blog, Edit layouts in Blocks, Add sample posts / Remove sample posts (admins).
1707
+ - Post editor: title, slug (auto from title), content (Markdown or HTML), excerpt, featured image (media picker), categories and tags as pillboxes (tags reuse existing spelling), status card with schedule date, SEO title and description, per-post layout choice, live preview in any layout (slideover), Ctrl+S save.
1708
+ - Categories: create/rename/delete in a slideover (name, slug, description).
1709
+ - Comments: moderation queue - approve, reject, mark as spam, delete; filter by status/post.
1710
+ - Settings (slideover from the banner cog): base path, posts per page, comments on/off, hold comments for approval, RSS feed length, default post layout, listing display, readers can switch display, in-page search/filter, related posts count.
1711
+ - Public pages (served under the live base path, default `/blog`; changing it needs no restart):
1712
+ - `{base}` index and `{base}/page/:n`; `{base}/category/:slug`; `{base}/tag/:tag`; `{base}/author/:id`; `{base}/:slug` post page; `{base}/feed.xml` RSS 2.0 feed.
1713
+ - Listing displays: Cards, List, Magazine (newest post as lead), Compact - plus any site block named `blog-card-*`. Readers can switch display; in-browser search, filter by category/tag/author, sort and a right-click menu ("More in Design") on listings of up to 300 posts. Works without JavaScript.
1714
+ - Post layouts (blocks): Classic, Feature (full-width hero), Minimal, Sidebar (sticky contents list), Split, Essay (serif, drop cap, pull quotes) - plus any site block named `blog-post-*`.
1715
+ - Post extras: reading time, table of contents, reading-progress bar, share links (email, copy link, native share), newer/older links, "Keep reading" related posts, comment list and form (honeypot anti-spam).
1716
+ - Scheduled posts go live automatically when their date passes (checked per request, no cron).
1717
+ - Shortcodes (any page):
1718
+ - `[blog-list count="5" category="news" /]` - recent post titles and dates (`count` or `limit`, `category` slug).
1719
+ - `[blog-grid count="6" cols="3" category="..." /]` - card grid with images (`cols` 1-4).
1720
+ - `[blog-featured slug="..." /]` - one hero card (omit `slug` for the newest post).
1721
+ - `[blog-categories show-counts="true" /]` - linked category list with optional counts.
1722
+ - Blocks seeded once into `content/blocks/` (editable in Blocks): `blog-post-classic|feature|minimal|sidebar|split|essay`, `blog-card`, `blog-card-row`, `blog-card-compact`.
1723
+ - Sample content: six sample posts with SVG covers and sample categories, added and removed in one click.
1724
+ - Navbar: enabling the plugin appends a "Blog" link to the navbar menu (unless one exists); disabling removes only the link it added; changing the base path moves it.
1725
+ - SEO: registers the `blog` sitemap source (index + public posts; `seo.noindex` posts excluded); canonical URLs on post pages; SEO fields merged so the SEO Tool's image/noindex/focus survive.
1726
+ - Roles: Blog Author (write and edit own posts, read categories) and Blog Editor (everything). Both level 2.
1727
+ - Permissions: `blog-posts` (read, create, update, delete, publish), `blog-categories` (read, create, update, delete), `blog-comments` (read, update = moderate, delete); all granted to `admin`. A writer without `publish` sees only their own posts.
1728
+ - Sidebar badge (`/comments/pending-count`, tone "attention"): comments awaiting approval; popover with waiting/published/drafts/scheduled (next date) and the latest pending comments.
1729
+ - Notifications: source `blog:comments` - "Comments awaiting moderation" (info) to Super Admins by default; resolved when the comment is moderated or deleted.
1730
+ - Blog Pro compatibility: shares its collections; posts from Blog Pro's other blogs are hidden here if Pro lapses.
1731
+
1732
+ **Where:** Admin sidebar "Blog" (file-text icon) with Posts, Categories, Comments; settings via the banner cog on `#/plugins/blog/settings`. Public site at `/blog`.
1733
+
1734
+ ### Blog Pro (`blog-pro`)
1735
+
1736
+ Tier: paid (closed source) · Version: 0.5.5 · Requires: no MongoDB; no `minCmsVersion` declared (sitemap needs core 0.89+) · Uses: none declared · Supersedes: `blog`
1737
+
1738
+ Everything in the free Blog, plus several separate blogs on one site - each with its own address, settings and look - dozens of layouts, a theme designer with self-hosted fonts, page transitions and hover effects, author profiles and pages, and privacy-first media embeds (video, audio, galleries, link cards, maps) usable on any page. It works over the free Blog's data, so upgrading changes nothing a reader sees and a lapsed licence falls back cleanly.
1739
+
1740
+ **Features**
1741
+ - Everything in Blog (posts, categories, comments, RSS, scheduling, samples, shortcodes `[blog-list]`, `[blog-grid]`, `[blog-featured]`, `[blog-categories]`, same roles and permissions, comment badge, `blog-pro:comments` notification).
1742
+ - Multiple blogs (Blogs screen): each with name, base path, description, image, its own settings overrides and theme overrides; posts carry `blogId`; removing a blog moves its posts to another first; the first blog cannot be removed. Per-blog navbar links added, moved, renamed and removed automatically.
1743
+ - Appearance screen: theme layered site < Blog Pro < individual blog; heading and body fonts, accent/text/muted/background/surface/border colours, radius, width, size, heading weight, case and tracking; 10 presets (Paper, Midnight, Ocean, Forest, Rose, Mono, Sand, Neon, Slate, Newsprint); live preview of index or post with unsaved changes.
1744
+ - 25 self-hosted variable fonts (Latin subset, OFL licences bundled): Bitter, Bricolage Grotesque, Crimson Pro, DM Sans, EB Garamond, Figtree, Fira Code, Fraunces, Inter, JetBrains Mono, Literata, Lora, Manrope, Merriweather, Newsreader, Nunito, Outfit, Playfair Display, Plus Jakarta Sans, Raleway, Sora, Source Serif 4, Space Grotesk, Syne, Work Sans.
1745
+ - 43 blocks: 18 post layouts (Bold, Cinematic, Classic, Essay, Feature, Guide, Interview, Letter, Magazine, Minimal, Notes, Overlay, Photo, Sidebar, Split, Sticky, Timeline, Zen), 15 card blocks, 6 index heroes, 2 author headers, 2 author cards.
1746
+ - Listing displays: Cards, List, Magazine, Compact, Overlay, Masonry, Mosaic, Tiles, Text, Polaroid, Ranked, Editorial, Timeline, Headlines, Carousel, Glass, Gallery, Gradient, Spotlight. Index headers (heroes): Simple, Centred, Banner, Split, Minimal, Featured.
1747
+ - Motion: page transitions (None, Fade, Slide, Rise, Zoom, Soft focus, Wipe, Picture morph) using cross-document View Transitions; cards on scroll (None, Fade up, Fade, Zoom, Slide, Cascade); cards under the pointer (None, Lift, Picture zoom, Glow, Tilt). All off under reduced motion.
1748
+ - Authors: profile per user (slug, name, title, bio, avatar, hidden flag, links to Website, X, Bluesky, Mastodon, LinkedIn, Instagram, GitHub, YouTube); author pages `{base}/author/:slug`; optional authors index `{base}/authors`; emails never shown publicly.
1749
+ - Media shortcodes (any page or post), rendered as click-to-load facades (no third-party iframe until clicked; YouTube via youtube-nocookie, Vimeo with dnt):
1750
+ - `[video url= title= caption= ratio= align= poster= /]` - YouTube, Vimeo, Loom or a video file.
1751
+ - `[audio url= title= artist= caption= cover= align= /]` - Spotify, SoundCloud or a sound file.
1752
+ - `[gallery layout=grid|masonry|mosaic|carousel|strip columns= caption= align=]` one `src | caption` per line `[/gallery]` - with lightbox.
1753
+ - `[linkcard url= title= layout=compact align= /]` - rich link preview from oEmbed / Open Graph.
1754
+ - `[map lat= lon= zoom= title= caption= ratio= align= /]` - OpenStreetMap.
1755
+ - `align`: wide, full, center, left, right. Admin "Insert media" slideover builds these.
1756
+ - Featured media on posts: a video/audio link leads the post in place of its picture; cards borrow the provider thumbnail.
1757
+ - Link metadata and thumbnails fetched once server-side (SSRF-guarded), cached in `data/media/` (30 days; failures retried after 6 hours).
1758
+ - Per-blog settings: posts per page, RSS length, comments, hold for approval, post layout, listing display, readers can switch display, in-page search/filter, related posts, author pages, authors index, index header, author page header, authors index style, transition, reveal, hover.
1759
+ - Sitemap source `blog-pro` (every blog's index and public posts).
1760
+
1761
+ **Where:** Admin sidebar "Blog" (book-open icon) with Posts, Blogs, Appearance, Authors, Categories, Comments; a tab bar joins the screens; Settings via the banner cog.
1762
+
1763
+ ### Calendar (`calendar`)
1764
+
1765
+ Tier: paid (closed source) · Version: 1.14.0 · Requires: no MongoDB; no `minCmsVersion` declared (links/sources need core 0.79 hooks, Tool switch-off handling core 0.88) · Uses: `contacts` (invite contacts and groups; add RSVPs to the organiser's contacts), `notes` (attach notes to events), `todo` (to-dos on the calendar)
1766
+
1767
+ An events calendar for the admin and the public site: month, day and list views, all-day and repeating events, types with colours, sharing with people and roles, public event pages with RSVP, an iCal feed, reminders, and a calendar that also shows your to-dos and contacts' birthdays.
1768
+
1769
+ **Features**
1770
+ - Events screen (admin): Month (default), Day and List views; left/right arrows change month, T today, N new; right-click a day for new / new all-day / open / copy the list / clear; hover cards on entries; context menus on rows and entries.
1771
+ - Event editor (slideover, draft kept, Ctrl+Enter): title, type, start/end with quick picks, all-day, repeat (daily, weekly with chosen weekdays, monthly, yearly; interval; until date or count), visibility (public on the site or not), RSVP on/off, "Remind me" (0/15/30/60/120 minutes or 1 day before), "Who sees it": on the site, private link, your team (share with everyone, roles or named people - read-only for them), invited from Contacts (contacts and groups), linked Notes.
1772
+ - Event types (calendar admins): own collection, add/rename/reorder/archive, colour tone (Info, Warning, Success, Danger, Primary, Secondary); five defaults (Meeting, Interview, Public event, Webinar, Other); deleting a used type moves its events first.
1773
+ - Private invite link per event (create/revoke) - opens a non-public event page and RSVP, `noindex`.
1774
+ - Calendar layers from other Tools: core Todo "To-dos" and Contacts Pro "Birthdays", shown outlined, toggleable, never on the public site.
1775
+ - Related items: an "Events" panel in Contacts and Notes editors, with "Schedule with {name}..." and "Add to calendar..." shortcuts.
1776
+ - Call now (1.11.0): a banner button makes a private online meeting starting now (an hour), opens its call as host in a new tab and shows the guest link to copy or email.
1777
+ - Instant calls (1.12.0): switches on the CMS's "Call {name} now…" on contacts and emails (core instant calls).
1778
+ - Add people (1.14.0): the host of a date's call clicks **Add people** - the organiser's contacts and the team, or any name and email - and each gets their own link for THAT date (`?e=&g=<pass>`, signed with the event's `callInviteKey`, which is remade with the guest link and never sent to the admin) by email (team members also a notification and a Messages message from the organiser); they name themselves and knock, and one turned away cannot knock again. Copy link makes a fresh one.
1779
+ - Call chat (1.13.0): each date's call chat is kept (`data/call-chat.json`, per room, `_lib/call/chatStore.js`), so a date's call reopened shows it. The event editor of an online event shows "Chat from the last call (N messages)" - the most recent date with a chat - for its organiser and calendar admins only (`GET /events/:id/call-chat`); it opens read-only, the organiser's lines on the right.
1780
+ - Online meetings (1.10.0, core `_lib/call`): "Online meeting" holds an event as a video call on the site, one call per date (opens 10 minutes before, closes an hour after, up to 6 people). The organiser (and calendar admins) Start the call and let people in; guests use Join the call on the event page (public or private link), the Copy guest link, or Email attendees (carries the link); people it is shared with join from the admin. Optional recording in the host's browser (downloaded, nothing stored; guests told). Unticking or cancelling ends a running call. Also fixed: a reminder chosen at creation was dropped until the next save.
1781
+ - From an email (1.9.0, core 0.103): "Schedule with {name}..." on a message's menu in Email / Mail Reader opens a new event "Meeting with {name}" with the subject in the description; a sender who is one of your contacts (any of their addresses) is invited, otherwise the editor says so.
1782
+ - RSVPs screen: filters, search, confirm/cancel, copy or email the address, open the event, delete; Export CSV of what the filters show; also opened per event from Events menus.
1783
+ - Settings (banner cog): base path (restart needed), events per page, RSVPs on, default public view, occurrence window (days generated ahead), iCal feed length, add people who RSVP to the organiser's contacts.
1784
+ - Public site (default `/calendar`): month grid (default, laid out in the visitor's time zone, steps month in place) or list (`?view=list`, `/page/:n`); `/event/:slug` event page with inline RSVP form (name, email; honeypot; 10 per 10 minutes per IP); `/feed.ics` iCal subscription feed (all-day as `VALUE=DATE`); `/month.json` data endpoint.
1785
+ - Shortcode: `[calendar /]` with `limit`, `type`; `[calendar view="month" month="YYYY-MM" type="..." /]` for a month grid on any page.
1786
+ - Seeded on enable: block `upcoming-events` (`[calendar limit="3" /]`), views Upcoming Events, Past Events, My Events; ships a `calendar` page and a `calendar-rsvp` form.
1787
+ - RSVP to Contacts: a new RSVP is matched by email in the organiser's contacts or added and linked as an attendee (while Contacts is on).
1788
+ - Roles: Calendar Organiser (level 2, create and manage own events) and Calendar Admin (level 1, everything).
1789
+ - Permissions: `calendar-events` (read, create = organise, update, delete = cancel), `calendar-occurrences` (read, create, update, delete), `calendar-rsvps` (read, update, delete), `calendar-settings` (read, update = administer).
1790
+ - Sidebar badge (`/count`): events today, tone "live" while one is on; popover with today, on now, next 7 days, RSVPs this week.
1791
+ - Notifications to the organiser: `calendar:rsvps` (new RSVP), `calendar:reminders` (every 15 minutes, event starting within the reminder time).
1792
+ - Cancelling is soft (status cancelled), per event or per single date.
1793
+
1794
+ **Where:** Admin sidebar "Calendar" -> Events (`#/plugins/calendar`) and RSVPs (`#/plugins/calendar/rsvps`). Public at `/calendar`.
1795
+
1796
+ ### Contacts Pro (`contacts-pro`)
1797
+
1798
+ Tier: paid (closed source) · Version: 0.8.0 · Requires: `contacts` (core Contacts Tool must be on); no MongoDB; `minCmsVersion` 0.93.0 (the group rule) · Uses: `calendar` (birthdays shown on the calendar) · Supersedes: `contacts`
1799
+
1800
+ An upgraded Contacts screen that replaces the built-in one while working on the same data: custom fields, birthdays and ages with reminders, styled groups, faster list editing, and sharing contacts or whole groups with colleagues to view or edit. Switch it off and the core screen returns with nothing lost.
1801
+
1802
+ **Features**
1803
+ - Takes over the core Contacts Tool: hides the core sidebar entry and redirects `#/contacts` to Pro; uses the same `contacts-contacts` collection and core API.
1804
+ - Everything the core screen does (search, groups, starred, import from file, Export all (JSON), duplicate, bulk select/delete, copy email/phone, billing address "Use for invoices" for the Invoices plugin).
1805
+ - Birthdays: date of birth with optional year, age, "birthdays coming up" strip, Birthdays chip, "Send birthday wishes" (opens email) when within 7 days; 29 February handled.
1806
+ - Group looks: emblem (any icon, emoji or up to three letters), colour and fill (soft, solid, outline) on chips, pills, picker and optionally the avatar; via a group chip's menu or Manage groups > Style.
1807
+ - Custom fields (kebab > Custom fields): Text, Long text, Number, Date, Yes / no, Choice, Link, Email, Phone; add, edit, reorder, delete; validated as typed; searchable; up to three as list columns; link/email/phone fields become actions in the contact's menu.
1808
+ - Fluid list: sort by any column (again to reverse), field filters shown as removable chips ("Tier: Gold x"), double-click to edit company/email/phone/custom cells in place (yes/no flips), keyboard navigation (arrow keys, Space select, Delete, Enter open), previous/next in the editor (Alt+Up/Down) saving first.
1809
+ - Sharing (0.6): share one contact, a selection, or a whole group (now and future members) with everyone, roles or named people, view-only or view-and-edit; "Shared with me" chip; "Shared by Sam - view only" labels; only the owner deletes, stars or re-shares.
1810
+ - Calendar source "Birthdays" (all-day items on the Calendar plugin, own contacts only).
1811
+ - Sidebar badge (`/birthdays/count`): birthdays in the next 7 days, "live" when one is today, with a details list.
1812
+ - Notifications: `contacts-pro:birthdays` - daily and at boot, a contact's birthday today, to the contact's owner.
1813
+ - Who may change a group (0.7.4, needs core 0.93.0): groups are shared, so only a group's creator or a holder of the core **Contacts > Manage groups** permission (`contacts.manageGroups`) may restyle, rename or delete it. Core enforces rename and delete; Pro's own style route (`PUT /groups/styles/:name`) applies the same rule. Locked groups have Style, Rename and Delete greyed out in Manage groups and the chip menu, with a help popover ("Why are these greyed out?"); a Style panel opened on a locked group is read-only. Sharing a group is not gated, because it shares only the caller's own contacts in it.
1814
+ - Permissions: uses the core `contacts` permission and `contacts.manageGroups` (no keys of its own); contacts remain per user.
1815
+ - User guide (0.7.5): shown under Documentation > Plugins.
1816
+ - Instant calls (0.8.0): switches on the CMS's "Call {name} now…" on contacts (core instant calls), with the recent calls on the contact.
1817
+
1818
+ **Where:** Admin sidebar "Contacts" (replaces the core entry) -> `#/plugins/contacts-pro`. Custom fields and Manage groups under the kebab menu; display settings under the banner cog (per browser).
1819
+
1820
+ ### Data Transfer (`data-transfer`)
1821
+
1822
+ Tier: paid (closed source) · Version: 1.3.2 · Requires: no hard requirement; MongoDB (a `default` connection) needed for file-to-Mongo transfers - backups, restores and clones work on file storage; no `minCmsVersion` declared · Uses: none declared
1823
+
1824
+ A safe way to move collections between file storage and MongoDB, take backups, restore them and clone collections - at the storage level, so entry ids and metadata survive (unlike the core migrate/import, which issues new ids). Every restore and transfer takes a safety snapshot first and refuses to run without one.
1825
+
1826
+ **Features**
1827
+ - Collections panel: every collection with its current adapter and whether it can move; row menu: Open entries, Move to MongoDB, Move to file storage, Clone..., Back up now.
1828
+ - Bulk transfer file -> MongoDB or MongoDB -> file for selected collections; schema rolled back if a transfer fails part-way; preset collections (roles, user profiles, core presets) never move.
1829
+ - Backups: create (all or selected collections, optional label) into `data/backups/<id>/`; list newest first; open a backup's manifest; restore all or some collections, Merge (upsert by id) or Replace; delete.
1830
+ - Safety snapshots automatically before every restore and transfer; retention counts manual backups (`maxBackups`, 50) and snapshots (`keepAutoSnapshots`, 5) separately.
1831
+ - Clone a collection: new title/slug, schema copied, entries optionally copied (with new ids).
1832
+ - One operation at a time (409 if busy); unsaved slideover input kept as a draft.
1833
+ - Backup format shared with the site manager's Data Manager.
1834
+ - Sidebar badge (`/count`): turns red when the newest manual backup is older than `staleDays` (30) or there is none.
1835
+ - Notifications: `data-transfer:backups` (warning, daily and at boot - backups stale; resolved by a new backup) and `data-transfer:failures` (critical - a transfer or restore failed, with where the data was left and the snapshot id).
1836
+ - Settings (`config/plugins.json`): `maxBackups`, `keepAutoSnapshots`, `staleDays`.
1837
+ - Access: admins only (`requireAdmin`); no permission keys.
1838
+
1839
+ **Where:** Admin sidebar "Data Transfer" (hard-drive icon) -> `#/plugins/data-transfer`.
1840
+
1841
+ ### Docs (`docs`)
1842
+
1843
+ Tier: paid (closed source) · Version: 2.6.0 · Requires: no MongoDB (documents are JSON files under the plugin's `data/`); no `minCmsVersion` declared · Uses: `blog` (Send to a new blog post)
1844
+
1845
+ A document editor inside the admin. Documents are private until shared: share with people as Viewers or Editors, or publish a read-only public link. Import and export Markdown, HTML and text, print to PDF, and send a document on as a draft page, a draft blog post or an email. One person edits at a time; everyone else reads and sees who is editing.
1846
+
1847
+ **Features**
1848
+ - **Home**: a collapsible rail - Recent, Starred, Shared with me, My documents (with folders), Templates, Trash - and cards (page preview, title, when, word count, Shared / Public link / "*name* is editing" chips) or a list. Search (`/`), sort (last modified, last opened by me, title, created), drag-and-drop import. Right-click, Shift+F10 or the three-dot button for Open, Open in a new tab, Star, Rename, Make a copy, Move to, Share, Download, Email, Move to Trash.
1849
+ - **Editor** (`#/plugins/docs/d/<id>`): a page-shaped canvas, sticky toolbar (text style Normal/H1-H4/Code/Quote, undo/redo, bold, italic, underline, strikethrough, highlight, inline code, alignment, bulleted/numbered lists, checklists, quote, link Ctrl+K, image from Media or pasted/dropped (uploaded to Media), tables with a right-click row/column menu, horizontal line, clear formatting), Outline panel, word/character count and reading time, Focus mode (Ctrl+Shift+F), Find and replace (Ctrl+F).
1850
+ - **Markdown shortcuts** while typing: `#`-`####`, `-`/`*`/`+`, `1.`, `[]`, `>`, ```` ``` ````, and `---` + Enter. HTML is the only stored format (2.0 removed the Markdown editor mode; a site set to Markdown has its documents converted once, originals in `data/.backup-markdown/`).
1851
+ - **Sharing**: Viewer (read, copy, export) or Editor (edit, share, public link, email); only the owner moves, trashes or removes editors. Optional email to the people added. Share picker lists people whose role holds Docs (emails shown to admins only).
1852
+ - **Public link** ("Anyone with the link can view"): `/docs/s/<token>` in the site theme, read-only, `noindex`, with Print / Markdown / HTML / Text downloads; off means the address dies at once. Site setting **Allow public links** switches all of them off.
1853
+ - **Edit lock**: one editing tab per document (heartbeat, expires after 90 s). Others read with "*name* is editing" and get **Edit now** when it is free; the owner can **Take over** - the other person's unsaved work is saved as "*title* (your unsaved changes)". Your own other tab shows **Edit here**.
1854
+ - **Unsaved work**: autosave, Ctrl+S, leave guard (Save and leave / Stay), browser prompt on close, a draft kept in the browser and offered back.
1855
+ - **Export**: Markdown (GFM tables, task lists), standalone HTML, plain text, print / Save as PDF (browser dialog, no dependency).
1856
+ - **Import**: `.md` (CMS `marked`, front matter ignored, task lists become checklists), `.html`, `.txt`; up to 4 MB; a leading H1 becomes the title.
1857
+ - **Send to**: Email (up to 10 addresses, the document inline or a link; 10 sends per 10 minutes), a new draft Page (Markdown body, address you choose), a new draft Blog post - both through the core/Blog APIs with the user's own permissions.
1858
+ - **Trash**: restore or delete for good; emptied after **Days in Trash** (1-365, default 30). **Stars** and **Recent** per user.
1859
+ - **Version history**: preview each version with who saved it and when; restore (the current text is kept as a version first); oldest trimmed beyond `maxVersions` (5-500, default 50).
1860
+ - Templates: Save as template, New from a template, Templates place on Home.
1861
+ - Every body is sanitised server-side (sanitize-html allowlist) on every way in and again on the public page.
1862
+ - Scope (config file only): `user` (private unless shared, default) or `all` (everyone holding the Docs permission reads all; admins may edit any).
1863
+ - Permission `docs` (read, create, update, delete) granted to `admin` and `user`; site settings changed by admins only.
1864
+ - Extension registry `globalThis[Symbol.for('domma.docs')]` for Docs Pro: services (documents, folders since 2.1, converters since 2.2, sanitiser, access, and since 2.4 `createDoc` and the HTML parser), admin hooks (document menus, Home places, share sections, editor side panel, and since 2.1 a status chip and a document watcher; since 2.4 add-on importers for New > Import, extra document attributes and editor selection helpers), public-link gate, events.
1865
+ - Saving a document accepts up to 5 MB (2.4.0) - documents with built-in pictures were refused at Fastify's 1 MB default.
1866
+ - **Suggester** share role (2.5.0, with Docs Pro): reads and suggests changes; without Docs Pro a Suggester is a Viewer. Pending suggestions are kept out of the public link, downloads, print, email and Send to by Docs itself; editors may take over editing from a Suggester.
1867
+ - Ready-made templates from an add-on (2.6.0) are listed in Templates after your own, marked with their category, read only.
1868
+ - `contentPaths: ["data"]` - documents are carried across plugin updates.
1869
+
1870
+ **Where:** Admin sidebar "Docs" (book-open icon) -> `#/plugins/docs`; documents at `#/plugins/docs/d/<id>`; public links at `/docs/s/<token>`; settings under the banner cog; guide under **Documentation > Plugins > Docs**.
1871
+
1872
+ ### Docs Pro (`docs-pro`)
1873
+
1874
+ Tier: paid (closed source) · Version: 0.8.0 · Requires: `docs` 2.6+ (2.8+ for context editing; feature-detected); no MongoDB (Spaces, pages and cloud copy settings are JSON files under the plugin's `data/`); optional `puppeteer-core` + a host Chrome for PDF copies · Adds to Docs through its registry (no `supersedes`)
1875
+
1876
+ **Domma Spaces**: publish a Docs folder as a section of the site - a staff handbook, help centre or set of guides - with a page tree, search and a Kindle-style reader. Pages are snapshots: a document reaches the site only when published, and later edits stay private until it is published again. A lapsed licence takes the Spaces offline; documents in Docs are untouched and Spaces are kept.
1877
+
1878
+ **Features**
1879
+ - **Spaces place** on Docs Home: every Space you look after (address, Online/Offline, who can read it, published / with changes / not published counts); New Space (name, address, folder, description, who can read it, online, starting theme and font); Settings, Take offline / Put online, Delete Space (pages go, documents stay) on a context menu.
1880
+ - **One Space**: its documents in reading order with subfolders as sections, each **Published**, **Changes not published** or **Not published**; Publish N changes, Publish all; per document Publish / Publish changes / Publish again, View on site, Move up / Move down, Make this the home page, Take down. "No longer in the Space" lists pages whose document went to Trash, was deleted or moved out (removable).
1881
+ - **Editor**: a chip with the document's state in its Space; document menu Publish to *Space* / Publish changes / Publish again, View on site, Take down (publishing saves first). Docs Home document menu: Publish to *Space*, Take down from the site.
1882
+ - **Who can read**: Public (indexable, in `sitemap.xml`), Signed-in members, or Chosen roles (role or above, core page-visibility rules). Not signed in -> a sign-in page (`/login`); wrong role -> 403; offline -> 404. Members-only pages are `private, no-store` and `noindex`.
1883
+ - **Reader** (`/spaces/<space>/<page>`): contents sidebar (drawer on phones), breadcrumbs, "On this page" (h2/h3, follows the reading position), Previous / Next, search within the Space (every word, titles weighted, highlighted snippets). Home page per Space (a chosen document at `/spaces/<space>`, else a contents page); `/spaces` lists every Space the visitor may read.
1884
+ - **Reading settings (Aa)**: text size (10 steps), font (Serif, Sans, Easy read), line spacing, margins, theme (site colours, Light, Sepia, Dark), layout (Scroll or Pages - CSS-column page turning with buttons, arrow keys, Page Up/Down, space, swipe; past the last page goes to the next page). A dock shows progress and minutes left and keeps Aa in reach. Settings and each page's place are remembered in the reader's browser (resume with "Start from the beginning"); nothing about readers is stored on the site.
1885
+ - **Links between documents** (admin addresses) become links to their pages - in the same Space, or another the reader may open - or plain text until published; publishing warns about links to unpublished documents.
1886
+ - Pages sanitised at publish and again at render; the admin never sends document addresses to readers.
1887
+ - Permission `spaces`: `publish` (publish / take down pages of documents you can edit) and `manage` (create, change and delete Spaces on your own folders - admins any; order, home page), granted to `admin`.
1888
+ - `contentPaths: ["data"]`.
1889
+
1890
+ - **Cloud copies** (0.2.0): copies of your own documents (one folder and its folders, or all; never Trash) in **Google Drive** (scope `drive.file` - only its own files), **OneDrive** (its own app folder, `Files.ReadWrite.AppFolder`), **Dropbox** (App folder) or **S3 / S3-compatible** (Amazon S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO; own SigV4 signing, no SDK) as Markdown, HTML, text and/or PDF, in folders matching your Docs folders under a folder you name. **Every save** (90 s after you stop editing), **Nightly** (after 2am, what changed) or **By hand**; unchanged documents are not sent again; copies stay when a document is trashed or deleted. Each customer makes their own app with the provider - a step-by-step guide per provider in the form, with the site's exact redirect address and a Copy button; Connect opens a sign-in window. Secrets encrypted at rest (AES-256-GCM keyed from JWT_SECRET), never sent back (Saved / last 4 of a key). Cards with state, what/as/when/where, Copy now, Test, See why; menu: Copy everything again, Connect again, Settings, Switch off, Remove. Editor chip ("Copied to Drive 10:42" / failed) and **Copy to the cloud now**. Failures notify the owner (source `docs-pro:cloud`), resolved when copying works again. Permission `cloud.use`, granted to `admin`.
1891
+
1892
+ - **Signatures** (0.3.0): **Request signatures…** on a document's menu - people (name + email, up to 20), optional signing order, message, deadline (7-90 days). The document is frozen with a SHA-256 fingerprint. Each signer is emailed their own link (in turn when ordered) to `/docs/sign/<token>`: the document, consent to sign electronically, typed or drawn signature, or decline with a reason. When everyone has signed, the signed copy - document, signatures and a certificate page (who, when, IP, browser, fingerprints, the full record) - is emailed to everyone as a PDF (web page when the server has no Chrome). **Signatures** place: each request's state, signers, Remind (once an hour each), Cancel, downloads, record. Editor chip ("Signed by 1 of 2"). Notifications to the sender (source `docs-pro:signing`). Needs the site's SMTP; a simple electronic signature (not for deeds/wills). Permission `signing.send`, granted to `admin`.
1893
+ - **Protected links** (0.3.0): an expiry date (the whole day counts) and/or a password (scrypt) on a document's public link, set in its Share panel. Expired -> a clear 410 page; password -> a form, then a 12-hour cookie for that link only; a new password asks everyone again; downloads are gated too. Kept with the document across new link addresses.
1894
+ - **Word files** (0.4.0): `.docx` in New > Import and drag-and-drop (15 MB) - headings, alignment, bold/italic/underline/strike, super/subscript, highlight, links, nested lists with numbering that carries on, quotes, code, tables with merged cells, pictures (PNG/JPEG/GIF to the Media library as `doc-…`, or up to 2 MB built in without Media rights); tracked changes accepted. **Download as Word (.docx)** on Home's and the editor's document menu. Own zip and XML code (no dependencies); hostile files capped per part, by node count and one import at a time; output validated against the Office 2019 schema.
1895
+ - **Merge fields** (0.4.0): **Insert a field** (contact name, first name, company, job title, email, phone, address, website, reference, today's date, your own `{{field}}`) and **Fill in the fields…** - search your Contacts or type values; makes a filled-in copy of your own (the original keeps its fields); values escaped, fields in the title too. A template with fields asks for them as soon as it is used and fills the new document in place.
1896
+ - **Comments** (0.4.0): on selected words (editors; the words are highlighted) or on the whole document (anyone who can open it), in the editor's side panel - reply, resolve / reopen, delete; a chip counts open comments; notifications (source `docs-pro:comments`) to the owner and the thread's people who can still open it. Comments never reach public links, Spaces, Word or cloud copies, or signing.
1897
+ - **Suggested changes** (0.5.0): **Suggest changes** on the document menu (owners and editors) or share as **Suggester** - typing becomes green underlined insertions, deleting strikes words in red, typing over a selection is one replacement; formatting and layout are off while suggesting; its own undo. **Suggestions** panel and chip: who, when, what ("Replace X with Y"), Accept / Reject / all (while editing), Take back for the author; click to find it. The server only saves a Suggester's own suggestions (checked against the stored text). Owner notified of new suggestions, authors when theirs are reviewed (source `docs-pro:suggestions`). Suggestions never reach readers until accepted.
1898
+ - **Context editing in the body** (0.7.0): right-click selected words while editing - bold, italic, underline, strikethrough, highlight, link, clear formatting, or Comment on the selection, right there in the page. The td/th table menu still wins inside tables; with nothing selected the browser's own menu (spellcheck) opens; while suggesting only Comment is offered. Needs Docs 2.8.0 (`editor.applyNamed`).
1899
+ - **Ready-made templates** (0.6.0): twelve in Docs' Templates place - Letter, Quote, Proposal, Simple service agreement, Meeting notes, Project brief, Weekly update, Policy, How-to procedure, Handbook page, Job description, Press release - most with merge fields, so using one opens Fill in the fields at once (Your name prefilled). In-app help popovers on the Comments, Suggestions and Fill-in panels.
1900
+
1901
+ **Where:** Docs -> **Spaces**, **Cloud copies** and **Signatures** in the rail (`#/plugins/docs`); the reader at `/spaces`, `/spaces/<space>`, `/spaces/<space>/<page>`, `/spaces/<space>/search?q=`; the sign-in return address `/api/plugins/docs-pro/cloud/oauth/callback`; guide under **Documentation > Plugins > Docs Pro**.
1902
+
1903
+ ### Email (`email-pro`)
1904
+
1905
+ Tier: paid (closed source) · Version: 0.5.4 · Requires: MongoDB for the message store's extras (without it reading, starring, moving, deleting, folders and sending still work; search, conversations, rules, undo send, send later, snooze and the away message need MongoDB and say so); npm `imapflow` ^2.0.5, `mailparser` ^3.9.28, `nodemailer` 8.0.5; no `minCmsVersion` declared (Contacts switch-off handling core 0.88) · Uses: `contacts` (recipient suggestions and group expansion in compose; Add sender to contacts) · Supersedes: `mail-reader`
1906
+
1907
+ A full email client in the admin: connect your own IMAP mailboxes, read, write, send, search and file mail. Chosen folders are mirrored (envelopes and flags, never bodies) into MongoDB so big mailboxes open instantly, and on top come identities, templates, rules, an away responder, undo send, send later, snooze, threads and one-click hand-off of messages into the CMS.
1908
+
1909
+ **Features**
1910
+ - Three-pane reader (shared with the free Mail Reader): folder tree, message list, reading pane; resizable panes; conversations (threads) view; remote images blocked by default with per-message override and a trusted-senders list; attachments.
1911
+ - Mailboxes: add/edit/remove/test your own IMAP accounts (per user); per-mailbox poll interval 3/5/10/15 minutes; diagnostics. (A quota route exists but the screen does not show it.)
1912
+ - Sign in with Microsoft / Google (0.6.0, engine 1.6.39+; feature-detected): a mailbox can use OAuth2 (XOAUTH2) instead of a password - Microsoft 365 / Outlook.com and Gmail / Google Workspace, with an app the customer registers themselves (the form carries a per-provider guide with this site's redirect URI and a Copy button). Connect opens the provider in a popup; tokens are sealed at rest; the access token is renewed before expiry and once after a refused login; Microsoft's rotated refresh tokens are saved; a refused renewal marks the mailbox "Needs reconnecting", stops polling it and notifies its owner; Disconnect revokes at Google (Microsoft has no app-side revoke). The outgoing server is pinned to the provider's.
1913
+ - Folders: create, rename, delete, subscribe/unsubscribe; choose which folders sync (default Inbox and Sent); run sync now; sync schedule.
1914
+ - Message actions (single and bulk): flag/unflag, read/unread, move, delete, mark as spam; search over the mirrored store.
1915
+ - Compose: shared compose window, replies and forwards, drafts saved to the server, attachments up to 20 MB, recipient suggestions and group expansion from Contacts.
1916
+ - Sending: outgoing SMTP server settings with test; undo send (0, 5, 10, 20, 30 seconds); send later (In an hour, This evening, Tomorrow morning, This weekend, Next week, or a time); Outbox with send now / cancel.
1917
+ - Snooze: hide a message until a chosen time; snoozed list with wake now.
1918
+ - Identities: several "send as" faces per mailbox with display name, Reply-To and signature (rich editor); first is the default; reorderable.
1919
+ - Templates: reusable subject + body snippets.
1920
+ - Rules: conditions on Sender, Recipient, Subject, Mailing list, Has an attachment, Priority, Size in KB with operators contains / does not contain / is exactly / starts with / ends with / matches (wildcard `*`, no regex) / is true / is false / more than / less than; actions Move to folder, Copy to folder, Mark as read, Flag it, Treat as spam, Move to Trash, Stop processing; "Test" previews what a rule would catch on mirrored mail before saving; chosen folders to run over.
1921
+ - Away (vacation) responder with a preview that runs the real decision over the mailbox and explains each message (skips lists, bounces, repeat senders).
1922
+ - Unsubscribe: RFC 8058 one-click performed server-side; other methods handed back to open or compose.
1923
+ - Hand-off to the CMS: save an attachment to Media, turn a message into a draft page, file a message as a collection entry; Add sender to contacts.
1924
+ - Keyboard shortcuts: j/k or arrows to move, Enter open, u read/unread, s star, r reply, Shift+R or a reply all, f forward, c compose, `/` search, x select, Shift+A select all, `!` spam, `#` or Delete delete, Esc, `?` help.
1925
+ - Sidebar badge (`/unread`): unread in Inbox (counted from the store).
1926
+ - Notifications to the mailbox owner only: `email-pro:mailbox` (warning after 5 failed checks in a row; resolved when a check works) and `email-pro:sending` (a scheduled/undo-send message that failed).
1927
+ - Permission `email` (read = Use: add own mailboxes, read and send) granted to `admin`.
1928
+
1929
+ **Where:** Admin sidebar "Email" (mail icon) -> `#/plugins/email-pro`. Settings sections inside the mail view: Folders, Sync, Identity, Outgoing, Sending, Templates, Rules, Away, plus the shared image-senders and diagnostics sections.
1930
+
1931
+ ### Feedback (`feedback`)
1932
+
1933
+ Tier: free (ships with the engine) · Version: 1.1.2 · Requires: a site started by the fleet manager (`MANAGER_URL` + `FLEET_TOKEN`) to send; nothing declared · Uses: Waypoint Pro on the manager (feature-detected) · No MongoDB
1934
+
1935
+ Admins of a managed site tell Domma what is broken, missing or good, straight from their admin, and see what became of it - seen, planned, in progress, done or not planned, with Domma's reply. One plugin, two sides: on a site it is "Send feedback" plus that site's reports; on the Manager (dcm) it is the inbox for every site's reports.
1936
+
1937
+ **Features**
1938
+ - Send (sites): a bug, a request, a question, praise or something else, with a title, description, where it happened and, for a bug, a severity. A report is written to an outbox (`data/outbox.json`) before it is sent, so a manager restart or a rotated fleet token loses nothing; waiting reports go on the next visit and on a timer (`syncMinutes`, default 60), and a retry never makes a second report.
1939
+ - Who sent it is never the browser's word: the site builds the report from the signed-in user, and the manager attributes it to the site its fleet token names.
1940
+ - Replies: the site's list shows each report's status and Domma's reply; unread replies count on the sidebar badge and raise a `feedback:reply` notification.
1941
+ - Inbox (the manager): every site's reports with status, reply and private notes; delete removes it for the site too. The badge counts new reports (red when one is a blocker). A per-site throttle (10 in 10 minutes by default) guards the submit route.
1942
+ - Waypoint Pro bridge (manager): file a report as a Waypoint issue (project, type, priority); the report then follows the issue - to do -> planned, in progress -> in progress, done -> done.
1943
+ - **The Manager's Super Admin can file feedback (1.1.0)**: on the manager, the level-0 role (whatever it is called) also gets a **My feedback** tab (`#/plugins/feedback/mine`) that files its own reports straight into the inbox as "Manager (this server)", after which they are ordinary reports (status, reply, notes, Waypoint).
1944
+ - A standalone site (not started by a manager) is told it has nowhere to send to.
1945
+ - Dates read dd/mm/yyyy (1.1.1).
1946
+ - Permission `feedback` with `send` (send and see this site's reports; on the manager, the level-0 role only) and `manage` (the manager's inbox), granted to `admin`.
1947
+ - Collection `feedback-reports` on the manager only, with all four API verbs off (reports carry admins' names and emails).
1948
+
1949
+ **Where:** Admin sidebar "Feedback" (`#/plugins/feedback`). Since: core 0.92.0 (1.0.0), 1.1.0 (the manager's own feedback).
1950
+
1951
+ ### Garage (`garage`)
1952
+
1953
+ Tier: paid (closed source) · Version: 1.4.3 · Requires: a DVLA Vehicle Enquiry Service API key; no MongoDB (uses the collections service, file or MongoDB); no `minCmsVersion` declared · Uses: none declared
1954
+
1955
+ UK vehicle lookups from the admin: type a registration and the DVLA returns make, colour, year, fuel, engine size, CO2 and MOT and road-tax status. Save vehicles to "My Garage" to see what is due, with a sidebar badge and notifications before MOT or tax runs out.
1956
+
1957
+ **Features**
1958
+ - Plate-style lookup box (UK yellow plate); results kept in a per-user History (newest first; re-lookup updates in place; oldest unsaved pruned beyond `maxHistoryItems`, 100; "Clear unsaved").
1959
+ - My Garage: saved vehicles sorted by urgency - red when MOT expired or tax run out, amber when due within 30 days or SORN; click for details.
1960
+ - Row menu: Details, Look up again, Copy registration, Save to / Remove from My Garage, Delete.
1961
+ - Per-user rate limit on lookups (10 per 60 s by default).
1962
+ - Settings slideover (banner cog): DVLA API key (stored in plugin settings through the core settings route, so saving it needs `plugins.update` - in practice admins only; never shown again; `DVLA_API_KEY` env var wins); applies without restart.
1963
+ - Shortcode `[garage-vehicles user="<user-id>" /]` - a user's saved vehicles as a card grid on a public page (user required).
1964
+ - Sidebar badge (`/count`, per user): saved vehicles needing attention.
1965
+ - Notifications: `garage:due` (daily and at boot) to the vehicle's owner - MOT/tax expired (critical), due within 30 days (warning).
1966
+ - Permission `garage` (read = view, create = look up and save, delete) granted to `admin` and `user`; every user sees only their own vehicles.
1967
+
1968
+ **Where:** Admin sidebar "Garage" (truck icon) -> `#/plugins/garage`.
1969
+
1970
+ ### Invoices (`invoice`)
1971
+
1972
+ Tier: paid (closed source) · Version: 1.7.0 · Requires: no MongoDB; optional `puppeteer-core` + Chrome/Chromium (`CHROME_BIN`) for PDF; SMTP for sending; no `minCmsVersion` declared (notifications need core 0.80+) · Uses: `contacts` (import receivers and pick email recipients from Contacts)
1973
+
1974
+ An invoice generator for sole traders and small businesses: issuer profiles with bank details, receivers (importable from Contacts), line items with optional VAT, automatic numbering, payments and balances, overdue tracking, a print-ready A4 invoice in three styles, PDF download and emailing with the PDF attached, and CSV/Excel export.
1975
+
1976
+ **Features**
1977
+ - Invoices list: KPI tiles (Total invoices, Outstanding, Overdue, Paid, Drafts); filters by status (Draft, Sent, Paid, Overdue, Cancelled), date range, search (includes receiver name), Show archived; sort (newest/oldest, number, billed to, largest/smallest, due soonest).
1978
+ - Row context menu (right-click or row button): Open, Print / preview, Download PDF, Send by email, Mark as sent, Mark as paid, Duplicate, Cancel invoice, Archive / Restore, Export (CSV, Excel), Delete. Batch PDF (up to 100 invoices, one page each).
1979
+ - Editor: issuer, receiver, issue and due dates (date pickers; due from default payment terms), currency, VAT toggle and rate, line items (description, quantity, net unit price) with live subtotal/VAT/total, notes, status; Record payment (date, amount, method - bank transfer, card, cash, cheque, direct debit, other - reference) with running paid/balance; unsaved drafts kept.
1980
+ - Numbering: `PREFIX-YYYY-0001`, resets each calendar year; prefix and digits configurable; hand-typed numbers respected.
1981
+ - Status logic: overdue computed live from due date and balance; paid set automatically when the balance clears (and reverted if a payment is removed); overpayments shown.
1982
+ - Issuers: name, email, phone, website, address, VAT number, logo (upload/pick), structured bank details (account name, bank, sort code, account number, IBAN, BIC) plus free text.
1983
+ - Receivers: name, company, email, phone, address, VAT number, reference; import from Contacts (uses the contact's "Use for invoices" billing address).
1984
+ - Print templates: Classic, Modern, Minimal (with automatic no-logo variants); sites can add their own HTML templates; preview in Settings > Templates.
1985
+ - PDF: server-side render of the same print template via headless Chrome; button hidden when the host cannot render.
1986
+ - Send by email: subject and covering note from templates with `{{number}} {{issuer}} {{receiver}} {{total}} {{dueDate}}`; PDF attached when possible, otherwise the invoice inline; send trail (sent at, to, count); moves draft to sent.
1987
+ - Settings slideover (banner cog, tabbed): Issuers, Receivers, Defaults (currency, VAT on by default, VAT rate, payment terms, default issuer), Numbering, Email (subject, covering note, attach PDF), Branding (house logo, footer), Templates.
1988
+ - Help panel from the banner `?` and inline help popovers.
1989
+ - Sidebar badge (`/outstanding-count`): outstanding invoices, red while any are overdue; popover with amount owed per currency and the next five due.
1990
+ - Notifications: `invoice:overdue` (daily 08:00 and at boot, one per overdue invoice; resolved by payment, cancel, archive or new due date) and `invoice:payments` (payment recorded; off by default).
1991
+ - Access: admins only (`requireAdmin`); no permission keys; no per-user scoping.
1992
+ - **Quotes and self-billing (1.7.0)** - Quotes in their own Q- sequence with a validity date (expiry derived on read, like overdue); Send emails an accept link - the client reviews the quote online and accepts by typing their name or declines with a reason, single decision, admin notified; Convert turns an accepted quote into a draft invoice once, with the two linked both ways. A cross-plugin registry (`Symbol.for('domma.invoice')`: createInvoice/createQuote/recordPayment) for Booking Pro and other Tools. Self-billing: mark a receiver as a self-billing supplier (agreement + review dates, reminder when review passes) and raise SB-numbered invoices printed with the roles swapped and marked SELF-BILLING.
1993
+
1994
+ **Where:** Admin sidebar "Invoices" -> Invoices (`#/plugins/invoice`), Quotes, Issuers, Receivers; settings via the banner cog.
1995
+
1996
+ ### Job Board (`job-board`)
1997
+
1998
+ Tier: paid (closed source) · Version: 2.0.0 · Requires: SMTP configured (sign-up confirmation emails; sign-up answers 503 without it); no MongoDB; no `minCmsVersion` declared (notification audiences need core 0.86, the Calendar source core 0.79) · Uses: none declared
1999
+
2000
+ A recruitment board with three kinds of account plus staff: companies (set up by staff, or self-signed-up and approved) post roles and contracts and manage applications, agents recruit for companies that invite them and represent the candidates who accept them, and candidates sign up publicly, keep a profile and apply. Publishing costs credits. Every record is scoped so people only ever see what concerns them.
2001
+
2002
+ **2.0.0 (2026-10-13)** - plan: `dcms-marketplace/docs/job-board-plan.md`
2003
+ - Company sign-up (`signupCompanies`, off by default): the account and its company, which waits for approval when `approveCompanies` is on.
2004
+ - Credits: wallets per company and per agent (`jb-credit-ledger` under a lock), `creditsPerJob` (1; 0 = free), publishing charges once (`paid_with`); an agent pays from the company's wallet or its own (`charge: 'agent'`); staff gift (`POST /credits/gift`), owners pass credits to their active agents (`/credits/transfer`); Stripe Checkout + webhook for buying (`creditPacks`, `stripeSecretKey`, `stripeWebhookSecret` - masked in the cog). Credits screen.
2005
+ - Every job: type (permanent, full-time, part-time, fixed-term, contract, temporary, freelance, internship), `ir35` (inside / outside / not-applicable) + `ir35_status` (determined / pending / not-applicable) - a contract cannot publish without both - `pay_unit` (year/day/hour), duration, start date, skills. Public JSON API carries them.
2006
+ - Candidate profile: bio, phone, LinkedIn, work history (list), preferences (types, IR35, remote, locations, keywords, minimum pay); Jobs gets "Matches me"; profile completeness for onboarding.
2007
+ - Agent <-> candidate links by invitation (`jb-agent-candidates`), Candidates screen for agents (profile, completeness, notes, review, message), "My agents" on the candidate's profile.
2008
+ - Messages (`jb-messages`): threads per application (candidate <-> team) and per agent-candidate link; Messages screen; notifications and email.
2009
+ - Interviews (`jb-interviews`): proposed by the team on an application (moves it to Interview), confirmed/declined by the candidate, rescheduled/cancelled/completed by the team; Interviews screen; Calendar source `job-board-interviews`.
2010
+ - Notes (`jb-notes`, company-scoped or an agent's personal) and reviews (`jb-reviews`, 1-5, one per author) of candidates and of agents; the subject never reads them (count and average only).
2011
+ - Testimonials (`jb-testimonials`): given by a company or agent (pending until the candidate shows it) or added by the candidate quoting a past employer; shown ones travel with the profile.
2012
+ - Notification sources `credits`, `messages`, `interviews`, `agent-candidates`, `testimonials`.
2013
+ - Invitations by email are bound to the account holding the address (or to a Job Board-verified address) - an account that merely changes its email cannot take them; publishing charges once per job, before the job goes live.
2014
+
2015
+ **Features**
2016
+ - Accounts and roles (created via `registerRole()`, confined to `/job-board`):
2017
+ - Company (`jb-company`) - edit own companies, post jobs, move applications, invite and remove agents.
2018
+ - Agent (`jb-agent`) - work for every company whose invitation it accepted: post jobs, move applications.
2019
+ - Candidate (`jb-candidate`) - browse published jobs of approved companies, apply once per job, withdraw, keep a profile.
2020
+ - Staff - any role holding `job-board.manage`: everything, approve/suspend/reinstate companies, settings.
2021
+ - Permission `job-board` (read = Use, manage = Staff), granted to `admin`.
2022
+ - Admin screens (under `#/job-board`, role-aware):
2023
+ - Dashboard - tiles per role (Jobs, Published, Jobs open to apply for, Applications sent, New applications, In progress, Offers, Companies, Companies to approve, Invitations waiting) and quick actions (Post a job, Browse jobs, My profile).
2024
+ - Jobs - list with menu: Edit, Publish, Back to draft, Close, Copy job id, My applications; job editor (title, company, description, salary min/max, type, remote, location, closing date - shown, not enforced; Ctrl+S).
2025
+ - Applications - list and detail (cover letter, CV link, candidate profile for applicants only); status moves reviewed, shortlisted, interview, offered, rejected; candidate withdraw.
2026
+ - Companies - new/edit company (Company name, Industry, Location, Size, and Owner's email for staff; about, website and logo are API-only), Agents... slideover to invite/remove agents, invitations card for agents (accept/decline, stop working for a company), staff Approve / Suspend / Reinstate.
2027
+ - My Profile - candidate headline, summary, CV URL, skills, years of experience, availability, location.
2028
+ - Public sign-up for candidates and agents (companies never self-sign-up):
2029
+ - Pages `/jobs/join` and `/jobs/verify` (confirm by button, token valid 48 hours).
2030
+ - Shortcode `[job-board-signup /]` or `[job-board-signup type="agent" /]` (also `type="candidate"`, optional `heading`) on any page; must be self-closing.
2031
+ - Rate-limited (5 per hour per IP), honeypot, no account enumeration (existing addresses get an "already have an account" email).
2032
+ - Settings (staff cog): Candidates can sign up, Agents can sign up (both off by default), company contact link under the form (default `/contact`), company accounts' own new companies wait for approval (on), Email about applications (1.5.0).
2033
+ - Public JSON API `GET /api/plugins/job-board/public/jobs` (published jobs of active companies, safe fields).
2034
+ - Emails: agent invitation, invitation accepted, sign-up confirmation, "already have an account", company waiting for approval (staff), company approved/suspended/reinstated (owner), new application and withdrawn application (owner and agents), application status change (candidate).
2035
+ - Notifications (to the people concerned): `job-board:applications`, `job-board:application-news`, `job-board:agents`, `job-board:companies` (waiting for approval goes to Super Admins).
2036
+ - Sidebar badge (`/count`): new applications to review (team/staff) or applications in progress (candidate).
2037
+ - Access rules: records you have no part in answer 404; writes are whitelisted; collections API disabled for every `jb-*` collection.
2038
+
2039
+ **Where:** Admin sidebar "Job Board" (briefcase icon) -> `#/job-board` (Dashboard, Jobs, Applications, Companies, My Profile). Public sign-up at `/jobs/join`.
2040
+
2041
+ ### Mail Reader (`mail-reader`)
2042
+
2043
+ Tier: free · Version: 1.2.3 · Requires: nothing (no MongoDB; no minimum CMS version declared in plugin.json - CLAUDE.md notes core 0.88 for the Contacts integration) · Uses: `contacts` ("Add sender to contacts")
2044
+
2045
+ Read your own IMAP mailboxes inside the Domma CMS admin without anything ever changing on the mail server. Mail Reader connects to any standard IMAP server, shows folders, message lists and fully rendered messages in a three-pane reader, and deliberately cannot mark, move, delete or send - so a phone or desktop client still shows exactly what you have and have not read. It is the free edition of the shared mail core that Email Pro builds on.
2046
+
2047
+ **Features**
2048
+ - Three-pane reader (folders, message list, message) shared with Email Pro (`plugins/_lib/admin/mail/reader-view.js`); resizable panes.
2049
+ - Several mailboxes per user; each user sees only their own. Add, edit, delete a mailbox in the Mail Settings slideover ("Mailboxes" section); "Test connection" tries the details before saving; a blank password on edit keeps the stored one.
2050
+ - Passwords sealed AES-256-GCM (key chain `MAIL_SECRET` -> `INSTANCE_SECRET` -> generated `data/.mailkey`), stored in `plugins/_lib/data/accounts.json` (0600), never returned to the browser (`hasPassword` only).
2051
+ - Read-only enforced at protocol level: mailboxes opened with EXAMINE/`readOnly`, bodies fetched with `BODY.PEEK[]` - reading never sets `\Seen`.
2052
+ - Folder list (`\Noselect` folders hidden); paged message list, newest first (50 per page by default, capped at 200); opening with no account chosen shows the first mailbox.
2053
+ - Message view rendered in a sandboxed iframe (no scripts, no same-origin), HTML sanitised; "Show headers" toggle; icons for priority, replied-to, attachments.
2054
+ - Remote images blocked by default (tracking pixels); per-message "Load images" (never persisted); optional per-mailbox list of trusted image senders ("remember senders" switch, add/remove senders) in settings.
2055
+ - Attachments downloaded through an authenticated fetch-to-blob.
2056
+ - Right-click / context menu on a message: Open, Copy sender's address, Copy subject, Add sender to contacts (core Contacts; hidden when the Contacts Tool is off or the sender is already known).
2057
+ - Keyboard navigation: arrows or j/k, left/right between folders and messages, Enter to open, Esc, `?` for the shortcut list (write shortcuts only do anything under Email Pro).
2058
+ - Diagnostics section (build, configuration, key source - never secrets) for bug reports.
2059
+ - Sidebar badge: unread count across each mailbox's INBOX via IMAP STATUS (never selects, so nothing is marked read), cached one minute per user.
2060
+ - Permission `email` (group Plugins) with one action `read` ("Use"), granted to `admin` only; shared with Email Pro. Give it to other roles under Roles.
2061
+ - Configuration (`config/plugins.json`): `listLimit`, `maxListLimit`, `connectionIdleMs`, `maxConnections` (20), `connectionTimeoutMs`, `allowRemoteImages` (false), `allowInsecureTLS` (false, for self-signed servers), `inlineImageMaxBytes`, `maxMessageBytes`.
2062
+ - API under `/api/plugins/mail-reader`: `/unread`, `/accounts` (CRUD + `/accounts/test`), `/folders`, `/messages`, `/messages/:uid`, `/messages/:uid/attachments/:index`, `/body`, `/diagnostics`, `/image-senders` (+ `/remember`).
2063
+ - Superseded in practice by Email Pro (same routes plus writing, sending and a synchronised store).
2064
+
2065
+ **Where:** Admin sidebar "Mail Reader" (`#/plugins/mail-reader`), gear icon in the reader opens Mail Settings.
2066
+
2067
+ ### Messages (`messages`)
2068
+
2069
+ Tier: paid (closed source) · Version: 1.7.0 · Requires: nothing declared (no MongoDB requirement; collections use the site's default adapter) · Uses: nothing declared (uses core media upload and notifications)
2070
+
2071
+ Private, real-time chat for everyone who signs in to the admin. Staff can message each other one to one, start group conversations, share files, and admins can set up managed groups (such as "Editors"). New messages arrive instantly, conversations can be archived, marked unread and searched, and an unread badge sits in the sidebar.
2072
+
2073
+ **Features**
2074
+ - Conversation kinds: direct messages (deduplicated - starting a DM with the same person reopens it), ad-hoc groups, and admin-managed groups.
2075
+ - Real-time delivery over Server-Sent Events (`/stream`): new, edited and deleted messages appear live.
2076
+ - Add people (1.7.0): anyone in a conversation's call clicks **Add people** and rings a team member who is not in the conversation (someone they may message): a notification, a message from them with a link of the invitee's own (`&i=1`, signed; tick Email for an email too). The link lets them straight into THAT call while it runs - never a later one - and does not add them to the conversation; they do not see the call's earlier chat and cannot ring others. The direct messenger keeps Messages' rule (only someone the sender may reach). Messages is also core's direct messenger (`hooks.registerDirectMessenger`), so other Tools' Add people reaches team members in Messages.
2077
+ - Calls (1.4.0, core `_lib/call`): a Call button in a conversation starts a video call for it; everyone in it gets a live "… is calling - Join" notice (and a notification) and joins straight in (signed-in team: no waiting room); up to 6 people; the conversation shows "started a call" and "Call ended" lines. Links are per person, signed and short-lived; leaving the conversation stops them working.
2078
+ - Call chat in the conversation (1.6.0): what someone types in the call's chat is posted into the conversation as an ordinary message from them (marked "in the call" beside the time) - once, live to everyone, read already by whoever was in the call at the time and unread for the rest. Joined/left lines are not posted. A call started again later shows the conversation's earlier call lines; a deleted one does not come back.
2079
+ - Conversation list with Inbox / Unread / Archived filters and "Search conversations"; per-conversation unread counts.
2080
+ - Composer: "Write a message", attach files (through core `/api/media`, so attaching needs the Media upload permission, which the default `user` role lacks; up to 20 per message), send with Ctrl+Enter; drafts kept per conversation including uploaded-but-unsent attachments, and a half-made new conversation is kept too.
2081
+ - "New conversation" slideover (banner action) with a "Find someone" picker and optional group name.
2082
+ - Conversation context menu (right-click, the more button, or Shift+F10): Open, Mark as read / unread, Archive / Move to inbox, Rename (groups), "Keep messages for..." (retention, starter or admin only), Leave conversation.
2083
+ - Message context menu: Copy text, Edit (within the edit window, 15 minutes by default, showing minutes left), Delete (soft delete, shown as "message deleted"). A conversation's starter can remove others' messages in a group, never in a DM; admins can remove any.
2084
+ - Archiving is per user; a new message un-archives it for everyone.
2085
+ - Retention: per-conversation days (whole number >= 1) clamped to a global ceiling (365 days); default 90 days; pruning runs lazily when a conversation is opened.
2086
+ - Who you can message is enforced on the server: admins reach every active account; others reach staff, people sharing one of their roles, and people they already talk to.
2087
+ - Managed groups (admins only): banner action "Managed groups" to list and create them.
2088
+ - Sidebar badge: unread messages in conversations that are not archived, with a popover of waiting conversations.
2089
+ - Notification source `messages:unread` ("Unread messages", every 15 minutes): a message unread for 10+ minutes (and under 2 days old) notifies its recipient once per conversation; reading or archiving resolves it.
2090
+ - Permission `messages` with one action `read` ("Use"), granted to `admin` and `user`; the sidebar item and every route need it.
2091
+ - Collections `user-message-threads` and `user-messages` (created by the plugin).
2092
+ - Settings (`config/plugins.json`): `globalRetentionCeilingDays` (365), `defaultRetentionDays` (90), `editWindowMinutes` (15).
2093
+
2094
+ **Where:** Admin sidebar "Messages" (`#/plugins/messages`); banner actions New conversation and (admins) Managed groups.
2095
+
2096
+ ### MongoDB Adapter (`mongodb-adapter`)
2097
+
2098
+ Tier: paid (closed source) · Version: 1.0.2 · Requires: MongoDB (a manager-provisioned database) · Uses: nothing declared · No minimum CMS version declared
2099
+
2100
+ Infrastructure plugin for sites run from the Domma CMS manager: it gives each site its own MongoDB database. The manager creates the database and credentials centrally; on the site the plugin just loads the connection details so data-heavy plugins can store their collections in MongoDB instead of files.
2101
+
2102
+ **Features**
2103
+ - Reads `config/connections.json` (written by the manager's `mongoProvisioner.js` during provisioning) at start-up.
2104
+ - Exposes the connections on the Fastify instance as `fastify.mongoConnections`; logs how many connections were registered.
2105
+ - Inactive (logs a warning) when `connections.json` is missing - the site keeps using file storage.
2106
+ - No admin screen, sidebar entry, permissions, shortcodes or routes.
2107
+ - `mongoUri` in `config.js` is the Manager's setting (the master connection it provisions from); the plugin on a site does not read it. The `mongodb` driver is an `optionalDependency` of domma-cms, so it is installed with the CMS where the platform allows.
2108
+ - Enables the MongoDB storage declared by plugins such as Surveys (collections with `storage.adapter: "mongodb"`), which otherwise fall back to files.
2109
+
2110
+ **Where:** No admin UI. Provisioned from the Domma CMS Manager (Site Manager).
2111
+
2112
+ ### Security (`security`)
2113
+
2114
+ Tier: free · Version: 1.1.3 · Requires: nothing (no MongoDB); `minCmsVersion` 0.83.0 (sign-in lockout and password rules, `registerLoginCheck`); reset-request logging needs core 0.88+ (`auth:resetRequested`) · Uses: nothing declared
2115
+
2116
+ Keeps a Domma CMS site safe with no set-up. It ships with the engine and switches on when it arrives: a health check that says what is weak and how to fix it, lockouts after repeated wrong passwords, sensible password rules, a log of every sign-in and password reset request, and an edge filter that turns away scanners hunting for WordPress, `.env` files and the like.
2117
+
2118
+ **Features**
2119
+ - Overview tab: Health (checks needing attention), tiles for failed sign-ins (24 h), blocked now, probes refused today, reset requests (24 h); Refresh.
2120
+ - Health check, grouped: People (unused admin accounts, how many admins, accounts never used), Sign-in (lockout, password rules, two-factor, email for resets and alerts), Server (Domma CMS up to date vs npm, production mode, visitor addresses / TRUST_PROXY, listening address), Web (HTTPS certificate, security headers, HTTPS, private files not served), Plugins (plugin licences). Unknowns are "info", never a pass; cached 10 minutes.
2121
+ - Sign-in lockout: per account (5 failures) and per address (20) within 15 minutes; progressive lock lengths; refusal never reveals whether an account exists; a success clears the account.
2122
+ - Password rules (core `registerPasswordCheck`): minimum length 10 (8-64), block the 10,000 most common passwords (SecLists), block passwords containing the name or email, block one or two repeated characters.
2123
+ - Scanner guard (global `onRequest`): probe paths (WordPress, `.env`, `.git`, phpMyAdmin, `*.php`, backups, traversal...) get a bare 404 before rendering; 5 probes in 10 minutes = 60-minute block; blocked addresses get a bare 403. Loopback and addresses signed in from in the last 7 days are never auto-blocked. Fails open. Site-specific extra probe paths and allowed paths.
2124
+ - Sign-ins tab: All / Failed / Succeeded / Resets filters, Last 24 hours / 7 days / 30 days / All kept; CSV export (formula cells defused). Log kept 7-365 days (30 default).
2125
+ - Password reset requests logged ("Forgot your password?"), with the core's outcome.
2126
+ - Blocked tab: current lockouts with "Unlock all", addresses blocked for probing, addresses blocked by hand; block and unblock manually.
2127
+ - Settings slideover (cog): lockout thresholds and durations, password policy, probe settings, sign-in log days, stale-admin days (90), notification switches.
2128
+ - Notifications (admins): `security:lockouts`, `security:suspicious` (success after many failures), `security:resets` (one address asking about 3 accounts in an hour, or 10 unknown-account requests site-wide in an hour), `security:probes` (off by default).
2129
+ - Sidebar badge: number of health checks needing attention, red only when a failing check is critical.
2130
+ - Permission `security` with `read` (View) and `manage` (Manage), granted to `admin`.
2131
+ - Emergency CLI: `node plugins/security/bin/unlock.js --list | --all | --ip X | --user email` - picked up within about 3 seconds.
2132
+ - On the manager (dcm) the guard runs in front of every proxied site, so a block there protects the whole fleet.
2133
+ - Superseded by Security Pro.
2134
+
2135
+ **Where:** Admin sidebar "Security" (`#/plugins/security`, tabs `/overview`, `/signins`, `/blocked`); cog opens settings.
2136
+
2137
+ ### Security Pro (`security-pro`)
2138
+
2139
+ Tier: paid (closed source) · Version: 1.2.3 · Supersedes: `security` · Requires: nothing declared (no MongoDB); `minCmsVersion` 0.84.0 (two-factor and enrolment at sign-in) · Uses: nothing declared
2140
+
2141
+ Everything in Security, plus the protections a busy or high-value site needs: two-factor sign-in with authenticator apps and recovery codes (required per role if you wish), a firewall that bans repeat offenders for longer each time, per-route rate limits, country rules for the admin, breached-password checks, every signed-in session in one place, an audit trail, file integrity checks against the release, a weekly summary and fleet-wide reporting for the manager.
2142
+
2143
+ **Features**
2144
+ - All Security features (health check, lockout, password rules, scanner guard, sign-in and reset log, blocks, unlock CLI), with data adopted from `plugins/security/data` on first start.
2145
+ - Tabs: Overview, Sign-ins, Blocked, Sessions, Two-factor, Audit, Files (integrity), Fleet (manager only).
2146
+ - Two-factor sign-in (TOTP, RFC 6238): QR code and manual key, six-digit confirmation, recovery codes shown once, "trust this browser" for `trustDays` (30), enforced per role (`enforceRoles`) with enrolment during sign-in. Secrets AES-256-GCM sealed, recovery codes hashed, never on the user record.
2147
+ - "Two-factor" sidebar item for every user (`#/plugins/security-pro/my-2fa`): set up, save recovery codes, get new codes, forget trusted browsers, turn off (unless enforced).
2148
+ - Two-factor tab (admins): who uses it; reset someone's two-factor. CLI `--2fa email`.
2149
+ - Firewall: escalating bans 15 min -> 1 h -> 1 day -> 7 days (strikes forgotten after 30 days); scanner user-agents (sqlmap, nikto, nuclei, wpscan...) blocked on sight; bursts of 404s from one anonymous address (30 in 5 minutes); per-route rate limits for unauthenticated requests (defaults `/api/forms/ 20/min`, `/api/search 60/min`, editable as text lines).
2150
+ - Country rules (DB-IP country database): allow or block countries for the admin and API, or the whole site.
2151
+ - Breached-password check (opt-in, Have I Been Pwned range API, k-anonymity; fails open).
2152
+ - Sessions tab: every signed-in session, end one, sign a user out everywhere.
2153
+ - Audit tab: core audit events, auth events and two-factor events in `audit.jsonl` (kept `auditDays`, 180), filters, CSV/JSON export; records admin-issued password resets.
2154
+ - Files tab: engine files compared to the npm release tarball for the site's CMS version; plugins baselined on first use with "Accept plugin changes"; daily plus "Check now"; critical notification on new differences.
2155
+ - Fleet: sites report every 5 minutes to the manager (`FLEET_TOKEN`); the manager's Fleet tab shows every site, and with `fleet.share` each site's recent blocks join the edge block list (max 24 h).
2156
+ - Weekly summary (Monday from 08:00): notification, plus email to admins when SMTP is set.
2157
+ - Notifications: all of Security's plus `integrity` (critical), `weekly`, and `two-factor` (to everyone: recovery codes and when one is used).
2158
+ - Permission `security` (`read` / `manage`), granted to `admin`; manage now also covers ending sessions and resetting two-factor.
2159
+
2160
+ **Where:** Admin sidebar "Security" (`#/plugins/security-pro[/tab]`) and "Two-factor" (`#/plugins/security-pro/my-2fa`, every user); Pro settings in the settings slideover.
2161
+
2162
+ ### SEO Pro (`seo-pro`)
2163
+
2164
+ Tier: paid (closed source) · Version: 0.3.5 · Requires: `seo` (the built-in SEO Tool); `minCmsVersion` 0.90.0 · Uses: nothing declared · No MongoDB
2165
+
2166
+ An add-on for the SEO Tool built into Domma CMS. It brings Google Search Console into the admin, adds structured data for rich results, finds content problems (links you should add, pages competing for the same search, readability, near duplicates, heavy images), lets you edit every title and description in one grid, moves an old site's addresses across as redirects, logs visitors' dead ends with one-click redirects, and checks every link on a schedule with history and alerts.
2167
+
2168
+ **Features**
2169
+ - Adds tabs to core's SEO screen (via core SEO registry; no own sidebar item): Monitor, Search, Content, Rich results, Bulk, Not found, plus Fleet on the manager.
2170
+ - Monitor: runs SEO's audit, then crawls every page - internal and external links and images checked (4 at a time, 8 s timeout, up to 300 external), redirect hops shown with destination, "could not check" for 401/403/429/timeouts, orphan pages, slow pages (1500 ms); every 24 hours (configurable) and on demand; history charts for score and broken links (a year kept) with table view.
2171
+ - Search (Google Search Console) using the site's own Google OAuth client: paste client ID and secret, redirect URI shown with Copy, Connect with Google, choose property; daily report of the last 28 days vs the previous 28 by date, query, page and query+page; tracked positions for the top 100 searches; opportunities (seen often but under-clicked for its position -> fix the title; page-two queries); URL inspection on demand.
2172
+ - Rich results: forms for FAQ, How-to, Product, Event and Local business per page; Google's required fields as errors and recommendations as warnings; valid blocks are added to the page's JSON-LD (via the `seo:meta` transform); "Test in Google" link.
2173
+ - Content: link suggestions (a page mentions another's focus keyphrase or H1 without linking), competing keyphrases, readability (Flesch ease, sentence length, passive voice; English), near duplicates, image checks (over 300 KB, missing width/height, meaningless file names).
2174
+ - Bulk: every page's SEO title and description in an editable grid, saved back through core SEO.
2175
+ - Migration (in Bulk): read an old site's sitemap URL (indexes and .gz followed, public addresses only) or pasted URLs, plan each (live / redirected / same page / likely match / none), apply as redirects (410 where there is no target); copy plan as CSV.
2176
+ - Not found: logs every 404 outside `/api` and `/admin` (scanner noise and bots filtered), with hits, first/last seen, top referrers ("your own link" flagged red), suggested target page; one-click redirect (through core SEO redirects), ignore, delete; kept 30 days, up to 2000 paths.
2177
+ - Pro settings slideover (from the Not found tab): not-found logging, keep days, ignore list, count bots; monitor interval, external checks, external cap, slow threshold; notification thresholds; fleet reporting.
2178
+ - Notifications (admins): `seo-pro:broken` (newly broken links), `seo-pro:score` (score fell by 10+), `seo-pro:notfound` (a missing page visited 20+ times in a day).
2179
+ - Fleet: sites report counts (score, pages, broken, orphans, 404s, redirects) every 30 minutes to the manager; Fleet tab on the manager only.
2180
+ - Uses core SEO's `seo` permission (read/manage); no permission of its own. A lapsed licence leaves core SEO untouched.
2181
+
2182
+ **Where:** Tools > SEO (`#/seo/...`) - the Pro tabs appear in the built-in SEO screen, so they follow the core `seo` permission (level 0 and `admin` by default).
2183
+
2184
+ ### Booking (`booking`)
2185
+
2186
+ Tier: free · Version: 1.6.0 · Requires: nothing · Uses: `contacts`, `calendar` · Core 0.103 for Book… on an email; the call chat needs a core with `_lib/call/chatStore.js` (unreleased)
2187
+
2188
+ Appointments and resources booked from the site. People (appointments) and resources (rooms, equipment) each have weekly hours and days off; services have a length, buffers before and after, a start step, notice and how far ahead. Nobody is double-booked: free times come from one slot engine and are checked again, inside a write lock, at the moment of booking.
2189
+
2190
+ **Features**
2191
+ - Admin Booking tool with tabs Diary (day / week, people and rooms as columns, click an empty time to book, right-click / ⋮ / Shift+F10 menu), Bookings (upcoming / past / cancelled), Services, People & rooms; banner cog (settings), open booking page, new booking; a getting-started checklist.
2192
+ - Bookings: new (free times offered, or any time), move (free times, clash confirm), cancel with reason, completed, no-show, staff note, history; "Email the customer" on staff changes. Staff may double-book after a confirm; customers never can.
2193
+ - Booking page at a configurable path (default `/booking`): services -> who (optional) -> day and time -> details (name, email, phone optional/required, notes) -> confirmation with Add to calendar. `/booking/<service>` goes straight to one service; `/booking/manage/<id>?t=<token>` lets the customer move or cancel up to the notice (24 hours by default). Honeypot and rate limits.
2194
+ - Shortcode: `[booking /]`, `[booking service="<slug>" /]`, `[booking service="<slug>" with="<id>" /]` (self-closing).
2195
+ - Emails (need SMTP; the settings warn without it): confirmation with .ics, reminder N hours before, moved, cancelled, and an owner copy to the person booked (else the settings address, else the site's).
2196
+ - Time zone per site (default Europe/London); DST-safe.
2197
+ - Contacts: customers become contacts (linked when already there); "Book (name)…" on a contact's menu and a Bookings panel on the contact card.
2198
+ - Email / Mail Reader: "Book (name)…" on a message's menu (core `registerRelatedItems('message')`, 0.103). A sender already in your Contacts (any of their addresses) is booked as that contact, phone filled in (1.2.0, `GET /contact?email=`).
2199
+ - Calendar: a Bookings source; a person linked to a user has their own Calendar events block their times (per person and site-wide switches).
2200
+ - Notifications: `bookings` source - made, moved or cancelled from the booking page. Sidebar badge: bookings still to come today.
2201
+ - Permission `booking` with `read` (See) and `manage` (Manage), granted to `admin`; the Book… actions show only to roles with manage.
2202
+ - Collections `booking-bookables`, `booking-services`, `booking-bookings` (API access off). Extension registry (`Symbol.for('domma.booking')`) for Booking Pro.
2203
+
2204
+ **Where:** Admin sidebar "Booking" (`#/plugins/booking`, `/:tab`, `/:tab/:id`, `/new?contact=` / `?email=&name=`); public page at `/booking`.
2205
+
2206
+ 1.6.0: Add people in an online booking's call - the person booked (host) searches their contacts and the team, or types a name and email; each gets their own link `?b=&g=<pass>` (signed with the session's `callExtraKey`, never the customer's token or the host key), names themselves and knocks; team members also get a notification and a Messages message. When the person booked has no account on the site (the host link may be anyone's), the call offers Copy link only - no directory, no contacts, nothing sent. Needs a core with `_lib/call/invite.js` (unreleased); an older core shows no button. 1.5.0 keeps an online booking's call chat per session (`data/call-chat.json`, `_lib/call/chatStore.js`; a class's shared call has one chat), so the call reopened shows it; the booking panel offers "Call chat (N messages)", read-only, to staff with booking read (`GET /bookings/:id/call-chat`; the host's lines on the right). 1.3.0 serves online bookings' calls (`<bookingPath>/call/<session>`, a class shares one call; emails, .ics and pages carry Join the call / Start the call) - see Booking Pro. 1.1.0 adds the extension registry Booking Pro uses (`lib/extensions.js`, `version: 2`): `rules` (places, companions, daily limit, waiting list), `form` / `answers` (extra questions), `check` (refuse a public booking), admin sections in Booking's own slideovers, extra tabs, and `registry.services` (book, cancel, mail, time helpers). Bookings carry `holds` (every bookable they block) and `alsoNames`. Without an extension every service is plain.
2207
+
2208
+ ### Booking Pro (`booking-pro`)
2209
+
2210
+ Tier: paid (closed source) · Version: 0.4.0 · Requires: `booking` 1.1+ (1.3+ for online, 1.4+ for Call now) · An add-on that registers into Booking's registry (the Cart / Cart Pro pattern), not a `supersedes`
2211
+
2212
+ Everything is set up in Booking itself: each service gets a Booking Pro section, new bookings can repeat, and a Waiting lists tab appears beside the Diary.
2213
+
2214
+ **Features**
2215
+ - Call now (0.3.0): on an online booking, open its call at once even if it is booked for later (the customer's existing link works straight away; email them that it is open, or copy their link); and instant calls that are not bookings - "Call {name} now…" on a contact's or an email's menu, or the video button in Booking's banner - lasting 3 hours, the link emailed or copied, guests let in by the host. Booking 1.4.0 adds the registry `actions` (banner buttons) and honours `callOpenedAt`, so an opened call survives a lapse.
2216
+ - Instant calls moved into the CMS (0.4.0): Pro switches on core instant calls at boot, and Booking's banner Call now opens the core Call now screen (`#/calls/new`). Contacts Pro and the Calendar switch them on too, so they no longer go with a Booking Pro lapse while either runs. On an older CMS only the booking panel's Call now remains.
2217
+ - Online services (0.2.0): "Held online" makes a service a video call on the site (core `_lib/call`, browser-to-browser, TURN relay). Customer: Join the call in the confirmation, reminder, .ics and booking page; the person booked is the host (Start the call in their email and the booking panel) and lets people in. Opens 10 minutes before, closes an hour after. Up to 5 places plus the host; no rooms. Optional recording in the host's browser (downloaded, nothing stored; guests told, red notice while recording). Booking stores `online` on each booking, so booked calls survive a lapse.
2218
+ - Companions: a service that needs a person AND a room (e.g. coach + one of the courts); a time is offered only when both are free and the booking holds both.
2219
+ - Classes: places per session; a session shares places and never clashes with itself.
2220
+ - Waiting lists: a full class offers "join the waiting list" on the booking page; a cancellation books the first person waiting and emails them (`booking-pro-waitlist` collection, API off).
2221
+ - Repeat bookings: weekly, fortnightly or monthly for a number of times; a taken date is skipped; the series can be cancelled together.
2222
+ - Questions on the booking form (text, choice, yes/no; required or not), stored as `booking.ext.answers`.
2223
+ - Limits: a most-per-day per person or room; one booking per customer (by email) every N days (public bookings only).
2224
+ - Lapse: Booking carries on as the free Tool; bookings keep their holds and answers; Pro settings stay on each service.
2225
+ - Planned (not built): payment or deposits, booking reports.
2226
+
2227
+ **Where:** inside Booking (`#/plugins/booking`): the service editor's Booking Pro section, the new-booking slideover, the Waiting lists tab.
2228
+
2229
+ ### Shopping Cart (`shopping-cart`)
2230
+
2231
+ Tier: free · Version: 1.0.3 · Requires: nothing (no MongoDB; no minimum CMS version declared) · Uses: nothing declared
2232
+
2233
+ Sell from your Domma CMS site. Add products with options, stock and pictures; shoppers browse a storefront, keep a basket that follows them from page to page, and check out on one page with Stripe, PayPal or payment offline. Orders to fulfil appear in the sidebar, with fulfilment, tracking, refunds and packing slips from the order.
2234
+
2235
+ **Features**
2236
+ - Admin Shop tool with tabs Overview, Orders, Products (plus Pro tabs when Shopping Cart Pro is present); banner cog (settings), visit shop, add.
2237
+ - Overview: takings and what needs doing.
2238
+ - Products: name, address (slug), summary, Markdown description, price and "was" price, SKU, category, tags, featured, status, stock, images gallery, weight; options (e.g. size, colour) each with its own price, SKU and stock; duplicate; load sample products (bundled SVG pictures).
2239
+ - Orders: statuses pending -> awaiting (offline) -> paid -> fulfilled, plus cancelled and refunded; fulfil with tracking, refund (full, through Stripe or PayPal first), cancel, internal note, packing slip (printable window), delete (not paid/refunded). Stock is taken when an order is paid and can be returned on refund.
2240
+ - Storefront at a configurable path (default `/shop`): product grid with category chips, search and sort; product pages with gallery; `/shop/cart` (also `/checkout`, `/basket`) one-page basket and checkout; receipt at `/shop/order/<id>?t=<token>` that waits for payment confirmation. Server-rendered, works without JavaScript; moving the shop path needs no restart.
2241
+ - Floating basket button and drawer on every page (`inject/body.html`, `public/cart.js`); position left/right.
2242
+ - Payments: Stripe hosted Checkout (with webhook `/stripe/webhook` for closed tabs), PayPal Orders v2 (sandbox or live), offline (e.g. bank transfer with custom instructions). "Test" button per provider in settings. Secrets masked, never sent to the browser.
2243
+ - Checkout safety: prices worked out on the server only; checkout refuses and re-quotes when the basket changed (sold out, fewer left).
2244
+ - Shortcodes:
2245
+ - `[shop-products category="" tag="" featured="true" limit="8" columns="3" /]` - product grid (limit 1-48).
2246
+ - `[shop-product slug="" /]` - one product card.
2247
+ - `[buy-button slug="" label="Add to basket" /]` - add-to-basket button with price (links to the product page when it has options).
2248
+ - Settings (cog): shop title, path, intro, currency (GBP default), columns, search and category toggles, floating cart and position, low-stock threshold and "only N left", terms URL; delivery countries, flat shipping, free-shipping threshold, click-and-collect; tax rate, label, prices include tax, tax delivery; Stripe, PayPal and offline payment settings.
2249
+ - Notifications: `orders` source ("Shop orders", on by default) - "New order #n - total" for each paid order; an "awaiting payment" notice for offline orders.
2250
+ - Sidebar badge: paid orders waiting to be fulfilled.
2251
+ - Permission `shop` with `read` (See) and `manage` (Manage), granted to `admin`.
2252
+ - Collections `shop-products` and `shop-orders` (API access off - orders hold shoppers' details); order numbers start at 1001.
2253
+ - Extension registry for add-ons (discount, shipping and tax stages, events, admin tabs, checkout extras) - used by Shopping Cart Pro.
2254
+
2255
+ **Where:** Admin sidebar "Shop" (`#/plugins/shopping-cart`, `/:tab`, `/:tab/:id`); public storefront at `/shop`.
2256
+
2257
+ ### Shopping Cart Pro (`shopping-cart-pro`)
2258
+
2259
+ Tier: paid (closed source) · Version: 0.1.3 · Requires: `shopping-cart` · Uses: nothing declared · No MongoDB; notes say core 0.88
2260
+
2261
+ An add-on for the free Shopping Cart that adds what a growing shop needs: discount codes, shipping zones with rates by weight or order value, tax by country and product class, branded order emails from confirmation to dispatch, and Insights with takings, best sellers, repeat customers and abandoned checkouts you can nudge. If the licence lapses the Cart and every order carry on exactly as before.
2262
+
2263
+ **Features**
2264
+ - Adds four tabs to the Shop, marked "Pro": Discounts, Shipping & tax, Emails, Insights.
2265
+ - Discounts: codes of three kinds - percentage off, amount off, free delivery; minimum spend, start and end dates, usage limit, once per email, restrict to categories or products, active switch, note; use counts; "Discount code" box on the storefront checkout.
2266
+ - Shipping zones: countries per zone (one zone per country, plus one "everywhere else"); services per zone priced flat, by weight bands (grams) or by order-value bands, with free-over thresholds; shoppers choose between two or more services at checkout (otherwise cheapest). No zones = the Cart's flat rule.
2267
+ - Tax rules by country and tax class; discounts shared across lines by value; delivery taxed at the standard rate; inclusive/exclusive from the Cart's settings.
2268
+ - Emails (HTML tables, inline styles, accent colour and footer): Order confirmation, Awaiting payment (with offline instructions), On its way (with tracking), Refund, New order (to the owner, configurable address), Checkout reminder (manual). Placeholders `{{number}}`, `{{name}}`, `{{total}}`; per-email on/off, subject, intro, button; live preview and "Send test". Never sent without an SMTP host.
2269
+ - Insights: twelve weeks of takings (chart), best-selling products and categories, repeat customers, code use, low stock, abandoned checkouts (pending over an hour with an email) with their value and a one-per-checkout "Send reminder".
2270
+ - Plugs into the Cart's pricing stages (discount, shipping, tax) and order events (paid, status); a failing Pro rule falls back to the Cart's built-in rule.
2271
+ - Uses the Cart's `shop` permission (read / manage); no permission of its own.
2272
+ - Collection `shop-pro-codes`; settings (zones, taxRules, emails) in `config/plugins.json`.
2273
+
2274
+ **Where:** Admin sidebar "Shop" - the Pro tabs sit in the Shop's tab row.
2275
+
2276
+ ### Social Media Pro (`social-media-pro`)
2277
+
2278
+ Tier: paid (closed source) · Version: 0.2.3 · Requires: nothing declared (share cards need the core `seo:meta` transform, core 0.89+/0.90+) · Uses: `seo` ("Share cards start from each page's SEO title, description and image"), `calendar` (scheduled and sent posts on the Calendar), `blog` and `blog-pro` (a post drafted for each new blog post) · No MongoDB
2279
+
2280
+ Control how every page looks when it is shared, and post it. Social Media Pro edits share cards for Facebook, X, LinkedIn, WhatsApp and Slack with previews, draws a share image in the site's colours for any page without one, adds share and follow buttons, and posts to Bluesky, Mastodon and LinkedIn - straight away, scheduled, or drafted automatically for each new blog post or page - with links tagged for analytics and posts shown on the calendar. Sold together with SEO Pro.
2281
+
2282
+ **Features**
2283
+ - Tabs: Cards, Posts, Accounts (`#/plugins/social`, `/posts`, `/accounts`), settings slideover.
2284
+ - Cards: every page listed (All / Changed here); per page Open Graph (Facebook, LinkedIn, WhatsApp, Slack) and X card fields, empty falling back to the page's SEO values; live platform previews; links to Facebook and LinkedIn debuggers to refresh their cache. Saving clears that page's cache.
2285
+ - Drawn share images: 1200x630 PNG for pages without an image, three designs (bold, minimal, split), colours, font, logo (from SEO organisation settings or site brand) and URL toggles, live preview; served publicly at `/api/plugins/social-media-pro/og.png?p=<path>` only for real pages; cached.
2286
+ - Shortcodes:
2287
+ - `[share /]` or `[share networks="x,facebook,linkedin,whatsapp,bluesky,reddit,email,copy" label="Share" /]` - share buttons (plain links, no network scripts or tracking; native share on touch devices; Copy link). Must be self-closing.
2288
+ - `[follow /]` or `[follow label="Follow" /]` - follow links from SEO settings' `organisation.sameAs` (X, Facebook, LinkedIn, Instagram, Bluesky, YouTube, TikTok, GitHub, Threads, Mastodon).
2289
+ - Accounts: Bluesky (handle, app password, custom server/PDS) and Mastodon (server, access token with `write:statuses`); Test and Disconnect. Secrets stored 0600, never returned.
2290
+ - LinkedIn (0.2.0): posts through the site owner's **own LinkedIn app** (products "Sign In with LinkedIn using OpenID Connect" and "Share on LinkedIn"). Enter the app's Client ID and secret on the Accounts tab (the secret never leaves the server), copy the shown redirect URI (`<site.baseUrl>/api/plugins/social-media-pro/accounts/linkedin/callback` - so `site.baseUrl` must be right) into the app's Auth tab, then Connect to sign in with LinkedIn. Posts go out as the member (`w_member_social`), or as a company page (`w_organization_social`, which needs LinkedIn's Community Management API approval). Posts carry an article card (title, description, uploaded thumbnail), because LinkedIn does not scrape API posts; the composer counts against 3000 characters.
2291
+ - Sign-ins last 60 days: the Accounts pill, a banner with Reconnect on every tab, the sidebar count and a `social-media-pro:linkedin-expiry` notice warn 7 days ahead and turn critical when it has run out. Disconnect keeps the app (reconnecting is one click); "forget the app" removes it too.
2292
+ - Posts: compose (character counting that keeps the link whole), draft, schedule, "Post now"; statuses draft / scheduled / sending / sent / failed (Waiting / Posted / All filters); Bluesky posts get link facets and a card with the page image as thumbnail.
2293
+ - Auto-posting: every 5 minutes new sitemap URLs from chosen sources (blog, blog-pro, pages) become a draft or scheduled post per network after a delay (10 min); template default `{title}\n\n{description}\n\n{url}` (must contain `{url}`).
2294
+ - UTM tagging (`utm_source=<network>`, `utm_medium=social`, campaign `social`) so posts show in Analytics.
2295
+ - Calendar source `social-posts` ("Social posts") shows scheduled and sent posts on the Calendar.
2296
+ - Notifications: `social-media-pro:failed` (warning), `social-media-pro:drafted` (off by default), `social-media-pro:linkedin-expiry` (0.2.0).
2297
+ - Sidebar badge: failed posts and drafts waiting (red when any failed).
2298
+ - Permission `social` with `read` (View) and `manage` (Manage), granted to `admin`.
2299
+ - Injects `public/share.css` in the head and `public/share.js` at body end on every public page.
2300
+
2301
+ **Where:** Admin sidebar "Social" (`#/plugins/social`).
2302
+
2303
+ ### Surveys (`surveys`)
2304
+
2305
+ Tier: paid (closed source) · Version: 1.5.0 · Requires: nothing declared; collections declare MongoDB (`storage.adapter: "mongodb"`) and fall back to files when `config/connections.json` is absent · Uses: `contacts` ("Import an audience from Contacts"); notes say core 0.88 · Built over core Forms
2306
+
2307
+ Targeted surveys on top of the Forms engine. Pick any form, choose an audience from your own contact list and groups, send tokenised email invitations and reminders, and watch response rates and per-question statistics - identified or anonymous. Surveys can also be embedded in any page or opened from a hosted survey page with a code.
2308
+
2309
+ **Features**
2310
+ - Surveys list: All / Open / Drafts / Closed, search, latest activity, response rate per survey; context menu Edit, Results, Open / Close, Send invitations, Send a reminder (non-responders), Copy embed shortcode, Duplicate.
2311
+ - Editor (`#/plugins/surveys/new`, `/edit/:slug`): title, slug (fixed once created), form (any core Form), description, audience groups, status (Draft / Open / Closed), mode (Identified / Anonymous), opens and closes dates, reminder after N days (7), Markdown intro and footer, thank-you message, invite email subject and body (`{{name}}`, `{{link}}` - link becomes a "Take the survey" button); Ctrl+S, unsaved-changes guard, draft restored on next visit.
2312
+ - Audience (`#/plugins/surveys/audience`): contacts (name, email, groups) with search, add, copy email, delete (contacts cannot be edited once added); named groups (no rename, and no delete-group route); "Import from Contacts" (your own core Contacts; hidden when the Contacts Tool is off).
2313
+ - Invitations: one 8-character code per recipient (e.g. `ABCD-EFGH`, SHA-256 hashed, never stored in plain text), sent through the site's SMTP; one response per invite. Links are built on the Site URL setting (`site.baseUrl`), else the old `site.url`, else the request's address (1.4.4).
2314
+ - Hosted page `/survey/:slug` (token in `?t=` or typed code), 404 unknown / 403 before its Opens day / 410 closed; submit at `POST /api/plugins/surveys/submit/:slug`.
2315
+ - Opens/Closes are days in the site's time zone (`timeZone` setting, default `Europe/London`): a survey takes answers from the start of its Opens day to the END of its Closes day. The hosted page, submissions, scheduler reminders, Send (409 once closed), the list chip, the badge and the closing notices all use the same rule (1.4.4).
2316
+ - Shortcode `[survey slug="..." /]` - embeds the survey on any page; Copy embed shortcode copies exactly this, and the page gets the survey script added automatically (1.4.4).
2317
+ - Results (`#/plugins/surveys/results/:slug`): invited, answered, response rate, per-question aggregates (only this survey's responses, tagged `survey:<slug>`); Export CSV and Export JSON; anonymous flag.
2318
+ - Anonymity: contact-to-response link kept only in the invite ledger, never on the response.
2319
+ - External scheduler contract: `GET /api/plugins/surveys/due-work` and `POST /api/plugins/surveys/run` with a `schedulerToken` for automatic reminders.
2320
+ - Notifications: `surveys:responses` (one notice per survey per day, updated with the count), `surveys:closing` (daily and at boot: open surveys closing within 3 days).
2321
+ - Sidebar badge: responses in the last 7 days, with open and closing-soon surveys in the popover.
2322
+ - Permission `surveys` with `read`, `create`, `update` (edit and send invitations), `delete` (contacts and groups), granted to `admin`.
2323
+ - Collections `surveys-surveys`, `outreach-contacts`, `outreach-groups`, `outreach-unsubscribes`, `surveys-invites` (1.5.0 - rows from the old `survey-contacts`/`survey-groups` are carried across once at boot).
2324
+ - **Shared outreach store (1.5.0)** - the survey audience moves to the shared outreach store (one audience for Surveys and Campaigns, carried across automatically), and sends honour the new shared unsubscribe list: an address that opts out is never emailed by any outreach Tool again.
2325
+ - Settings: `schedulerToken`, `fromName` ("Surveys"), `timeZone` ("Europe/London"; read at boot).
2326
+
2327
+ **Where:** Admin sidebar "Surveys" (`#/plugins/surveys`); banner actions Audience and New survey.
2328
+
2329
+ ### Theme Roller (`theme-roller`)
2330
+
2331
+ Tier: paid (closed source) · Version: 1.1.3 · Requires: Domma CMS 0.73+ (`hooks.registerThemes`; stated in the description, not as a field) · Uses: nothing declared · No MongoDB
2332
+
2333
+ Make your own themes. Start from any built-in Domma theme, change its colours, fonts, spacing and borders with a live preview, and save it. Saved themes join the built-in ones wherever a theme is chosen - the site, the admin, day and night themes, or a region of a page - and you can edit the site's custom CSS in the same place.
2334
+
2335
+ **Features**
2336
+ - Roller tab: pick a base theme and change `--dm-*` tokens with a live preview of the whole admin; Save, "Save as a new theme", New theme; unsaved indicator; unsaved work offered back on the next visit.
2337
+ - Saved themes tab: list with usage; context menu Edit in the roller, Use on the site / Stop using, Use in the admin / Stop using, Duplicate, Rename, Export CSS, Delete (a theme in use falls back to its base).
2338
+ - Custom CSS tab: edit `content/custom.css` (needs the Site Settings permission), Ctrl+S to save.
2339
+ - Themes offered to core via `hooks.registerThemes`: listed under "Custom themes" in Content > Theme, usable for site theme, admin theme, auto day/night themes and page regions; applied as `dm-theme-<base> dm-custom-<id>`.
2340
+ - Using a theme on the site switches off auto day/night; changing a theme in use clears the page cache.
2341
+ - Token values checked with the core's safe-value rule (no `url()`, `@import`, comments, braces).
2342
+ - Themes stored as `data/themes/<id>.json` (declared `contentPaths`, never shipped).
2343
+ - Sidebar badge: number of custom themes and how many are in use; popover shows site (or day/night) and admin theme.
2344
+ - Uses core permission `theme` (`read` / `update`); no permission of its own.
2345
+
2346
+ **Where:** Admin sidebar "Theme Roller" (`#/plugins/theme-roller`); saved themes then appear in Content > Theme.
2347
+
2348
+ ### Waypoint Pro (`waypoint-pro`)
2349
+
2350
+ Tier: paid (closed source) · Version: 1.1.2 · Requires: nothing declared · Uses: nothing declared (Feedback on the manager uses it, feature-detected) · No MongoDB (five plugin collections, all API verbs off)
2351
+
2352
+ An issue tracker in the style of Jira: projects with their own workflows, issues on a board or in a list, a backlog planned into sprints with a burndown, and comments, mentions and watchers that tell the right people.
2353
+
2354
+ **Features**
2355
+ - Projects with a key (`WEB`), lead and description, and their own workflow: statuses (each in a to do / doing / done category), allowed moves (or any move), issue types and priorities. Ids survive renames; removing a status, type or priority still in use answers 409 with how many issues to move first.
2356
+ - Board (kanban, drag between columns; moves the workflow refuses are dimmed and refused by the server with 409), List (filter and sort), Backlog (rank by drag, plan into sprints).
2357
+ - Sprints: one active per project; start, complete (unfinished issues go to the backlog or a planned sprint), and a burndown (story points when any issue has an estimate, else issue count).
2358
+ - Issues (`KEY-N`): type, status, priority, assignee, reporter, sprint, epic, estimate, labels, due date, watchers; comments with @mentions; history.
2359
+ - Telling people: watchers, the assignee and anyone mentioned get an in-admin notice (and an email when `emailNotifications` is on and mail is set up); nobody is told about their own change.
2360
+ - Sidebar badge: the open issues assigned to you, red when one is overdue.
2361
+ - 1.1 - the Tool Upgrade: banner actions (+ New project / New issue, ? long help, settings cog), every slideover rebuilt to the Tool standard (resizable, width remembered per kind of panel, sticky Cancel/Save foot, Ctrl+Enter / Ctrl+S), unsaved work kept as drafts and offered back, right-click, ⋮ and Shift+F10 menus on issues, projects, sprints and comments. 1.1.1: an issue opened from its address (`/i/KEY-N`) can be closed and others opened again.
2362
+ - For other plugins: `globalThis.dommaWaypoint` (`listProjects`, `createIssue`, `getIssue`) - how Feedback files a report as an issue.
2363
+ - Permission `waypoint` with `read` (see projects and issues), `write` (create, edit, move and comment on issues) and `manage` (create projects, change workflows, plan and close sprints, delete issues, edit others' comments).
2364
+
2365
+ **Where:** Admin sidebar "Waypoint" (`#/plugins/waypoint-pro`); a board at `#/plugins/waypoint-pro/p/KEY`, an issue at `/i/KEY-N`.
2366
+
2367
+ ### Campaigns Pro (`campaigns-pro`)
2368
+
2369
+ Tier: paid (closed source) · Version: 1.0.1 · Requires: the site's email set up for sends; no MongoDB · Uses: `contacts` (import the audience), `analytics-pro` (UTM-tagged landings appear in Sources), `shopping-cart-pro` (linked discount codes), `surveys` (send a survey as a campaign); shares the outreach store (audience, opt-outs) with Surveys
2370
+
2371
+ **Campaigns Pro 1.0.0 - one paid Tool (2026-10-13).** The free Campaigns Tool was folded in at Darryl's request; there is no free tier. A campaign is an audience, a message and optionally an offer. Offers close by an end date OR a claim limit, first reached; claims are a locked ledger that can never overshoot. Every email carries a working unsubscribe link feeding the shared opt-out list Surveys also honours. Sends are batched, schedulable (within a quarter of an hour) and resume safely after a restart. Claim page at `/campaign/<slug>` plus a `[campaign-offer]` embed.
2372
+
2373
+ Measurement and automation: a Performance screen per campaign - reached, opened (counted by a pixel some mail apps block - the true number is at least what you see; a click always counts), clicked and claimed, link by link. Site links are UTM-tagged so Analytics' Sources shows the landings. Link the Shop discount code to an offer and see claims and redemptions side by side. Follow-ups run themselves (everyone / non-openers / non-claimers, after 0-14 days); resend to non-openers with a new subject in one press; A/B subject tests send two slices, measure opens, and send the winner to the rest automatically - the ordinary Send now is guarded while a test runs. Send a survey to the campaign's audience with personal links; answers stay in Surveys. Stats feed: `GET /api/plugins/campaigns-pro/stats` (version 1) for the Sheets Tool. Opt-outs are never emailed by any wave.
2374
+
2375
+ **Unsubscribe outlives the licence (engine 1.6.35+, Campaigns Pro 1.0.1).** The engine owns the unsubscribe secret (`config/outreach.json`, adopting the one Campaigns Pro kept before) and answers `/campaign-unsubscribe` itself (`server/routes/outreach-public.js`, signing in `plugins/_lib/outreach/unsubscribe.js`), so a link in an email already sent still opts the reader out after the licence lapses or the Tool is removed (PECR). Campaigns Pro keeps its own route only on older engines; the engine stands aside if a plugin already holds the path.
2376
+
2377
+ **Where:** Admin sidebar "Campaigns Pro" (`#/plugins/campaigns-pro`); Performance per campaign at `#/plugins/campaigns-pro/performance/:slug`; the audience screen is shared with Surveys.
2378
+
2379
+ ### Onboard Pro (`onboard-pro`)
2380
+
2381
+ Tier: paid (closed source) · Version: 1.0.0 · Requires: nothing; no MongoDB · Uses: `contacts` (details land on a contact; auto-enrolment watches a group), core Forms (form steps), `docs` (read steps can point at a document or Space), `docs-pro` (signature steps through the signing registry)
2382
+
2383
+ **Onboard Pro 1.0.0 - one paid Tool (2026-10-13).** The free Onboard Tool was folded in at Darryl's request; there is no free tier and no journey limit. Design an onboarding journey once (read these pages or documents, fill in this form, give us your details, things done outside the site), enrol people from Contacts or typed email, and each gets a personal link - no account needed. Progress from one screen: tick or waive steps, send a fresh link, nudge the quiet (opt-outs never emailed), morning stalled notifications and a notice when someone finishes. Details land on their contact; form answers land with the form's submissions, tagged with the enrolment.
2384
+
2385
+ Sign steps send a document for signing through Docs Pro and tick themselves when the signed copy returns (declines notify you with the reason); account steps create the person's site account on your approval with a set-your-password email; due dates mark the overdue; automatic reminders go out with a fresh link and contacts arriving in a watched group enrol themselves; manual steps can need your countersignature; any step can be waived with a reason; and the funnel shows reached/done/waiting, drop-off and median days per step, with CSV.
2386
+
2387
+ **Where:** Admin sidebar "Onboard Pro" (`#/plugins/onboard-pro`), with the funnel, signing requests and Pro settings under Insights (`#/plugins/onboard-pro/insights`); each person's journey at `/onboard?t=<their token>`.
2388
+
2389
+ ### Sheets (`sheets`)
2390
+
2391
+ Tier: paid (closed source) · Version: 0.4.0 · Requires: nothing; no MongoDB (workbooks are JSON files under the plugin's `data/`) · Uses (feeds, each optional): `contacts`, collections, forms, `surveys`, `campaigns-pro`, `analytics-pro`, `shopping-cart`
2392
+
2393
+ **Sheets (0.2.0)** - workbooks of up to 20 sheets, 50,000 filled cells each. Windowed grid with keyboard-first editing, Excel-compatible clipboard, undo, column types/formats (display-only), sort and filters. Formulas: A1 references, ranges, cross-sheet by name, 21 functions, dependency-graph recalculation with #CYCLE!/#REF!/#VALUE! errors, recalculated server-side on every save. Import CSV or JSON (objects -> header row; arrays -> rows); export CSV with BOM. Owner/editor/viewer sharing, Docs-style edit lock with take-over, Trash.
2394
+
2395
+ **Sheets 0.4.0 - feeds, Excel and print.** Bind a sheet to a source (Contacts, any collection, a form's submissions, survey progress, campaign results, visitor analytics by day/page/source, Shop orders or products): read-only rows refreshed on demand, stored in the workbook so they outlive the source, fetched on the asking user's permissions, and readable by formulas on other sheets (`=SUM(Orders!D2:D100)`). Real `.xlsx` import and export (values, formulas, widths, number formats; 15 MB cap with zip-bomb guards). Print a sheet as a clean table with a repeating header row.
2396
+
2397
+ **Where:** Admin sidebar "Sheets" (`#/plugins/sheets`).
2398
+
2399
+ ### Other plugin directories
2400
+
2401
+ No other directories exist in `dcms-marketplace/plugins/` beyond the 29 plugins above and `_lib`. `_lib` is not a plugin (skipped by plugin discovery): it holds the shared mail core used by Mail Reader and Email Pro, plus shared `dataStore.js` and `schemaSync.js`.
2402
+
2403
+ ### Reserved slugs
2404
+
2405
+ Held in `dcms-marketplace/reserved.json` so no third-party plugin can take the name of a built-in feature or a retired plugin.
2406
+
2407
+ - `analytics` - became part of the CMS 2026-09-22 (`server/routes/api/analytics.js`); held so a plugin cannot wear a built-in Tool's name.
2408
+ - `contacts` - became part of the CMS 2026-09-22 (`server/routes/api/contacts.js`); same reason.
2409
+ - `notes` - became part of the CMS 2026-09-22 (`server/routes/api/notes.js`); same reason.
2410
+ - `todo` - became part of the CMS 2026-09-22 (`server/routes/api/todo.js`); same reason.
2411
+ - `demo-viewer` - became the built-in `[demo]` shortcode 2026-09-22 (`server/services/markdown.js`); same reason.
2412
+ - `theme-switcher` - became a theme setting 2026-09-22 (`config/theme.json` `switcher`); same reason.
2413
+ - `form-builder` - retired when forms moved into core; held to avoid resurrecting a dead name.
2414
+ - `site-search` - retired into `server/services/search.js`; same reason.
2415
+ - `seo` - became part of the CMS in 0.90.0 (`server/services/seo`, `#/seo`); held so a plugin cannot wear a built-in Tool's name, and because SEO Pro requires the Tool.
2416
+
2417
+ ## Feature history
2418
+
2419
+ The CHANGELOG has no entries introducing reference fields, file fields, row-level access, storage migration,
2420
+ or the timeline, carousel and listgroup displays, and it skips 0.6.21-0.22.2, 0.25.15-0.30.0, 0.37.0-0.41.0 and
2421
+ 0.62.0-0.64.2. The versions below come from `git log`: the commit that first added the code, and the next
2422
+ `package.json` version bump after it (the project commits features first, then bumps the version). The old
2423
+ CHANGELOG sections are left as they are.
2424
+
2425
+ | Feature | First commit | Arrived in | How it was found |
2426
+ |---|---|---|---|
2427
+ | Row-level access, `owner` and `field` modes (actions and views) | `124eddc` (2026-03-09) | 0.5.0 | `git log -S'buildRowLevelMatch'` |
2428
+ | Storage migration (`POST /api/collections/:slug/migrate-storage`, editor prompt) | `9d1a098` (2026-03-24) | 0.6.20 | `git log -S'migrate-storage'` |
2429
+ | `[collection display="timeline"]` (timeline/roadmap, layouts, themes) | `f8674ff` (2026-04-22) | 0.14.3 | `git log -S"display === 'timeline'" -- server/services/markdown.js` |
2430
+ | Reference fields (`type: "reference"`, `references.js`, write-time integrity) | `e73e8e8` (2026-05-24) | 0.20.0 | `git log -S'validateReferences'` |
2431
+ | File fields (`type: "file"`, multipart form submissions) | `e73e8e8` | 0.20.0 | `git log -S'parseMultipartForm'` |
2432
+ | Row-level `reference` mode | `e73e8e8` | 0.20.0 | same commit message |
2433
+ | Also in `e73e8e8`: filter engine, Collection Browser, `scope="mine"`, transitions, `createInCollection`, multi-role users, scaffolder | `e73e8e8` | 0.20.0 | same commit message |
2434
+ | `[collection display="carousel"]` and `display="listgroup"` | `5046b14` (2026-07-22) | 0.38.0 | `git log -S"display === 'carousel'" -- server/services/markdown.js` |
2435
+
2436
+ Later changes to these features are in the CHANGELOG (for example 0.92.1: row-level `reference` applied to views;
2437
+ 0.93.0: id-preserving storage moves and interactive `scope="mine"` with transitions).
2438
+
2439
+ ---
2440
+
2441
+ ## Appendix A - Tutorial ideas
2442
+
2443
+ ### Content, authoring and the public site
2444
+
2445
+ - Build your first page: create, write Markdown, preview in split and Live Preview at mobile width, publish.
2446
+ - Draft, review and publish: use the draft banner, send a client a 7-day share link, revoke it, publish from the banner.
2447
+ - Members-only content: gate pages with "role and above" versus "role only" (`=role`), arrays for sibling roles, and matching menu item visibility.
2448
+ - A landing page in 15 minutes: `[hero]` with a gradient, a `[grid]` of `[card layout="icon-top"]`, a `[row reveal]`, `[button]` calls to action and a `[countdown]`.
2449
+ - Card layouts gallery: pricing tables, testimonials, stat metrics and team profiles using `layout=` and effect injection.
2450
+ - Make it move: scroll reveals, a scripted `[scribe]` headline, seasonal `[celebrate theme="auto"]`, and respecting reduced motion.
2451
+ - Reusable blocks: design a block with companion CSS and sample values, use it inline and as a `[collection display="block"]` template.
2452
+ - Your first component: build a `.dmc` with props and a script, insert it with `[component]`, export and import it.
2453
+ - Theming a brand: pick a theme, set fonts, add day/night switching, re-theme the navbar with an element override, enable the visitor theme switcher.
2454
+ - Getting found: fill SEO fields, run the SEO audit, fix warnings, add redirects after restructuring, check sitemap.xml and robots.txt.
2455
+ - Undo anything: version history, named versions before a big edit, compare and restore, trimming revisions.
2456
+ - Media workflow: upload by drag and drop, crop and convert an image to WebP with a watermark, insert it into a page.
2457
+
2458
+ ### Structured data
2459
+
2460
+ - "Your first collection": create a Products collection, add fields in a grid layout, import JSON, then show it with `[collection display="cards"]` and a `where_*` filter.
2461
+ - "Build a searchable directory": the interactive Collection Browser with `searchable filterable sortable`, fixed author filters, and sharing a filtered view with `dmf=` links.
2462
+ - "One collection, eight looks": the same data as table, cards, list, accordion, timeline, carousel, listgroup and a custom `block` template.
2463
+ - "Multi-step application form with an age gate": page breaks, conditional logic, an `end-form` trigger, and admin notifications.
2464
+ - "Relational data without code": reference fields (jobs and applications), `resolveRefs`, `scope="mine"` dashboards, and reference-mode row-level access.
2465
+ - "Reporting with Saved Views": a `$group` or `$lookup` pipeline built in the view editor with live Results, published with an interactive `[view]`.
2466
+ - "Approval workflows with Actions" (Pro): transitions (pending, reviewing, approved), email and notify steps, and per-row `transitions` buttons.
2467
+ - "Ship a public JSON API in five minutes": token-mode `/api/v1`, a project-scoped API token, then a custom API Builder endpoint with `:params`.
2468
+ - "Scaffold an onboarding portal in one click": apply the Onboarding recipe, then customise the generated collection, form and actions.
2469
+ - "Moving a collection to MongoDB": set up connections, switch storage in the editor, and understand the fallback behaviour.
2470
+ - "A candidate dashboard with `scope="mine"`" (0.93.0): an interactive `[collection scope="mine" searchable transitions]` where each applicant sees only their own applications and can withdraw one, and what the 403 and 409 answers mean.
2471
+ - "Moving storage without breaking links" (0.93.0): move a collection with references and owned rows to MongoDB and back, keeping ids; what a 409 id collision means; where the `.bak` file and the `__moved_` collection go.
2472
+ - "Who can run this action?" (0.93.0): listing several roles on an action (`admin` and `hr`), how the role ladder works, and why a deleted role name locks an action to the level-0 role.
2473
+ - "Notes and Todo on MongoDB" (0.93.0): what happens on a site with a MongoDB connection, the both-stores log line, and settling it with a Storage move.
2474
+ - "Where did that date come from?" (0.93.0): dd/mm/yyyy throughout the admin, what stays ISO (API payloads, CSV exports, feeds) and why.
2475
+
2476
+ ### Admin, users, security and operations
2477
+
2478
+ - Your first 15 minutes: `npx domma-cms my-site`, the browser setup wizard, the dashboard tour and setting the Site URL and SMTP (including why the "No SMTP host" badge matters).
2479
+ - Building a client portal with a confined role: create a role with an Admin area scope, give it a home screen, add a user, then sign in as them and see what they can reach.
2480
+ - Rearranging the admin sidebar: move Tools between folders, hide ones you do not use, choose an admin home, and see which Tools are locked in place.
2481
+ - Staying on top of your site with notifications: the sources, audiences, desktop pop-ups, sample notifications and to-do reminders.
2482
+ - Locking down sign-in: the Security plugin's lockout and password rules, `TRUST_PROXY=loopback`, sessions and "sign out everywhere", and using `npm run users` or `unlock.js` when locked out.
2483
+ - Handling password resets as an admin: send a link, copy a link for a site without email, withdraw it, and what the user sees.
2484
+ - Switching built-in Tools off and on: what happens to data, cascades to dependent plugins, and manager locks.
2485
+ - Updating a site safely: `npx domma-cms update --dry-run`, backups, what is kept, and `--force` repairs.
2486
+ - Personal productivity in the admin: Todo with due dates and the Calendar layer, Notes categories, Contacts groups and import/export.
2487
+ - Adding custom profile fields for users and using the My Profile completeness badge.
2488
+ - Shared Contacts groups without surprises (0.93.0): who may rename or delete a group, granting **Contacts > Manage groups** to an office manager, and the greyed-out buttons with their help popover.
2489
+ - Checking your Node version before an update: Node 24, what `npx domma-cms` says on an older Node, and how to switch with nvm.
2490
+ - Finding your way round the Documentation folder: Usage, Tutorials, API Reference, the new Editions & Licences page, and the user guides of your plugins under Documentation > Plugins.
2491
+ - Calling the API from outside: the External API & Tokens and API Builder reference pages, walked through with curl.
2492
+
2493
+ ### Plugins & the Marketplace platform
2494
+
2495
+ - Build your first plugin in 10 minutes: New plugin in the Marketplace, add a route and a screen in the code editor, restart, see it in the sidebar.
2496
+ - Add a custom shortcode and let its HTML through the sanitiser (`registerShortcode` + `registerSanitizeRules`).
2497
+ - Give your plugin a Configure form with `settingsSchema`, including a masked secret read at request time.
2498
+ - Plugin roles and permissions: a job-board style `candidate` role confined to its own screens with `roles/*.json` and `admin.allow`.
2499
+ - Ship collections with your plugin and keep them up to date on existing sites with `_lib/schemaSync`.
2500
+ - Declaring dependencies: `requires` vs `uses`, and what the admin sees in the cascade confirmation.
2501
+ - Moving a plugin between sites: Export `.dcmsplugin`, Install from file, the update/downgrade/unsigned prompts, and `requireSignedPlugins`.
2502
+ - Hooking into the platform: a sidebar badge with `countUrl`, a notification source that checks every hour, and a sitemap source.
2503
+ - Understanding licences: rental vs perpetual, grace, "Support ended", and renewing by dropping in a new entitlement.
2504
+ - Free vs Pro: how `supersedes` swaps Blog for Blog Pro (and back again if the licence lapses) without losing data.
2505
+ - Writing a user guide for your plugin: `docs/guide.md` with a `title` and `order`, extra pages as tabs, choosing who can read it with `docs.permission`, and why shortcodes show literally.
2506
+ - Setting `minCmsVersion`: what the installer says to a site on an older CMS, and choosing the right minimum.
2507
+ - Formatting dates in a plugin with `plugins/_lib/admin/ui/dates.js` so they match the admin.
2508
+ - Adding a component from a plugin with `hooks.registerComponent`.
2509
+
2510
+ ### Analytics Pro (`analytics-pro`)
2511
+
2512
+ - Letting a marketing role see the figures (2.3.0): granting the `analytics-pro` permission without handing over settings or IP addresses.
2513
+ - Tracking a campaign end to end: tag links with UTM parameters, then filter by Campaign and Channel and export the visits.
2514
+ - Privacy-first setup: masked IPs, "each day" visitor memory, ignoring your office IP, and erasing a visitor on request (GDPR).
2515
+ - Finding your busiest hours with the heatmap and saving that as a named filter.
2516
+ - Turning on city-level locations and reading the Locations tab.
2517
+
2518
+ ### Backup Pro (`backup-pro`)
2519
+
2520
+ - Before a big change: Back up now with a name, make the change, and restore just the collections you broke.
2521
+ - Reading Activity: what each line means, who "Domma, on schedule" is, and why Domma's restores carry a reason.
2522
+ - Restoring one page or a media folder without touching the rest of the site, and undoing a restore with its safety backup.
2523
+ - Who may restore: giving an office manager Back up but not Restore under System > Roles.
2524
+
2525
+ ### Blog (`blog`)
2526
+
2527
+ - Start a blog in five minutes: enable, add samples, pick a layout and listing display, remove samples.
2528
+ - Setting up a writer/editor workflow with the Blog Author and Blog Editor roles and scheduled publishing.
2529
+ - Customising a post layout block in the Blocks tool and making your own `blog-card-*` display.
2530
+ - Putting the latest posts on the home page with `[blog-grid]` and `[blog-featured]`.
2531
+
2532
+ ### Blog Pro (`blog-pro`)
2533
+
2534
+ - Running two blogs on one site (for example `/news` and `/journal`) with different themes and nav links.
2535
+ - Designing a blog theme: preset, fonts, colours, transitions, then previewing before saving.
2536
+ - Embedding privacy-friendly media: a YouTube facade, a Spotify track, a gallery carousel and a map.
2537
+ - Author pages and an authors index for a multi-writer publication.
2538
+
2539
+ ### Calendar (`calendar`)
2540
+
2541
+ - Publishing a public events page with RSVPs and an iCal feed visitors can subscribe to.
2542
+ - A repeating team meeting shared with a role, with reminders and linked notes.
2543
+ - Inviting a Contacts group to a private event using a private link.
2544
+ - Custom event types and colours for a club or venue.
2545
+
2546
+ ### Contacts Pro (`contacts-pro`)
2547
+
2548
+ - Building a lightweight CRM with custom fields (Tier as a choice, Renewal date, LinkedIn link) and field filters.
2549
+ - Never miss a birthday: dates of birth, the sidebar badge, and the Birthdays layer on the Calendar.
2550
+ - Sharing a client group with a sales role as view-only.
2551
+ - Styling groups with emblems and colours.
2552
+ - Group housekeeping on a shared team (0.7.4): who may restyle, rename or delete a group, and giving one person **Manage groups**.
2553
+
2554
+ ### Data Transfer (`data-transfer`)
2555
+
2556
+ - Moving a growing collection from file storage to MongoDB without losing ids.
2557
+ - A backup routine: labelled backups before a big edit, then a selective merge restore.
2558
+ - Cloning a collection as a template for a new project.
2559
+
2560
+ ### Docs (`docs`)
2561
+
2562
+ - Using Docs as an internal wiki: folders, templates for meeting notes, and Markdown shortcuts.
2563
+ - Sharing a document with a colleague as an Editor, and publishing a read-only link for a client.
2564
+ - Turning a Docs draft into a page or blog post with Send to.
2565
+ - Recovering from a mistake with Version history.
2566
+ - Switching to shared scope for a small team.
2567
+
2568
+ ### Docs Pro (`docs-pro`)
2569
+
2570
+ - Building a staff handbook as a members-only Space: folders as sections, a home page, publishing changes in one go.
2571
+ - A public help centre: Space settings, the sitemap, and linking pages to each other from Docs.
2572
+ - The reader for your readers: Aa settings, Pages layout, Easy read, resume.
2573
+ - Working on a page without readers seeing it: Changes not published, Publish changes, Take down.
2574
+ - Backing up Docs to Google Drive: making your own Google app (publish it!), Connect, Every save.
2575
+ - Keeping a Markdown copy of a handbook in Dropbox or OneDrive for offline reading.
2576
+ - Nightly copies to Cloudflare R2 or Backblaze B2 as a cheap off-site backup.
2577
+ - Getting a tenancy agreement signed in order: tenant first, then landlord; the certificate page explained.
2578
+ - Sharing a price list with a password and an expiry date.
2579
+ - Bringing a folder of Word documents into Docs, and sending one back as .docx.
2580
+ - Quotes and letters from a template with merge fields, filled from Contacts.
2581
+ - Reviewing a policy with comments: comment on words, reply, resolve.
2582
+ - Getting a contract reviewed: share as Suggester, then accept or reject their changes.
2583
+
2584
+ ### Email (`email-pro`)
2585
+
2586
+ - Connecting a mailbox, choosing folders to sync, and setting up identities with signatures.
2587
+ - Inbox zero with rules (test before save), snooze and send later.
2588
+ - Turning an enquiry email into a CMS page or a collection entry, and saving its attachment to Media.
2589
+ - Setting an away message and checking it with the preview.
2590
+
2591
+ ### Feedback (`feedback`)
2592
+
2593
+ - Telling Domma about a bug from your admin, and reading the reply when it comes.
2594
+ - For the Manager: triaging the inbox, replying, and filing a report as a Waypoint Pro issue that the report then follows.
2595
+ - For the Manager's Super Admin: filing your own feedback from the My feedback tab.
2596
+
2597
+ ### Garage (`garage`)
2598
+
2599
+ - Getting a DVLA API key and doing your first lookup.
2600
+ - Fleet reminders: saving company vehicles and using the badge and notifications for MOT/tax.
2601
+ - Showing a garage's stock on a public page with `[garage-vehicles]`.
2602
+
2603
+ ### Invoices (`invoice`)
2604
+
2605
+ - First invoice in ten minutes: issuer with bank details and logo, receiver from Contacts, line items, send as PDF.
2606
+ - Chasing payments: overdue badge, notifications, recording part payments.
2607
+ - Year-end export to Excel for your accountant.
2608
+ - Making your own invoice template.
2609
+ - Quoting a job: send a quote with an accept link, the client accepts online, one click makes the invoice.
2610
+ - Self-billing a supplier: the agreement, SB numbering and the review reminder.
2611
+
2612
+ ### Job Board (`job-board`)
2613
+
2614
+ - Launching a job board: SMTP check, creating a company account and its company, opening candidate sign-up.
2615
+ - The agent workflow: inviting a recruitment agent and having them post a job.
2616
+ - A candidate's journey: sign up, confirm, complete profile, apply, follow status notifications.
2617
+ - Approving companies and suspending one.
2618
+
2619
+ ### Mail Reader (`mail-reader`)
2620
+
2621
+ - Connect a Gmail/Fastmail/own-server mailbox with an app password and test the connection.
2622
+ - Why a read-only reader: privacy defaults (blocked images, sandboxed rendering) explained.
2623
+ - Giving an editor role access to their own mailbox via the `email` permission.
2624
+ - Upgrading from Mail Reader to Email Pro without re-entering passwords.
2625
+
2626
+ ### Messages (`messages`)
2627
+
2628
+ - Setting up team messaging: roles, who can reach whom, and managed groups for departments.
2629
+ - Sharing files in a conversation and how retention affects them.
2630
+ - Keyboard-first messaging: Shift+F10 menus and Ctrl+Enter.
2631
+ - Removing Messages from a role (e.g. members) under Roles.
2632
+
2633
+ ### MongoDB Adapter (`mongodb-adapter`)
2634
+
2635
+ - Moving a site from file storage to a dedicated MongoDB database via the manager.
2636
+ - What changes for plugins that declare MongoDB storage once the adapter is active.
2637
+
2638
+ ### Security (`security`)
2639
+
2640
+ - Reading the health check and fixing each red item (TRUST_PROXY, headers, HTTPS).
2641
+ - What to do when you are locked out: the unlock CLI.
2642
+ - Tuning probe blocking and adding site-specific allowed paths.
2643
+ - Spotting password-reset probing in Sign-ins > Resets.
2644
+
2645
+ ### Security Pro (`security-pro`)
2646
+
2647
+ - Turning on two-factor for yourself, then requiring it for the admin role.
2648
+ - Locking the admin to your own country and setting rate limits for forms.
2649
+ - Using the Audit and Sessions tabs after a suspected account compromise.
2650
+ - Running a fleet from the manager: shared blocks and the Fleet tab.
2651
+
2652
+ ### SEO Pro (`seo-pro`)
2653
+
2654
+ - Connecting Google Search Console with your own Google Cloud client, step by step.
2655
+ - Relaunching a site: importing the old sitemap as redirects.
2656
+ - Adding FAQ and Product rich results and testing them in Google.
2657
+ - Weekly routine: Not found log, Monitor broken links, Content link suggestions.
2658
+
2659
+ ### Booking (`booking`)
2660
+
2661
+ - Taking appointments in ten minutes: a person, a service, the settings and the booking page link.
2662
+ - Rooms and equipment: a meeting room with a cleaning buffer, booked from the `[booking /]` shortcode on a page.
2663
+ - Booking a customer straight from an email or a contact.
2664
+ - Keeping a team's Calendar and bookings from clashing.
2665
+
2666
+ ### Shopping Cart (`shopping-cart`)
2667
+
2668
+ - Opening a shop in ten minutes with the sample products and offline payment.
2669
+ - Connecting Stripe (secret key and webhook) and PayPal sandbox, then a test purchase.
2670
+ - Embedding products in ordinary pages with `[shop-products]` and `[buy-button]`.
2671
+ - Fulfilling, refunding and printing packing slips.
2672
+
2673
+ ### Shopping Cart Pro (`shopping-cart-pro`)
2674
+
2675
+ - Running a launch promotion with a limited, once-per-customer code.
2676
+ - Setting up UK and international zones with weight-based Royal Mail style rates.
2677
+ - Branding order emails and sending a test.
2678
+ - Recovering abandoned checkouts from Insights.
2679
+
2680
+ ### Social Media Pro (`social-media-pro`)
2681
+
2682
+ - Making every page share beautifully: designing the drawn share image.
2683
+ - Connecting Bluesky with an app password and Mastodon with a token.
2684
+ - Auto-drafting a post for each new blog article and reviewing it before sending.
2685
+ - Adding `[share]` and `[follow]` to a blog template.
2686
+ - Posting to LinkedIn (0.2.0): creating your own LinkedIn app, the two products to add, pasting the redirect URI, connecting, and posting as yourself or a company page.
2687
+ - Keeping LinkedIn connected: the 60-day sign-in, the expiry warning and reconnecting in one click.
2688
+
2689
+ ### Campaigns Pro (`campaigns-pro`)
2690
+
2691
+ - A first campaign: audience from Contacts, an offer that closes at 50 claims, and reading the results.
2692
+
2693
+ ### Onboard Pro (`onboard-pro`)
2694
+
2695
+ - Onboarding a new client: read, form, details and a signed agreement, start to finish.
2696
+
2697
+ ### Sheets (`sheets`)
2698
+
2699
+ - A living sales sheet: feed Shop orders in and total them with formulas.
2700
+
2701
+ ### Surveys (`surveys`)
2702
+
2703
+ - Building a customer-satisfaction survey from a Form and inviting a group.
2704
+ - Running an anonymous staff survey - and what anonymity does and does not guarantee.
2705
+ - Automating reminders with an external scheduler and the service token.
2706
+ - Embedding a survey on a page with `[survey]`.
2707
+
2708
+ ### Theme Roller (`theme-roller`)
2709
+
2710
+ - Creating a brand theme from a built-in theme and using it on the site.
2711
+ - Separate admin and site themes, and day/night themes with a custom one.
2712
+ - Exporting a theme's CSS for use elsewhere.
2713
+
2714
+ ### Waypoint Pro (`waypoint-pro`)
2715
+
2716
+ - Setting up a project: a key, a workflow with your own statuses and allowed moves, types and priorities.
2717
+ - Running a two-week sprint: planning from the backlog, the board, and reading the burndown.
2718
+ - Keeping people in the loop: watchers, @mentions, notices and email.
2719
+
2720
+ ---
2721
+
2722
+ ## Appendix B - Doc gaps and code discrepancies
2723
+
2724
+ Found during the sweep. "Doc gap" means docs and code disagree or a feature is undocumented; some
2725
+ entries are genuine code issues.
2726
+
2727
+ ### Verified code issues (checked by hand, 2026-09-27)
2728
+
2729
+ - **Site search leaks role-gated pages** - `server/services/search.js:386` skips only drafts and (fixed in 0.92.1)
2730
+ `visibility: private`; pages gated to a role (and `noindex` pages) appear in anonymous search results with
2731
+ title, description and a snippet.
2732
+ - **`[view]` ignores the view's own access settings** - `processViewBlocks` in `server/services/markdown.js` (fixed in 0.92.1)
2733
+ calls `executeView(slug, …)` with no viewer and never checks `access.roles` / `access.public`, so an embedded
2734
+ view shows its results to anyone who can see the page.
2735
+ - **Collection editor offers roles that do not exist** - `admin/js/lib/collection-editor-arrange.js:27` lists (fixed in 0.92.1)
2736
+ `subscriber`, `editor`, `manager` as API/export access levels and defaults export to `subscriber`; a default
2737
+ install seeds only `super-admin`, `admin`, `user`.
2738
+
2739
+ ### Content, authoring and the public site
2740
+
2741
+ - **Processing order is out of date** in `docs/markdown-shortcodes.md`. Code order: components, `[collection]`, `[view]`, `[block]`/short tags, `[menu]`, `[dconfig]`, effects, plugin shortcodes, tabs, accordion, carousel, countdown, timeline, listgroup, spacer, center, icon, demo, embed, form, hero, table, badge, text, button, link, cta, grid, card/box, banner, slideover, Markdown, sanitise. The doc puts collection/view/form mid-list and omits block, menu, listgroup, demo, embed, text, box and banner. (fixed in docs)
2742
+ - **Undocumented shortcodes** in `docs/markdown-shortcodes.md`: `[banner]` (no section at all), `[listgroup]`/`[item]` (only referenced from the collection section), `[component]` / `<dm-*>` tags, block short tags (`[block-name … /]`), the named celebration tags (`[christmas /]` etc., only in the in-editor reference), and `[scribe]`'s `[redraw]` sub-tag and `[render effect]`. (fixed in docs)
2743
+ - **`[card]` is under-documented**: the doc's table lists `variant="primary"` only and omits `layout` (33 layouts), `image`, `gradient`, `font`, `font-size`, `borderless`, `shadow`, `rounded`, `text-align`, `padding` and the variant set (`clean`, `gradient`, `glass`, `accent`, `dark`, `glow`). (fixed in docs)
2744
+ - **Effect injection onto host tags** (`[card reveal reveal-animation="zoom"]`, hero, banner, col, button, box) is not described in the shortcode doc. (fixed in docs)
2745
+ - **Missing attributes**: `[tabs]` `style="bordered"`, `align`, `fade`; `[grid]` `responsive="false"`; `[hero]` `color`, `min-height`; `[text]` `point-size`. (fixed in docs)
2746
+ - **`[block]` escaping claim**: the doc says all values are HTML-escaped, but `{{{field}}}` passes values through raw; `{{field:FORMAT}}` date formatting is not in the shortcode doc. (fixed in docs)
2747
+ - **FEATURES.md is stale for this area**: `[block name="…"]` should be `template=`; there is no per-page CSS (the page editor's CSS pane edits site-wide `content/custom.css`, which is inlined, not served at `/custom.css`); search covers pages only, not collections or docs; day/night switches at fixed clock times, not sunrise/sunset; nine layouts ship, not three; the theme switcher is built in, not a plugin; "27+ shortcodes" undercounts (around 50 including effects); collections have eight display modes, not five; a custom 404 page must be published, not just dropped in. (fixed in docs)
2748
+ - **`docs/configuration.md` page frontmatter** calls `tags` "cache tags" - they are editorial tags (search, Pages list); cache tags are separate. It lists only three layouts and omits `sortOrder`, `category`, `project`, `theme`, `width`, `sidebar`, `breadcrumbs`, `showInNav` and `bundled`. (fixed in docs)
2749
+ - **`docs/cache.md` key format** (`page:<url>:role<role>`) is outdated: the key also carries file mtime, the sorted set of all viewer roles joined with `+`, and the origin. The tag table omits `sitemap`, and context menus and theme changes invalidate via `site`. (fixed in docs)
2750
+ - **`showInNav` is vestigial**: the editor still offers "Show in Navigation" and it is saved, but nothing on the public site reads it since menus replaced the old navigation. Either remove it or document that navigation is managed in Menus. (documented as legacy in docs/configuration.md; the editor field remains) (the editor field removed in 0.93.0; stored values are kept) (fixed in 68a2b8e)
2751
+ - **Search may expose role-gated pages**: `searchPages()` skips only `status: draft` and `visibility: private`, so pages gated to a role (`member`, `=candidate`, arrays) and `noindex` pages appear in public search results with title, description and snippet for anonymous visitors. The System > Search copy ("Draft and private pages never appear") is literally true but misleading. Worth a code fix before marketing search. (fixed in 0.92.1)
2752
+ - **Reserved names**: `BUILTIN_SHORTCODES` reserves `effect` and `list-group`, neither of which is a shortcode (the real tag is `listgroup`), so a block named `listgroup` would be treated as a short tag. Minor. (fixed in 0.93.0)
2753
+ - **Role-gated pages return 403, drafts 404**: the 403 reveals that a gated page exists; there is no built-in redirect to sign in. Documented in configuration.md but worth stating in user-facing docs. (needs a decision: keep 403, answer 404, or redirect to sign-in)
2754
+
2755
+ ### Structured data
2756
+
2757
+ - **`config/presets.json` is page-layout presets, not collections.** It holds default, landing, blank, article and similar layouts. The built-in collections are defined in `server/services/presetCollections.js`. (not a code bug; CLAUDE.md's config table said "Preset collection schemas" - fixed in 0.93.0)
2758
+ - **Field types are under-documented and inconsistent.** (needs a decision - editor and form-mapping feature work, not a bug fix)
2759
+ - The collection editor offers only 13 types. It cannot create `reference`, `file`, `boolean`, `datetime`, `multiselect` and others, which need hand-edited JSON even though the display, filter and API code supports them.
2760
+ - `docs/configuration.md` lists `text`, `boolean`, `multiselect`, `chooser` and `tags` as schema types but not the editor's own `string`/`tel`/`radio`/`checkbox-group`/`time`/`url`/`hidden`.
2761
+ - Collection-to-form mapping turns every unknown type into a plain text input.
2762
+ - The form editor has no UI for a file field's `accept`/`maxSize`.
2763
+ - **Collection editor offers role names that do not exist by default.** It hard-codes `subscriber`, `editor` and `manager` for API and export access, while the base roles are `super-admin`, `admin` and `user`. (fixed in 0.92.1)
2764
+ - For the API, an unknown role resolves to level Infinity, so any signed-in user passes.
2765
+ - For export, an unknown role fails closed.
2766
+ - The editor's default export access is `subscriber`.
2767
+ - `docs/configuration.md`'s export example uses `"access": "editor"`. (fixed in docs)
2768
+ - **Import is JSON only.** FEATURES.md says "CSV/JSON import & export". Also, the import help text says fields the collection lacks are ignored and mentions a "Data Transfer" tool. Neither is true in the code: `importEntries` stores the data as given, and no Data Transfer feature exists in the repo. Import also skips reference validation. (FEATURES.md claim fixed in docs; the import help text is admin code) (help text fixed in 0.93.0, and import now checks references like any other save; unknown fields are still stored as given)
2769
+ - **Reference integrity is only checked on write.** Deleting a target entry is not blocked and does not cascade. It is handled at display time as "(missing)". (needs a decision: block the delete, cascade, or keep the display-time "(missing)")
2770
+ - **`[view]` docs are stale.** `docs/markdown-shortcodes.md` says `slug` is "the collection slug" and `display` is only `table|cards|list`. The code supports all eight displays, interactive mode, the view's stored display mode and CTA through `action=`. The view editor itself only offers table, list and block. (fixed in docs)
2771
+ - **`[view]` shortcode may bypass view access.** The editor help says roles and Public gate "[view] on pages", but `processViewBlocks` calls `executeView` with no user and no role check. Only `/api/views/:slug/public` enforces `access.roles`/`public`. **Worth verifying: this could expose data.** (fixed in 0.92.1)
2772
+ - **Row-level `reference` mode is not applied to Views.** `docs/configuration.md` says views are checked with `checkEntryAccess()`, but `views.js` only uses `buildRowLevelMatch`, which returns `null` (no filter) for reference mode. (fixed in 0.92.1)
2773
+ - **Transitions.**
2774
+ - `docs/markdown-shortcodes.md` shows `[collection … scope="mine" … transitions /]`, but `transitions` only takes effect in the interactive Browser shell, and `scope="mine"` takes the separate hydration path. (fixed in docs; `docs/recruitment-recipe.md` step 8's `scope="mine"` + `transitions` works since 0.93.0 - an interactive "my entries" block is the Collection Browser with transition buttons) (fixed in 0.93.0)
2775
+ - The action editor help and route comments mention a `[transitions]` shortcode that does not exist. (fixed in 0.93.0)
2776
+ - The docs say an illegal transition returns HTTP 409; both execute routes actually return 400 (the 409 status is lost in the route's mapping). (fixed in 0.93.0)
2777
+ - **Actions docs are incomplete.** `createInCollection` is missing from the action editor's step picker and from the in-admin Actions usage page ("Phase 1 Step Types"). The trigger picker has only `manual`. Nothing in the docs or changelog introduces `[cta]` itself. (fixed in 0.93.0 - `createInCollection` is in the step picker and the usage page; not a bug: `manual` is the only trigger the server implements; `[cta]` is now in docs/markdown-shortcodes.md)
2778
+ - **In-admin docs still say Views need MongoDB** (`usage-views.html`, `api-views.html` mention `cms__views`). This has been wrong since 0.34.0. (fixed in docs)
2779
+ - **The in-admin API reference** has no pages for Forms, Scaffold, `/api/v1`, token mode or `/api/x`, and its Collections page does not mention any of them. `docs/api-reference.md` also has no Forms, Actions or Scaffold sections. (fixed: Forms, Scaffold, External API and API Builder pages)
2780
+ - **`docs/guides/mongodb-storage-adapter.md` is stale.** (fixed in docs)
2781
+ - Its examples use `displayName` (the code uses `title`).
2782
+ - It names a `user-types` preset (now `roles`, `user-profiles` and the eight presets listed above).
2783
+ - It says a restart is needed and that there is no migration, but the editor's Storage tab and `migrate-storage` move entries.
2784
+ - It does not mention the Database connections admin tab or the storage-down notification.
2785
+ - **Preset storage contradiction.** `presetCollections.js` declares `notes-notes` and `todo-items` with `storage.adapter: "mongodb"` so that MongoDB sites keep their rows, but `adapterRegistry.js` forces every preset slug onto the file adapter. Those declarations are never used, and a site that had them in MongoDB would see empty lists. (needs a decision - atlas-learning, ginga-ninja, oneill-leisure and others declare `notes-notes` on MongoDB; honour it, or drop the declaration after moving any rows) (fixed in 0.93.0 - honoured where the site has a working connection and the files hold no rows; a Storage move settles a site with rows in files)
2786
+ - **Scaffolder.**
2787
+ - `docs/scaffolding.md` says a project block tags "blocks, views, pages", but recipes cannot create those. (fixed in docs)
2788
+ - FEATURES.md calls the scaffolder "idempotent on re-apply", but collection, form, action and menu conflicts refuse the whole apply with 409. (fixed in docs)
2789
+ - `docs/recruitment-recipe.md` still refers to `user-types` and to candidate and employer levels 5 and 4 from the old role model. (fixed in docs)
2790
+ - **FEATURES.md** lists "five public display modes" (the code has eight), does not mention the MongoDB requirement for Actions, and lists Form Builder as a premium plugin even though Forms are core. (fixed in docs)
2791
+ - **Changelog gaps.** It has no entries introducing reference fields, file fields, row-level access, storage migration or the timeline, carousel and listgroup displays. It skips 0.6.21-0.22.2, 0.25.15-0.30.0, 0.37.0-0.41.0 and 0.62.0-0.64.2. (fixed: feature history in DommaFeatures.md)
2792
+
2793
+ ### Admin, users, security and operations
2794
+
2795
+ - **`docs/getting-started.md` is stale**: (fixed in docs)
2796
+ - It lists bundled plugins `analytics`, `theme-roller` and `domma-effects`. Analytics is now core, and the other two are not in `plugins/`, which holds blog, mail-reader, security and shopping-cart.
2797
+ - It says to run `npm install` after `npx`, but the scaffolder already runs install, setup and seed.
2798
+ - It says the setup wizard asks for the site title first. The script actually generates secrets, creates the admin, sets title and tagline, then theme, then port.
2799
+ - It describes `npm run reset` as "delete users and clear setup state" and `npm run fresh` as "reset + reseed". The code does a full content and config wipe, and runs reset then setup.
2800
+ - It does not mention the in-browser setup and onboarding wizard.
2801
+ - **`bin/cli.js` inconsistencies**:
2802
+ - The header says `[--seed]` but the flag is `--no-seed`. (fixed in 0.93.0)
2803
+ - It checks for Node 18 or later, but `package.json` `engines` requires Node 24 or later. (fixed in 0.93.0 - the check now reads `engines`; it warned while the fleet ran Node 22 and stops again since the fleet moved to Node 24)
2804
+ - `ENABLED_BY_DEFAULT` names `analytics`, `domma-effects` and `theme-roller`, none of which exist as plugins now, so the list is dead code. (fixed in 0.93.0 - removed; bundled plugins still start switched off)
2805
+ - **`SECURITY.md` is stale**: (fixed in docs)
2806
+ - It says the refresh-token blacklist is in memory and cleared on restart. Since 0.83.0, sessions are file-backed.
2807
+ - It does not mention `TRUST_PROXY`, sessions, audit events, the Security plugin, two-factor or the per-account reset limit.
2808
+ - **Proxy guidance disagrees with the code** - `docs/configuration.md` and `docs/api-reference.md` recommend `TRUST_PROXY=true`. The code and the 0.83.0 changelog recommend `loopback`, because `true` lets visitors spoof IP addresses. (fixed in docs)
2809
+ - **Old role system still in docs** - `docs/structure/user-types.md` describes `userTypes.js` and a `user-types` collection, neither of which exists; the real ones are `roles.js` and the `roles` collection. The in-admin Usage doc (`templates/docs/usage-users-roles.html`) and comments in `server/routes/api/users.js` still describe Manager, Editor and Subscriber roles, but the seeded roles are super-admin, admin and user. (fixed in docs - user-types.md, usage-users-roles.html and api-users.html; the `users.js` code comments fixed in 0.93.0)
2810
+ - **Wrong page title** - The `tools.js` header and the Marketplace help say the switches are at "System > Plugins". The screen is **Marketplace** (`#/plugins`), and the Tool list now includes SEO as well as the four named Tools. (not a bug: the sidebar entry registered in server.js is System > Plugins, the Marketplace screen; the `tools.js` header now names SEO - 0.93.0)
2811
+ - **Two FEATURES.md issues**:
2812
+ - The "Six free plugins included" count does not match the current plugins directory. (fixed in docs)
2813
+ - The Contacts, Notes, Todo, Analytics, notifications, Security, avatars and Tool switches features are missing or understated.
2814
+ - **Undocumented or UI-less features**: (needs a decision - each is a screen or setting to add)
2815
+ - The sessions API (`/api/auth/sessions`) has no core screen; only Security Pro shows sessions.
2816
+ - Health checks are only reachable through `/api/dashboard/summary`.
2817
+ - The `#/pro/docs` route is not linked from the sidebar.
2818
+ - `adminBrand.title` and `server.json` `restartOnPluginToggle` are not in the settings UI.
2819
+ - **Obsolete `INSTANCE_SECRET`** - `managerClient.js` says the INSTANCE_SECRET handshake was removed in favour of `FLEET_TOKEN`, but `setup.js` and `scripts/gen-instance-secret.js` still generate an `INSTANCE_SECRET`. (not a bug: it is still read - `plugins/_lib/mail/secretbox.js` derives the Mail Reader credential key from it, ahead of a key file beside the data - so setup keeps generating it; the comments and `.env.example` that tied it to the manager are fixed in 0.93.0)
2820
+ - **Contacts groups are shared** - Groups are visible to every user even though contacts are private per user. This is deliberate and pinned by a test, but it is not stated anywhere a site owner would read. (stated in the Contacts filter help in 0.93.0) (fixed: manage rule shipped in 0.93.0)
2821
+ - **Password minimum is 8 characters in core** - The core rule is only 8 characters. The stricter rule (10 characters, no common passwords) comes only from the Security plugin. Docs should say which applies. (fixed in docs)
2822
+ - **Permission group order** - `GROUP_ORDER` in `permissionRegistry.js` omits the "Tools" group that the Tool resources use. This is a possible ordering quirk in the role editor. (fixed in 0.93.0)
2823
+ - **The seeded `admin` role holds no Tool permissions** (found 2026-09-27, 0.93.0 sweep) - `BASE_ROLES` in `server/services/roles.js` gives `admin` pages, media, ... theme but not `contacts`, `notes`, `todo`, `analytics` or `seo`, so on a default install only the level-0 role sees those Tools until an admin grants them. 0.93.0 grants `admin` `contacts.manageGroups` once while `admin` may still lack `contacts` itself. (fixed after 0.94.0: `admin` is granted `notes`, `todo`, `contacts`, `analytics` and `seo` once at boot - `grantAdminToolDefaults()` - so a removed grant stays removed; `seo` was already granted once by the SEO Tool when it is on, which every fleet site had)
2824
+ - **Plugin release dates read dd.mm.yyyy** - the 0.93.0 dd/mm/yyyy sweep left `fmtReleased()` in `admin/js/lib/plugins-arrange.js:61` (Marketplace rows) and the banner date in `admin/js/lib/plugin-chrome.js` on dd.mm.yyyy. (not a bug: the code comment says this dotted format was asked for; the "everywhere" wording in the CHANGELOG should be read with it)
2825
+
2826
+ ### Plugins & the Marketplace platform
2827
+
2828
+ - **Code/doc discrepancies (trust the code):**
2829
+ - CHANGELOG 0.68.0 says `POST /api/plugins/entitlement` takes a renewal without reinstalling; **no such route exists** in `server/routes/`. Renewal today means placing the file in `plugins/<slug>/.entitlement.json` or `config/entitlements/` and restarting, or re-installing the envelope as an update. (fixed in 0.93.0 - the CHANGELOG entry is corrected; whether to build the route is a decision)
2830
+ - `dcms-marketplace/README.md` says "The free tier is two plugins" (blog, mail-reader) and that `sync-free-tier.js` "will" exist; the lock file and manifests show **four** free plugins (blog, mail-reader, security, shopping-cart) and the sync script is implemented. (fixed in docs)
2831
+ - `docs/plugin-development.md` "Lifecycle Hooks" says `options.hooks` exposes four mechanisms; it now carries about 20 (see table). `registerCalendarSource`, `registerRelatedItems`, `registerSitemapSource`/`sitemapChanged`, `notify`/`registerNotificationSource`, `registerLoginCheck`/`registerPasswordCheck` and `onEnable`/`onDisable` context are only in the CHANGELOG (the auth events are documented). (fixed in docs)
2832
+ - The transform table lists four hook points; `seo:meta`, `sitemap:entries`, `robots:lines` (0.89.0) are missing. (fixed in docs)
2833
+ - The manifest table documents `inject.head`/`inject.bodyEnd` only; the code also honours `inject.headLate`. `tier`, `closedSource`, `licence`, `admin.css`, `contentPaths`, `settingsSchema` (documented in a later section only), `npmDependencies`, `minCmsVersion` are missing from the optional-fields table. (fixed in docs)
2834
+ - A garbled leftover table fragment and an outdated "Admin UX" paragraph (hiding plugin items by same-text) sit after the Admin Sidebar section and contradict the current `{url, ref: true}` placement model. (fixed in docs)
2835
+ - "Bundled Plugins Reference" lists `theme-roller` (now a paid marketplace plugin) and `domma-effects` (not present); the actual bundled set is blog, mail-reader, security, shopping-cart. (fixed in docs)
2836
+ - The admin view example uses `fetch()` with a hand-built Bearer header and Bootstrap-style classes (`container py-4`), both of which `plugins/CLAUDE.md` and the template's `CLAUDE.md` tell authors not to do. (fixed in docs)
2837
+ - The doc still says "admin Plugins panel"; the screen is the Marketplace. (fixed in docs)
2838
+ - **Undocumented behaviour worth documenting:** `requireSignedPlugins` is not in `docs/configuration.md` (only in the CHANGELOG); the `.dcmsplugin` format, entitlement file format and states, `config/entitlements/` owner wildcards, the 409/423 cascade API, `GET /api/plugins/_count`, `plugins.develop` vs other plugin permissions, and `plugin.public.js` (what it receives) have no user-facing doc page. (`requireSignedPlugins` fixed in docs - configuration.md and plugin-development.md)
2839
+ - **Code observations to confirm:**
2840
+ - `installPlugin()` calls `onEnable(null)` at install time, while enabling calls `onEnable({fastify, services, hooks})`; a plugin that destructures its argument throws at install (caught and logged), and `onEnable` runs twice across install then enable. (fixed in 0.93.0 - install no longer runs it)
2841
+ - `registerComponent` is not handed to plugins in `options.hooks` and no plugin uses it; either expose it or document the direct import. (fixed in 0.93.0 - on `options.hooks`, and it refreshes the page sanitiser's component tag list)
2842
+ - The catalogue build writes a manifest's `npmDependencies` into the catalogue as `requires`, which clashes with the manifest's own `requires` (Tool/plugin dependencies, not carried into the catalogue). Neither `npmDependencies` nor `minCmsVersion` is enforced by the CMS at install. (`minCmsVersion` fixed in 0.93.0 - install and update refuse a plugin that needs a newer CMS; the catalogue key is dcms-marketplace's `build-catalogue.js`; `npmDependencies` enforcement is a decision) (catalogue half fixed: now written as `npmDependencies`)
2843
+ - The design spec says entitlement `versions` ranges refuse out-of-range installs and a `support-ended` perpetual refuses updates at install; `pluginEntitlement.js` does not check `versions`, so any such refusal must happen manager-side. (needs a decision - manager-side today; a CMS-side check would need the range format pinned down)
2844
+ - The CMS does not itself verify `catalogue.json.sig`; a managed site trusts the catalogue from its manager over the fleet token, while each bundle's signature is verified at install. (needs a decision)
2845
+ - There is no manifest field for "requires MongoDB"; Email Pro states it only in its description, so the Marketplace cannot warn before install. (needs a decision - a manifest field plus catalogue and Marketplace UI work in dcms-marketplace)
2846
+ - **Marketing/tutorial gaps:** no public explanation of free vs paid vs Pro, rental vs perpetual licences, managed-only plugins (MongoDB Adapter), or how third parties can submit a plugin. (fixed: Editions & Licences page and docs/editions-and-licences.md)
2847
+
2848
+ ### Analytics Pro (`analytics-pro`)
2849
+
2850
+ - No user-facing README; the only doc is the developer CLAUDE.md. (needs decision: user docs) (fixed: user docs)
2851
+ - Manifest declares no permission, so "who can see analytics" is simply "any signed-in admin user" - not stated anywhere a site owner would read. (fixed: an `analytics-pro` permission, granted to admin - it was any signed-in account)
2852
+ - The printable report is a browser print page (save as PDF), not a server-generated PDF; the menu label "Printable report (PDF)" may suggest otherwise. (fixed)
2853
+
2854
+ ### Blog (`blog`)
2855
+
2856
+ - CLAUDE.md still says the publish gate is a named-role check (`['blog-editor','admin','super-admin']`) and warns not to change it; the code now uses the `blog-posts.publish` permission (1.8.0 section of the same file). (fixed)
2857
+ - No user-facing README; the Settings screen is not listed in the sidebar children (reached by the cog only). (sidebar: by design, the banner cog - noted in CLAUDE.md; README see decisions) (fixed: user docs)
2858
+
2859
+ ### Blog Pro (`blog-pro`)
2860
+
2861
+ - No user-facing README; the displays, heroes and effects lists exist only in code. (needs decision: user docs) (fixed: user docs)
2862
+ - Upgrade path from free Blog (nothing changes until first save; lapse behaviour) is only in CLAUDE.md. (needs decision: user docs) (fixed: user docs)
2863
+ - CLAUDE.md says plugin sub-items do not render in the sidebar, yet plugin.json declares six children - which is true for current core is unclear. (clarified: core reads `items`, not `children`, so they are inert; rendering them is a decision)
2864
+
2865
+ ### Calendar (`calendar`)
2866
+
2867
+ - CLAUDE.md "Ownership gate pattern" shows a role-name check; since 1.6.0 access is by permission. (fixed)
2868
+ - CLAUDE.md "Types are the schema's five" predates 1.5, where types became an editable collection. (fixed)
2869
+ - CLAUDE.md mentions a "Jobs" plugin importing `service.js`; no such plugin exists in the marketplace (Job Board does not use it). (fixed)
2870
+ - The shipped `pages/calendar.md` and `forms/calendar-rsvp.json` are said to be seeded on enable, but `onEnable` seeds only the block and views. (not a bug: core setupPlugin seeds pages/ and forms/ on enable; notes corrected)
2871
+ - No email is sent to people who RSVP (public RSVPs have no account); not stated for site owners. (noted in CLAUDE.md) (fixed: user docs)
2872
+
2873
+ ### Contacts Pro (`contacts-pro`)
2874
+
2875
+ - The Calendar "Birthdays" source (0.5) is missing from the version summary at the top of CLAUDE.md. (fixed)
2876
+ - Display settings are per browser, not per user - worth telling users. (noted in CLAUDE.md; user docs see decisions) (fixed: user docs)
2877
+ - No user-facing README. (needs decision: user docs) (fixed: user docs)
2878
+
2879
+ ### Data Transfer (`data-transfer`)
2880
+
2881
+ - The manifest description says clones keep "every entry's id"; cloning with entries actually assigns new ids (by design, to avoid ambiguous references). Only transfers, backups and restores keep ids. (fixed in docs)
2882
+ - No download/upload of backup files from the admin; backups live only on the server under `data/backups/`. (needs decision: feature) (stated in the plugin guide; the feature decision is still open)
2883
+ - Settings have no admin UI; they are edited in `config/plugins.json`. (needs decision: feature) (stated in the plugin guide; the feature decision is still open)
2884
+
2885
+ ### Docs (`docs`)
2886
+
2887
+ - No README or CLAUDE.md at all. (CLAUDE.md added; user README see decisions) (fixed: user docs)
2888
+ - `config.js` suggests `storage.adapter: 'mongodb'` is supported; the plugin always writes JSON files to `data/` and ignores it. (fixed)
2889
+ - No export (Markdown/HTML/PDF download) and no public publishing of a document. (fixed in 2.0.0: Download, Print / Save as PDF, public links)
2890
+
2891
+ ### Email (`email-pro`)
2892
+
2893
+ - CLAUDE.md covers only notifications and dependencies; there is no feature or setup documentation. (fixed)
2894
+ - Description says "Requires MongoDB", but the plugin loads without it in a reduced read-only mode - behaviour users should know. (fixed)
2895
+ - Display name is "Email" while the slug is `email-pro`; the catalogue shows "Email" (possible confusion with core email settings). (needs decision: display name)
2896
+
2897
+ ### Garage (`garage`)
2898
+
2899
+ - The `[garage-vehicles]` shortcode reads the file store directly (`content/collections/garage-vehicles/data.json`), so it shows nothing when the collection is on MongoDB, although the plugin header says MongoDB is supported. (fixed in 1.4.1)
2900
+ - How to obtain a DVLA key is only a URL in `config.js`. (not a bug: the settings panel explains the key and links the DVLA developer portal) (fixed: user docs)
2901
+
2902
+ ### Invoices (`invoice`)
2903
+
2904
+ - CLAUDE.md route table omits `/outstanding-count` (the sidebar badge) and the Settings > Templates/Branding tabs in its config summary. (fixed)
2905
+ - `config.js` header lists `emailSubject` placeholders without `{{receiver}}`, which CLAUDE.md includes. (fixed)
2906
+ - CLAUDE.md calls Contacts "the Contacts plugin"; it is now a core Tool. (fixed)
2907
+ - PDF needs Chrome on the host - setup steps are only in CLAUDE.md. (needs decision: user docs) (fixed: user docs)
2908
+
2909
+ ### Job Board (`job-board`)
2910
+
2911
+ - `config.js` keys (`emailNotifications`, `applicationStatuses`, `jobTypes`, `industries`) are not read anywhere; changing them has no effect. (fixed)
2912
+ - There is no public job listing page or shortcode - only the `/public/jobs` JSON API; candidates browse jobs inside the admin after signing in. Marketing copy should not imply a public jobs page. (needs decision: public jobs page) (stated in the plugin guide; the feature decision is still open)
2913
+ - No user-facing README. (needs decision: user docs) (fixed: user docs)
2914
+
2915
+ ### Mail Reader (`mail-reader`)
2916
+
2917
+ - CLAUDE.md "Gotchas" says there is "No permission resource, on purpose", but 1.2 declares the `email` permission and every route now checks it. (fixed)
2918
+ - CLAUDE.md's route table omits `/body`, `/diagnostics` and the `/image-senders` routes; trusted image senders are not described at all (the gotcha says the image override is "never persisted", which is true per message but not per sender). (fixed)
2919
+ - Lists `admin/templates/mail.html` and `admin/css/mail.css` as plugin files; they live in `plugins/_lib/admin/mail/`. (fixed)
2920
+ - No user-facing README; single-process only (connection pool is module state) is documented only for developers. (needs decision: user docs) (fixed: user docs)
2921
+
2922
+ ### Messages (`messages`)
2923
+
2924
+ - CLAUDE.md config table says `storage.adapter` defaults to `'mongodb'`; `config.js` has `storage: {}` and the collection schemas declare no adapter. (fixed)
2925
+ - CLAUDE.md describes `editWindowMinutes` as "Seconds after send"; it is minutes. (fixed)
2926
+ - CLAUDE.md still says admin-only routes check `request.user.level <= 1`, contradicting its own later note that admin is decided by effective level. (fixed)
2927
+ - `scope` config key is documented as reserved and ignored; no user-facing docs exist. (key removed; user docs see decisions) (fixed: user docs)
2928
+
2929
+ ### MongoDB Adapter (`mongodb-adapter`)
2930
+
2931
+ - No README or CLAUDE.md at all. (CLAUDE.md added; user README see decisions) (fixed: user docs)
2932
+ - `config.js` says `mongoUri` "must be set", but `plugin.js` never reads it. (fixed)
2933
+ - Core's `connectionManager.js` reads `config/connections.json` itself; nothing in core reads `fastify.mongoConnections`, so the plugin's actual role on a site is unclear from the code. (documented: the plugin is the licence marker; core reads connections.json) (fixed: user docs)
2934
+
2935
+ ### Security (`security`)
2936
+
2937
+ - No user-facing README; the minimum core versions (0.83 for lockout/rules, 0.88 for resets) are only in CLAUDE.md, not in plugin.json. (minCmsVersion fixed; user README see decisions) (fixed: user docs)
2938
+ - CLAUDE.md lists admin tabs as Overview / Sign-ins / Blocked but does not mention the CSV export. (fixed)
2939
+
2940
+ ### Security Pro (`security-pro`)
2941
+
2942
+ - Minimum core versions for two-factor (0.83/0.84) are not in plugin.json. (fixed)
2943
+ - Tab is called "Files" in CLAUDE.md but `integrity` in the route (`#/plugins/security-pro/integrity`). (fixed)
2944
+ - No user-facing README; country data credit (DB-IP) is only mentioned in developer notes. (needs decision: user docs) (DB-IP credit now shown beside the country rules) (fixed: user docs)
2945
+
2946
+ ### SEO Pro (`seo-pro`)
2947
+
2948
+ - Search Console integration is not tested against real Google (CLAUDE.md says so) - worth stating in user docs. (needs decision: user docs) (fixed: user docs)
2949
+ - plugin.json has no minimum CMS version field; the 0.90 requirement is only in the description. (fixed)
2950
+ - No user-facing README; Pro settings live behind the Not found tab, which is not obvious. (needs decision: user docs) (fixed: user docs)
2951
+
2952
+ ### Shopping Cart (`shopping-cart`)
2953
+
2954
+ - No order emails in the free Cart (confirmation and dispatch emails come only with Shopping Cart Pro) - not stated in its own docs. (noted in CLAUDE.md; user docs see decisions) (fixed: user docs)
2955
+ - Refunds are full refunds only; mentioned only in developer notes. (needs decision: user docs) (fixed: user docs)
2956
+ - No user-facing README; `site.baseUrl` needed behind a proxy for payment return addresses is developer-only knowledge. (needs decision: user docs) (fixed: user docs)
2957
+
2958
+ ### Shopping Cart Pro (`shopping-cart-pro`)
2959
+
2960
+ - Email templates require `site.baseUrl` for absolute links and pictures (else the last request's origin) - developer notes only. (needs decision: user docs) (fixed: user docs)
2961
+ - No user-facing README. (needs decision: user docs) (fixed: user docs)
2962
+
2963
+ ### Social Media Pro (`social-media-pro`)
2964
+
2965
+ - plugin.json `uses` lists only `seo`; the blog/blog-pro sources and the Calendar source are not declared. (fixed)
2966
+ - CLAUDE.md says share cards need core 0.89+, but SEO (and its `seo:meta` flow) became core in 0.90. (not a bug: seo:meta is 0.89.0; clarified in CLAUDE.md)
2967
+ - Description says auto-posting is "for every new blog post"; pages can also be a source. (fixed)
2968
+ - The admin route is `#/plugins/social`, not the slug - worth noting in user docs. LinkedIn, Facebook/Instagram and X posting are only planned (LinkedIn posting shipped in 0.2.0). (noted in CLAUDE.md; user docs see decisions) (fixed: user docs)
2969
+
2970
+ ### Surveys (`surveys`)
2971
+
2972
+ - CLAUDE.md "Permission family" gotcha says never to put `permission: "surveys"` on the sidebar item; plugin.json does, and the 1.3.0 section says the sidebar entry needs it. (fixed)
2973
+ - CLAUDE.md headings still call the routes "planned"; they are implemented. (fixed)
2974
+ - There is no delete-survey route or menu item, although the `delete` permission exists (it covers contacts and groups only). (needs decision: what deleting does to responses; noted in CLAUDE.md) (stated in the plugin guide; the feature decision is still open)
2975
+ - Reminders are not sent automatically without an external scheduler and `schedulerToken`; `reminderAfterDays` alone does nothing on its own. (by design; noted in CLAUDE.md) (fixed: user docs)
2976
+ - The scheduler `/run` route ignores `fromName` and always sends as "Surveys". (fixed)
2977
+
2978
+ ### Theme Roller (`theme-roller`)
2979
+
2980
+ - CLAUDE.md does not mention the Custom CSS tab or its Site Settings permission requirement. (fixed)
2981
+ - The public theme switcher cannot offer custom themes - only in developer notes. (needs decision: user docs) (fixed: user docs)
2982
+
2983
+ ### Found while writing the 0.93.0 Documentation (2026-09-27, not yet fixed)
2984
+
2985
+ Each was checked in code by the writer of the page or plugin guide that states it. The guides describe the current behaviour and a workaround where there is one. (needs decision: fix each, or keep and leave documented)
2986
+
2987
+ - **Scaffold apply needs only `collections.create`**, yet a recipe can create roles, users, menus, API tokens and endpoints; a route comment claims a forms permission check that does not exist. (fixed after 0.94.0: apply needs the permission for every kind of thing the recipe creates, refused with 403 naming each before anything is written; recipe roles and seed users must sit below the caller)
2988
+ - **Scaffold options are all slugified** - the onboarding recipe's `hrEmail`/`hrPassword` are mangled (`hr@acme.com` becomes `hr-acme-com`). `wireForm` is never read, and a project the recipe creates is kept when the conflict check then refuses the apply (`docs/scaffolding.md` says a failure writes nothing). (fixed after 0.94.0: options have a `type` and only `slug` ones are slugified - the onboarding email and password survive; the project is created after the conflict check. `wireForm` is still never read)
2989
+ - **Project scope gaps** - creating, updating and deleting a form, and updating and deleting an action, do not check the user's project scope (reads do).
2990
+ - **`limit=0`** on the admin entries list and `/api/v1` becomes the default 50, not "no limit" (API Builder endpoints do treat a saved 0 as no limit).
2991
+ - **Marketplace > Browse hides the `minCmsVersion` refusal** on a managed site: `plugin-marketplace.js` passes only whitelisted messages, so the admin sees "Installation failed". Install from file shows the real reason. (fixed after 0.94.0: the refusal is shown as it is, and the card says "Needs newer CMS" before the click)
2992
+ - **Help text out of step with code** - the view editor says "Admins always can / always see every result" (only level 0 bypasses row rules, and `=role` shuts out level-1 admins); System > Search says searchable pages are "the published, public ones" (since 0.92.1 results follow the searcher's roles).
2993
+ - **Collection API access cannot express an exact `=role`**, which Views and Actions can.
2994
+ - **`/api/navigation` still writes `config/navigation.json`**, which nothing reads (documented as legacy).
2995
+ - **`plugins/_template/plugin.js` comment** builds routes as `${BASE}/items` with `BASE=/api/plugins/<slug>`, doubling the prefix plugins already get.
2996
+ - **Calendar** - the Settings "View" action grants nothing (`GET /settings` needs Administer). The "Opens in" setting sets only the admin Events screen, not the public page.
2997
+ - **Surveys** - (fixed in 1.4.4: "Copy embed shortcode" copied `[survey slug="x"]` without ` /`, and the embed's script was stripped, so the pasted embed did not render; answers were refused from UTC midnight at the START of the closing date while the admin showed it closing at the end - now the end of that day in the site's time zone everywhere; email links used `site.url`, not `site.baseUrl`.) Still open: "remind after 0 days" is treated as 7; Import from Contacts does not skip duplicates and copies only your own contacts though the confirm says "every contact".
2998
+ - **Blog / Blog Pro** - writers without Publish still see Publish and Schedule (the server refuses). Removing a Blog Pro blog always moves its posts to the first blog (only the API lets you choose).
2999
+ - **Shopping Cart Pro help** - the Delivery zones popover says checkout uses "the cheapest service" (shoppers choose when a zone has two or more); SMTP messages point at "Settings > Email" (it is Site Settings > Email tab).
3000
+ - **Docs plugin help** - (fixed in 2.0.0: "Who sees documents" now says everyone with Docs.)
3001
+ - **Invoice help** - still says "the Contacts plugin" (Contacts is a core Tool); the Email tab's placeholder note omits `{{receiver}}`, which works.
3002
+ - **Email Pro** - `/quota` and folder `/export` (mbox) routes exist with no screen that calls them.
3003
+ - **Job Board** - the job closing date is never enforced; company about, website and logo can be set only through the API. 2.0: credits are bought through Stripe only (no PayPal); reviews of a candidate are visible to any agent linked to it, which is by design but worth a look at.
3004
+ - **Messages** - attachments upload through `/api/media`, so a role without Media upload (the default User role) can send text only.
3005
+ - **Seeded `admin` role and the Tool permissions** - see the entry above. (fixed after 0.94.0)