Back to docs
Guide 06

Widget Integration

Embed the chat widget anywhere.

Il widget embeddabile è un servizio separato (packages/widget, porta 3211) che espone una chat AI sul tuo sito. Architettura: Express + Preact IIFE in iframe sandboxed, proxy SSE trasparente verso il server, sessioni anonime, lead capture, branding per-sito.

Il widget è una feature Enterprise (license widget_enabled); Community ha 1 widget di default ma la creazione richiede il flag attivo.

Come embeddare (quick start)

  1. Crea il widget (admin UI → Settings → Widgets, o API):

    POST /api/widgets
    Authorization: Bearer <admin JWT>
    { "name": "Sito Azienda", "allowedOrigins": ["https://www.azienda.it"],
      "workspaceIds": ["<uuid>"], "primaryColor": "#4c6ef5", "botName": "Assistente" }
    

    Risposta: { widget } con widget.id.

  2. Whitelist knowledge basePUT /api/widgets/:id/workspaces con gli UUID dei workspace il cui RAG sarà accessibile al widget (tabella WidgetWorkspace, PK composita [widgetId, workspaceId]).

  3. Inserisci lo snippet nella pagina host:

    <div id="simmetric-widget" data-widget-id="<widget.id>" data-position="bottom-right"></div>
    <script
      src="https://widget.tuodominio.it/widget/<widget.id>.js"
      integrity="sha384-<HASH>"
      crossorigin="anonymous"
      async
    ></script>
    

Sicurezza: se servi il loader da un CDN di terze parti, genera l'hash SHA-384 del bundle e aggiungi integrity + crossorigin="anonymous" (Subresource Integrity). Se il loader è same-origin con il tuo widget service, SRI è opzionale ma consigliato per difendere da compromissione del CDN/asset.

Il loader (packages/widget/src/routes/loader.ts) legge gli attributi data-*, crea un <iframe sandbox="allow-scripts allow-forms"> (no allow-same-origin, no allow-top-navigation), carica /widget/<id> (HTML) che monta il bundle Preact /widget/app.js.

Configurazione branding & trigger

Campi del modello Widget (validati da widget.schema.ts in shared):

CampoDefaultDescrizione
primaryColor#4c6ef5Colore accento (regex hex)
botNameAI AssistantNome bot
logoUrl / avatarUrlURL immagine (http/https)
welcomeMessage / fallbackMessageMessaggi iniziale/fallback
positionbottom-rightbottom-right/bottom-left
allowedOriginsArray URL (max 50) per CORS dinamico
autoOpenDelay0ms auto-open (0 = disabled, 1–300s)
autoOpenUrlPatternsGlob patterns (**,*,?) per auto-open su URL
exitIntentEnabledfalseApertura su exit-intent
exitIntentCooldownMs60000–86400000 ms
leadCaptureEnabledfalseLead capture
leadCapturePromptPrompt lead (max 500)

Query param URL (?primaryColor=...&position=...&locale=...) override runtime.

Trigger (widget/hooks/useTriggers.ts): ascolta postMessage dal loader — simmetric:urlChange (check autoOpenUrlPatterns via globToRegex), simmetric:exitIntent (open con cooldown). Exit intent rilevato lato loader via mouseleave con e.clientY <= 10.

Autenticazione & API key

  • Widget ↔ Server: header X-Api-Key: <WIDGET_API_KEY> verso /api/internal/widget/*. La key deve matchare tra packages/widget/.env e packages/server/.env.
  • Visitatore ↔ Widget service: header x-session-token (sessione anonima, non JWT).
  • CORS dinamico (widgetCors.ts): header CORS per-origin basati su allowedOrigins. Fail-closed su DB error (no header = browser blocca).

Sessioni anonime

POST /api/sessions (widget) → server crea WidgetSession: token 256-bit hex (crypto.randomBytes(32)), 24h expiry, IP logged. sessionMiddleware valida il token ad ogni richiesta chat e attacca req.widgetSession + req.widgetConfig. A scadenza → 401 → il Preact useWidgetConfig ricrea la sessione transparentemente.

Chat flow (SSE proxy)

Visitatore → POST /api/chat/:widgetId/stream  (x-session-token, body {message, chatId?})
  → sessionMiddleware valida sessione
  → check hourlyRemaining (DB-tracked, 20/h) → 429 se esaurito
  → increment messageCount (fire-and-forget)
  → pre-search RAG: POST /api/internal/widget/search {query, widgetId}
      server risolve workspaceIds dalla whitelist WidgetWorkspace (IDOR prevention)
  → proxy a SERVER_URL/api/workspaces/:workspaceId/chat/stream
      headers: X-Api-Key, X-Widget-Id, X-Widget-Session-Id, X-Accel-Buffering: no
  → relay byte SSE verbatim (responseType: "stream")
  → Preact useWidgetChat parse: token | citations | done(modelUsed,mcpSources,dlp_warning) | error

Il proxy è trasparente: non parse/filtra/trasforma gli eventi SSE. Se la pre-search RAG fallisce, emette status: rag-degraded ma la chat continua senza contesto.

Lead capture

POST /api/lead/:widgetId (widget, sessionMiddleware):

  • Check config.leadCaptureEnabled (403 se off).
  • Body widgetLeadSubmitSchema: email (req), name (opt), transcript[] (role/content/timestamp).
  • submitLead(widgetId, email, name, transcript, session.id) — passa session.id (UUID FK).
  • Limiter 3 leads/hour per IP.
  • Admin recupera: GET /api/widgets/:id/leads, export CSV GET /api/widgets/:id/leads/export (license lead_export).

Rate limit (doppio layer)

LimiterScopeLimite (prod)Window
widgetChatLimiterper widgetId30/min1 min
widgetSessionLimiterper IP5/day24h
widgetLeadLimiterper IP3/hour1h
server hourlyRemainingper sessione20/h1h
server dailyRemainingper sessione5/24h24h

Sicurezza

  • iframe sandbox allow-scripts allow-forms (no same-origin, no top-navigation).
  • helmet rilassato per embedding (no CSP, no frameguard, no COOP/COEP/CORP).
  • Messaggi renderizzati via markdown-it + dompurify (anti-XSS).
  • DLP server-side: dlp_warning nell'SSE done → widget mostra PIIWarningPrompt.
  • No cookie, no localStorage dal sandboxed iframe.
  • Loader usa getAttribute() (no innerHTML) + encodeURIComponent.

MCP (indiretto)

Il widget non gestisce MCP: installa/unpin avviene nel server admin. I workspace linked possono avere MCP tool installati → l'agente li usa durante la chat widget automaticamente (zero codice widget). mcpSources nel SSE done indica quali MCP hanno contribuito.

Estendere

  • Nuovo componentepackages/widget/src/widget/components/ (Preact + Tailwind, no shadcn/ui).
  • Nuovo trigger → estendi useTriggers.ts + campo Widget + route internalWidget.ts.
  • Nuovo rate limitsrc/middleware/rateLimit.ts + mount in index.ts.
  • CSS themable → namespace --widget-* in src/widget/index.css; statiche con Tailwind utility.

File chiave

FileRuolo
packages/widget/src/index.tscreateApp() factory, mount routes
src/routes/loader.tsLoader JS + HTML iframe
src/routes/chat.tsSSE proxy + pre-search RAG
src/routes/session.tsSessione anonima
src/routes/config.tsConfig (cache 5 min)
src/routes/lead.tsLead capture
src/middleware/session.tssessionMiddleware
src/widget/App.tsxRoot Preact, hook coord
src/widget/hooks/{useWidgetChat,useWidgetConfig,useTriggers}.tsState

Cross-link