concepts
the markers, the handoff loop, connectors, theme, and the KV-backed TenantConfig.
core concepts
everything in Krispy hangs off a few small ideas. understand these five and the rest of the docs is just detail.
1. the loop (AI โ Telegram โ live handoff)
a visitor message goes to POST /api/chat. the Worker:
- ensures a Telegram forum topic exists for that session (one topic per visitor) and mirrors the visitor's message into it.
- if a human already took over this session, stays silent โ the operator drives.
- otherwise asks the AI (Workers AI by default), parses the control markers, mirrors the AI reply into the topic, and returns the reply to the widget.
the strongly-consistent "a human took over" flag lives in a Durable Object
(SessionDO), not KV โ KV is too eventually-consistent for a switch that must flip
instantly. operator replies are pushed to the visitor's browser over a WebSocket held by
that same DO (hibernatable, so idle sockets cost nothing).
2. the control markers โ [!HANDOFF] and [!FORM:<id>]
the whole product hinges on one convention: the model answers normally, and appends a literal marker at the very end of its reply when it needs to trigger something. the server strips the marker out (the visitor never sees it) and acts on it.
[!HANDOFF]โ the model hit a wall a human should own (pricing negotiation, a complaint, an explicit "talk to a person"). the server shows the human-readable text, flags a handoff, and the widget opens a contact-capture form.[!FORM:<id>]โ orthogonal to handoff. the model offers a concrete next step it can't finish in chat (booking, quote, demo) by naming one of your configured forms. the server resolves the matchingFormSpecand the widget renders it. a reply can raise a form without escalating to a human.
these are parsed independently (a reply can carry neither, either, or both). full grammar: reference โ the markers.
3. connectors โ where a lead goes
a connector is a delivery or CTA channel. four types:
| type | role | delivered server-side? |
|---|---|---|
telegram | drop the lead into the visitor's topic | yes (uses the top-level bot token) |
email | send the lead by email via Resend | yes (silent no-op without a Resend key) |
whatsapp | a wa.me CTA link in the widget | no โ visitor taps it |
instagram | a profile-link CTA in the widget | no โ visitor taps it |
telegram + email are delivery channels (the lead is sent for you). whatsapp +
instagram are CTA-only โ the widget renders them as links; nothing leaves the server.
a form can scope which connectors receive its leads (connectorIds), else all configured
connectors get it.
4. theme โ the widget's appearance
the widget reads a public, secret-free theme projection at boot from
GET /api/widget/config. no build step, no redeploy โ change the theme in KV and the
widget picks it up. themeable: primary/launcher color, position (br/bl), avatar
(buttr default or an https URL), greeting, header title, corner radius, font, and the
notification sound. see embed + theme the widget.
5. tenants + KV config
everything is keyed by a tenantId (default "self"), so the single-tenant self-host and
a future multi-tenant SaaS are the same code. for "self", the Telegram creds come
from Worker secrets; the forms / connectors / theme / prompt overrides live in KV under
tenant:self. any other tenant reads its whole config from KV. the shape is
TenantConfig.
graceful degradation is a feature
Telegram unconfigured โ chat still answers, topic ops no-op. AI provider down โ the Worker hands off to a human rather than dropping the visitor. no Resend key โ email delivery silently skips. nothing in the loop hard-fails on a missing optional dependency.