12. letchat.ai & the Chatbot Platform — Shipped Status
As of 2026-06-13. This chapter records what has actually shipped (to the
devenvironment) across two layers: the chatbot platform features insiderysh-server, and letchat.ai, the branded reseller front that resells that platform. It complements the planning docs underrysh-server/docs/letchat/(letchat-roadmap.md,rysh-features-for-letchat.md, and thechatbot-*.mddesign notes) — those describe intent; this chapter is the consolidated "done" view.
12.1 The resale model
rysh-server implements an embeddable website chatbot: a customer pastes a
widget.js snippet, visitors chat with a tool-less LLM pane, and the operator
gets leads, transcripts, analytics, CSAT and human-takeover. letchat.ai is a
separate product (Go BFF + React dashboard, its own repo at
github.com/letchat-ai/letchat) that resells this: customers register on
letchat, but their workspaces and chatbots are actually provisioned in rysh
under one shared rysh service account. letchat adds its own brand, its own
accounts, billing, and an authorization layer, then forwards everything else
upstream.
graph LR
Visitor["Website visitor
(widget.js)"] -->|white-label runtime| LB[letchat-backend]
Cust["Customer
(dashboard)"] -->|letchat JWT / lck_ key| LB
LB -->|"per-workspace scoped rysh_ key (F3)
/ service bearer for account-level calls
(isolation via user→workspace mapping)"| RS[rysh-server API]
LB --> PGL[(letchat Postgres:
users, mappings, subs,
domains, api keys, audit,
referrals, email tokens, webhooks)]
RS --> PG[(rysh Postgres:
workspaces, chatbots,
sessions, leads, knowledge,
daily usage)]
Staff["letchat staff"] -->|admin role| LB
The single isolation boundary is ProxyHandler in letchat-backend: every
ANY /api/workspaces/:wsID/* is ownership-checked against letchat's
user → rysh workspace mapping before it is streamed to rysh with the service
bearer. A second tier — rysh's per-workspace limit overrides (F1) — lets
rysh enforce per-customer runtime quotas even though every workspace shares one
owner.
12.1b Services the customer receives
A customer interacts with letchat (the branded product — accounts, dashboard, billing) while rysh powers the AI chatbot engine and runtime underneath. Together they deliver:
- Embeddable AI website chatbot — paste a small JS snippet and visitors chat with an LLM assistant on your site.
- Knowledge grounding — the bot answers from your own content, added by pasting text or importing a web-page URL.
- Lead capture — collect and qualify visitor name/email directly in the conversation.
- Human handoff & live takeover — visitors can ask for a person, and your team can watch live sessions, take over, reply, and release.
- Leads CRM — track and triage captured leads with a status workflow and CSV export.
- Analytics — conversations-per-day charts over 7/30/90 days plus ranked top-questions and unanswered-questions lists.
- CSAT feedback — gather per-reply ratings and triage negative feedback.
- Branding & engagement — customize colors, starter-question chips, and proactive "nudge" messages on the widget.
- Multi-language widget — UI auto-detects the visitor's language (en/es/fr/de/tr).
- White-labeling — serve the widget from your own brand and hide the "Powered by" footer on the Pro plan.
- Custom domains — host the chat widget at your own hostname (e.g. chat.yourcompany.com) via DNS verification.
- Chatbot templates — start fast from prebuilt personas (support, sales, FAQ, booking, product guide).
- Guided onboarding — a wizard that sets up workspace → chatbot → knowledge → install and verifies the widget is live on your page.
- Teams / multi-seat — invite teammates with owner / admin / agent roles.
- Customer API keys — programmatic access to the management API with revocable
lck_keys. - Outbound webhooks — receive HMAC-signed event POSTs (new lead, handoff request, session lifecycle) at your own endpoint.
- Alert channels — get notified on new leads or handoff requests via Slack, email, or a webhook connection.
- Account self-service — Google sign-in, profile management, email verification, password reset, and GDPR account deletion.
- Billing & plans — Free/Starter/Pro tiers, monthly or annual (2 months free), with per-plan limits.
- Usage metering & overage — see this month's AI-message consumption against your allowance, with metered overage billing beyond it.
- Runtime quotas — per-plan caps on concurrent sessions, daily AI messages, and transcript retention, enforced automatically.
- Activity / audit log — a per-account feed of sign-ins (including failed attempts) and account/workspace changes.
- Referral program — share a link and earn account credit when the people you refer upgrade to a paid plan.
Where each service is implemented is detailed in §12.2 (rysh F1–F11), §12.3–12.4 (letchat Tracks 1–2), and §12.6b (net-new extensions).
12.2 rysh-server — chatbot-platform features (F1–F11)
These are the rysh-side capabilities the reseller model needed. All shipped to
dev (see rysh-features-for-letchat.md for design detail).
| # | Feature | What it does |
|---|---|---|
| F1 | Per-workspace limit overrides | Nullable limit_* columns on workspaces (NULL = inherit owner plan, 0 = unlimited). ChatbotService.EffectiveLimits is the single resolver at every enforcement point. PATCH /api/workspaces/:id/limits. Lets rysh meter each letchat customer despite the shared owner. |
| F2 | Runtime per-chatbot rate limiting | Sliding 1-min window in ChatbotPaneService on visitor messages and session creation; over-limit drops the turn with a system notice (never an LLM call). |
| F3 | Workspace-scoped management keys | Shipped as workspace-scoped rysh_ API keys confined by EnforceKeyScope to their own /api/workspaces/<id>/* routes. letchat now adopts these (§12.7): one scoped key per customer workspace instead of the global service bearer. |
| F4 | Workspace identity in widget responses | workspace_id in session-create / config responses so a proxy can map chatbot → customer → plan at runtime. |
| F5 | Per-owner default widget branding | users.widget_brand_label/_href (chatbot override → owner default → server default). letchat sets it once on the service account. |
| F6 | Batch workspace fetch | GET /api/workspaces?ids=… — fixes the O(all-customers) dashboard enrichment. |
| F7 | Chatbot event stream (SSE) | Pane service publishes session lifecycle events to ws.{id}.chatbot.events; GET /api/workspaces/:id/chatbot-events exposes them as SSE (no polling). |
| F8 | Admin subscription assignment | PUT /api/admin/users/:id/subscription replaces SQL surgery for plan changes. |
| F9 | Knowledge grounding v1 (+ URL ingestion) | Per-chatbot knowledge docs, chunked + Postgres FTS (OR-term to_tsquery, simple regconfig); top chunks injected into the prompt (pane stays tool-less). POST …/knowledge/url fetches a page (SSRF-guarded: http(s)-only, public IPs, re-guarded redirects, size caps), extracts readable text (script/style stripped, entities unescaped), and feeds it through the same pipeline; surfaced as "Import from a web page" on letchat's Knowledge tab. |
| F10 | Widget UI i18n | Bundled locale strings (en/es/fr/de/tr) selected by LanguageConfig.ui_locale (auto → navigator.language). |
| F11 | Retention cleanup | Daily job deletes chatbot_messages past each workspace's effective chatbot_history_days and drops aged monthly partitions. |
Post-roadmap rysh endpoints (added 2026-06-13, see §12.6b): URL knowledge
ingestion (POST …/knowledge/url, F9 extension) and the monthly AI-message
usage feed (GET /api/workspaces/:id/usage) that powers metered overage
billing.
Critical fix shipped alongside these: the chatbot NATS envelope decoders declared
Payload []byte(base64) against a raw-JSON wire format, silently dropping every widget frame, transcript persist and share-bridge forward. Fixed tojson.RawMessagewith round-trip regression tests. Streaming was also corrected: assistant chunks now arrive aschatbot.stream_chunk/stream_end(one progressively-growing bubble) and persistence accumulates per session, flushing one row on terminal status.
12.3 letchat — Track 1 (net-new reseller features)
Everything letchat had to build itself. All shipped to dev.
| # | Feature | Highlights |
|---|---|---|
| 1.1 | White-label runtime | widget.js, visitor-session endpoints and the session WebSocket proxied through letchat; embed snippet rewritten to the letchat origin (body rewrite — can't override Host upstream without a vhost loop). The widget never exposes rysh. |
| 1.2 | Billing & subscriptions | letchat-side plan catalog (free/starter/pro), per-user subscription, workspace/chatbot gating; plans push F1 limit overrides to rysh. Stripe checkout — see §12.5. |
| 1.3 | Teams / multi-seat | workspace_members + workspace_invites; roles owner(3)/admin(2)/agent(1) via RoleRank; invites activate at signup and email the invitee; role-gated proxy; owner-plan billing binding. |
| 1.4 | Account lifecycle | Google SSO (Firebase) + refresh tokens, profile, change/set password, GDPR delete, email verification + password reset (§12.6). |
| 1.5 | Custom domains | Account-level (white-label plan): claim → DNS verify (ownership TXT + routing CNAME, A-record fallback) → active; embed snippets swap to the owner's active domain; the runtime proxy is host-agnostic; staff force-activate/disable; downgrade disables. TLS/ingress for customer hostnames is deployment work. |
| 1.6 | Operator back-office | Stats, customer search, plan/role assignment (full side effects), account deletion; LETCHAT_ADMIN_EMAILS bootstrap; JWT-only AdminOnly middleware. |
| 1.7 | Customer API keys | lck_ bearers (SHA-256 at rest, one-time reveal, 10/account, last-used). Accepted across the management API as the owning user; SessionOnly lockdown blocks account/billing/key-mgmt/back-office (a leaked automation key can't take over the account). |
| 1.8 | Onboarding wizard | 4-step guided setup (workspace → chatbot+origin → knowledge → install); captures the one-time full widget key into a copy-ready snippet; SSRF-guarded POST /api/install-check verifies the widget live on the customer's page. |
| 1.9 | Audit log + referrals | Append-only per-account feed (actor user/api_key/staff, IP, 180-day retention) over auth/account/billing/workspace/team/domain/key/chatbot events; cursor-paginated feed + staff cross-account view. Referral program: stable share codes, ?ref= attribution, $10 deferred-credit ledger per paid conversion (capped, idempotent). |
12.4 letchat — Track 2 (surfacing rysh capability)
Mostly dashboard work over rysh endpoints the proxy already forwards. All
shipped to dev.
| # | Feature | letchat surface |
|---|---|---|
| 2.1 | Live sessions + takeover | SSE live list, transcript, takeover/reply/release. |
| 2.2 | Leads CRM + alert channels | Leads CRM-lite; Alerts tab: workspace notification channels (schema-driven create from rysh's connection types) + per-chatbot notify targets stored in the handoff JSON. |
| 2.3 | Rich analytics | Conversations/day chart (7/30/90d) + ranked Top questions / Unanswered lists. |
| 2.4 | Branding & engagement | Full form: brand identity, theme colors, starter chips, proactive rules (triggers + cap), language set — parse-edit-reserialize so unknown keys survive. |
| 2.5 | CSAT triage | Negative-only feedback filter. |
| 2.6 | Knowledge grounding | Knowledge tab (add/list/delete docs) over F9. |
| 2.7 | Chatbot templates | 5 prebuilt personas (support/sales/FAQ/booking/product-guide); picker in onboarding + apply-in-Settings. |
Deploy dependency (2.2): rysh registers its External Connections routes only when
RYSH_ENCRYPTION_KEYis set. Added to the rysh dev env on 2026-06-13; prod already had it.
12.5 Payments — Stripe checkout (scaffolded, gated off on dev)
The full checkout path is built but payments are OFF on dev behind an
explicit guard, so a live key can sit configured without any charge being
possible. payments_enabled = STRIPE_ENABLED && key-present.
sequenceDiagram
autonumber
participant U as Customer
participant LB as letchat-backend
participant S as Stripe
U->>LB: POST /api/billing/checkout {plan}
Note over LB: 503 unless STRIPE_ENABLED && key set
LB->>S: create Checkout Session (inline price_data or STRIPE_PRICE_IDS)
S-->>LB: session url
LB-->>U: { url } → redirect
U->>S: pays on Stripe-hosted page
S->>LB: POST /api/stripe/webhook (signed)
Note over LB: HMAC-SHA256 verify (t.payload, 5-min tolerance)
LB->>LB: applyPlan() → rysh limit sync + white-label + referral conversion
- Thin, dependency-free client (
internal/stripe): checkout-session + billing-portal creation, read-onlyaccount.getprobe, webhook signature verification — mirrors the hand-rolledrysh.Clientstyle. - Stripe is the source of truth.
checkout.session.completedactivates the plan via the sharedapplyPlan(so all side effects fire) and links the customer/subscription;customer.subscription.deleted→ free. - Checkout/portal are session-only (API keys blocked); a paid
subscribeis rejected with acheckouthint when payments are live; free downgrades still switch directly. - Verified on dev: live key validated read-only (no charge), gated behavior confirmed (config off, checkout/portal → 503, webhook rejects forged sigs).
- To go live: register the webhook endpoint, set
STRIPE_WEBHOOK_SECRETandSTRIPE_ENABLED=true, redeploy. OptionalSTRIPE_PRICE_IDSto use named Prices instead of inline ones.
12.6 Transactional email — SMTP (scaffolded, gated off on dev)
Verification, password reset, and team-invite emails. Email is OFF on dev
behind EMAIL_ENABLED; when off, a log transport records each email's
subject + recipient + action link instead of sending — every flow stays
testable and login is never blocked on verification (the "works without
email" principle).
internal/email: dependency-freeSender, SMTP (STARTTLS/implicit-TLS) + log transports,multipart/alternativebuilder, templated mails.EmailTokenmodel: single-use, expiring, SHA-256-at-rest (verify 24h / reset 1h).User.EmailVerifiedis informational; social signups are created already verified.- Endpoints:
verify-email,forgot-password(never reveals whether an email exists),reset-password(revokes sessions),resend-verification(session-only). Frontend: verify/forgot/reset pages + a dismissible dashboard verify banner. - To go live: set
SMTP_HOST/PORT/USERNAME/PASSWORD+EMAIL_FROM,EMAIL_ENABLED=true, redeploy.
12.6b Net-new extensions (post-roadmap, 2026-06-13)
Built on top of the shipped roadmap:
- URL knowledge ingestion (rysh F9 extension) — see §12.2.
- Outbound customer webhooks (letchat) — customers register HTTP endpoints
that receive HMAC-signed chatbot events. A background
Dispatcherkeeps one rysh chatbot-events SSE consumer (F7) per workspace that has an enabled webhook and POSTs matching events (X-Letchat-Signature: sha256=HMAC,X-Letchat-Event), recording every attempt. Session-only CRUD, SSRF-guarded URLs, per-webhook delivery log. Verified live (a realsession_createdevent delivered + recorded). - Annual billing (letchat) — paid plans gain a yearly price (2 months free)
carried through the gated Stripe checkout (
interval=year, named-Price override<plan>_year); a Monthly/Annual toggle on the dashboard. - Metered overage charging (rysh + letchat) — the full chain is wired.
rysh exposes a monthly AI-message usage feed
(
GET /api/workspaces/:id/usage, aggregatingchatbot_daily_usages). letchat reads it (GET /api/billing/usageshows used vs included + cost on the dashboard) and a daily sweep bills accrued overage as Stripe invoice items — increment-only (current − already-reported), idempotent (Idempotency-Key = overage:<user>:<period>:<overage>), reset at month rollover. Per-planOverageRateCents(free = hard cap). Gated bySTRIPE_ENABLED: on dev the sweep logs the would-be charge and advances the counter. Verified live end-to-end (seeded 60.5k msgs → $5.00 overage computed, swept, and persisted).
12.7 Security & isolation model
- Proxy boundary.
ProxyHandler.Workspaceresolves:wsIDagainst the caller's mapping and refuses anything not owned (indistinguishable from non-existent — no tenant probing). The:wsIDsegment is canonicalized to the rysh workspace id before forwarding (rysh routes that resolve only ids would 404 on names). - Role gate. Reads/agent actions need
agent; most writes needadmin; delete/admin-granting stay with theowner. Team-management paths (/members,/invites) are letchat-native, never forwarded. - Two credentials, one slot.
KeyOrJWTaccepts a dashboard JWT or anlck_API key in the bearer; keys resolve to the owning user so isolation and plan gating are unchanged.SessionOnlyblocks keys from account-takeover / escalation surfaces; the back-office is JWT-only. - Webhook trust. The Stripe webhook is the only public state-changing route; it accepts nothing without a valid HMAC signature.
- Secrets at rest. API keys, email tokens, and refresh tokens are stored as
SHA-256 hashes; Stripe/SMTP secrets live only in the gitignored
.env(compose carries${VAR:-}placeholders). - Scoped upstream credentials (F3 adopted). The proxy no longer forwards
every customer with the global rysh service bearer: each workspace gets a
workspace-scoped
rysh_key (minted at creation, lazily for legacy mappings), confined by rysh'sEnforceKeyScopeto its own/api/workspaces/<id>/*routes. A leaked per-workspace key exposes one customer, not all tenants; a 401 on a scoped key drops it and falls back to the service bearer. Account-level calls still use the service bearer.
Production hardening (shipped 2026-06-13)
- PostgreSQL store. The deployed letchat runs entirely on Postgres (compose
letchat_postgres+pg_isreadyhealthcheck + persistent volume; GORM postgres driver). SQLite remains only as the in-memory unit-test dependency. - Readiness
/health. Pings the store and rysh upstream; returns 503 + a per-check body when a dependency is down, so a degraded upstream no longer reads as healthy. - F3 scoped keys (above).
12.8 Deployment & feature flags
letchat runs as its own docker compose stack (letchat_backend +
letchat_web) behind the host nginx vhost dev.letchat.ai
(127.0.6.252:34080). The capability guards:
| Flag (env) | Default (dev) | Gates |
|---|---|---|
STRIPE_ENABLED (+ STRIPE_SECRET_KEY) |
off | Real checkout / charges. Off ⇒ checkout/portal 503, free switch-plan path kept. |
STRIPE_WEBHOOK_SECRET |
unset | Webhook acceptance (signature verify). |
EMAIL_ENABLED (+ SMTP_*, EMAIL_FROM) |
off | Real email send. Off ⇒ log transport prints links. |
RYSH_ENCRYPTION_KEY (rysh-server) |
set on dev | rysh External Connections routes ⇒ letchat Alerts tab (2.2). |
FIREBASE_PROJECT_ID |
set on dev | Google social login. |
LETCHAT_ADMIN_EMAILS |
set | Back-office admin bootstrap. |
LETCHAT_DB_HOST (+ LETCHAT_DB_*) |
set (letchat_postgres) |
Store: PostgreSQL. Unset ⇒ SQLite fallback (tests only). |
12.9 Status summary
Everything below is shipped to dev and verified (Go tests + live browser/API
checks on dev.letchat.ai and dev.tangolife.london).
- rysh chatbot platform — F1–F11 (§12.2), plus the post-roadmap URL knowledge ingestion and the monthly usage feed.
- letchat Track 1 — 1.1–1.9 (§12.3): white-label runtime, billing, teams,
account lifecycle, custom domains, back-office, API keys, onboarding, audit
- referrals.
- letchat Track 2 — 2.1–2.7 (§12.4): live sessions, leads + alert channels, analytics, branding/engagement, CSAT, knowledge, templates.
- Net-new extensions (§12.6b) — URL knowledge ingestion, outbound customer webhooks, annual billing, metered overage charging (rysh usage feed → letchat idempotent sweep → Stripe invoice items).
- Production hardening (§12.7) — PostgreSQL store, readiness
/health, F3 workspace-scoped keys adopted. - Gated, go-live-by-config — Stripe (checkout, webhook, billing portal,
annual, overage charging) behind
STRIPE_ENABLED; SMTP email behindEMAIL_ENABLED. Both fully built and verified; no code change to enable.
Open items (operational, not code): flip STRIPE_ENABLED (+ webhook
secret) and EMAIL_ENABLED (+ SMTP creds) to go live; reconcile the Stripe
account currency (GBP) with the USD plan catalog before charging.