HomeDocsArchitecture › 12. letchat.ai & the Chatbot Platform

12. letchat.ai & the Chatbot Platform — Shipped Status

As of 2026-06-13. This chapter records what has actually shipped (to the dev environment) across two layers: the chatbot platform features inside rysh-server, and letchat.ai, the branded reseller front that resells that platform. It complements the planning docs under rysh-server/docs/letchat/ (letchat-roadmap.md, rysh-features-for-letchat.md, and the chatbot-*.md design 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 (autonavigator.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 to json.RawMessage with round-trip regression tests. Streaming was also corrected: assistant chunks now arrive as chatbot.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_KEY is 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-only account.get probe, webhook signature verification — mirrors the hand-rolled rysh.Client style.
  • Stripe is the source of truth. checkout.session.completed activates the plan via the shared applyPlan (so all side effects fire) and links the customer/subscription; customer.subscription.deleted → free.
  • Checkout/portal are session-only (API keys blocked); a paid subscribe is rejected with a checkout hint 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_SECRET and STRIPE_ENABLED=true, redeploy. Optional STRIPE_PRICE_IDS to 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-free Sender, SMTP (STARTTLS/implicit-TLS) + log transports, multipart/alternative builder, templated mails.
  • EmailToken model: single-use, expiring, SHA-256-at-rest (verify 24h / reset 1h). User.EmailVerified is 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 Dispatcher keeps 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 real session_created event 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, aggregating chatbot_daily_usages). letchat reads it (GET /api/billing/usage shows 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-plan OverageRateCents (free = hard cap). Gated by STRIPE_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.Workspace resolves :wsID against the caller's mapping and refuses anything not owned (indistinguishable from non-existent — no tenant probing). The :wsID segment 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 need admin; delete/admin-granting stay with the owner. Team-management paths (/members, /invites) are letchat-native, never forwarded.
  • Two credentials, one slot. KeyOrJWT accepts a dashboard JWT or an lck_ API key in the bearer; keys resolve to the owning user so isolation and plan gating are unchanged. SessionOnly blocks 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's EnforceKeyScope to 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_isready healthcheck + 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 behind EMAIL_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.