Skip to content

Recipe: helpdesk

Public contact form backend. The helpdesk bundled feature stores questions, contact requests and problem reports from website visitors and notifies every SystemAdmin (mail, Telegram only when the app turns it on). The notification names only the kind, the app and a link into the inbox: no name, address, phone, subject or message. Telegram is a third party where nothing can be shredded or recalled, so it gets the same minimal text. Website senders get a fixed acknowledgement that repeats nothing they typed; signed-in admin reports get a copy. SystemAdmins reply from the admin. Phone is off and Telegram is not mounted unless the app turns them on (fields.phone, telegram: true).

What the recipe demonstrates:

  • Mounting createHelpdeskFeature with its companions (config, delivery, user-data-rights, files, notes-history)
  • The anonymous flow: helpdesk:query:challenge hands out a proof-of-work challenge, helpdesk:write:submit verifies it; spam gets the same 200 body and stores nothing
  • The browser client at GET /helpdesk/client.js (window.KumikoHelpdesk.load() / submit())
samples/recipes/helpdesk/
├── package.json
├── README.md
└── src/
├── feature.ts # createHelpdeskFeature options
└── __tests__/
└── feature.integration.test.ts # challenge query + client route

The behavior (submit, silent drops, replies, Telegram, GDPR export and erasure, retention) is covered by the integration tests next to the feature in packages/bundled-features/src/helpdesk/__tests__/.

Terminal window
bun test --config=bunfig.integration.toml samples/recipes/helpdesk
  1. Mount createHelpdeskFeature({ appName }) plus delivery, a mail channel, user-data-rights, files with a file provider, notes-history and config.
  2. Run kumiko schema generate (new tables, plus the delivery idempotency_key column).
  3. SystemAdmins set helpdesk:config:reply-to in the admin.
  4. Embed /helpdesk/client.js on the website; the form is yours. The client posts to the relative paths /api/query and /api/write without cookies, so the page has to share the app’s origin. A website served by another server forwards /api to the app on its own origin; it can then serve the same script from HELPDESK_CLIENT_SOURCE and type its calls with HelpdeskWebsiteClient, both from @cosmicdrift/kumiko-bundled-features/helpdesk/client. samples/apps/use-all-bundled/public/helpdesk-contact.html (with helpdesk-contact.js) is a complete same-origin page.

Telegram is optional and off by default: notifications go by mail to SystemAdmins only. An app that turns it on (telegram: true) also mounts channel-telegram and secrets for the bot token, and SystemAdmins set helpdesk:config:telegram-chat.


The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):

// Helpdesk sample
//
// A public website form (question, contact request, problem report) that lands with the
// SystemAdmins: mail, optional Telegram, a copy to the sender. Spam is handled without a
// captcha: honeypot field, single-use proof-of-work challenge, minimum submit time, per-IP
// and per-email rate limits. Rejected submits answer 200 with the normal body.
//
// The website embeds the anonymous client the feature serves itself. The client posts to the
// relative paths /api/query and /api/write, so the page must share its origin with the app
// (or forward /api to it on its own origin):
// <script src="/helpdesk/client.js"></script>
// const { kinds, phone } = await window.KumikoHelpdesk.load();
// const result = await window.KumikoHelpdesk.submit({ kind, name, email, message, locale });
// // result.kind: "sent" | "rate-limited" (retryAfterSeconds) | "failed"
//
// Prerequisites the app wires itself:
// - `anonymousAccess` (defaultTenantId = SYSTEM_TENANT_ID) so the form needs no login
// - delivery with a mail channel (channel-telegram only with `telegram: true`, off by default)
// - config, user-data-rights and notes-history (required features)
// - files with a file provider, because the entities carry file fields
// - `kumiko schema generate` for the new tables
import { createHelpdeskFeature } from "@cosmicdrift/kumiko-bundled-features/helpdesk";
export const helpdeskFeature = createHelpdeskFeature({
appName: "Sample App",
fields: { phone: "optional" },
// Lowered only to keep the sample test fast; the default (19 bits, 3 seconds) suits production.
spamProtection: { proofOfWorkBits: 8, minSubmitSeconds: 0 },
});

📄 On GitHub: samples/recipes/helpdesk/src/feature.ts