@nimbusai/webchat-sdk
v1.1.2
Published
Embeddable WebChat SDK with Shadow DOM isolation and WebSocket messaging
Readme
Nimbus WebChat SDK
A modern, embeddable WebChat SDK with Shadow DOM isolation and real-time WebSocket messaging. Built with TypeScript, Tailwind CSS, and Lucide icons.
Screenshots
Welcome screen
Layout options
Features
- Zero runtime dependencies (Lucide icons are inlined as SVGs)
- Fully customizable UI and theming through configuration
- Responsive design with mobile breakpoint override
- Shadow DOM isolation prevents CSS conflicts with the host page
- Real-time WebSocket messaging with automatic reconnection
- Markdown rendering for text messages (GFM, sanitized with DOMPurify)
- TypeScript support with full type definitions
- Multiple distribution formats (ESM, CommonJS, IIFE/UMD)
- Flexible icons (Lucide icons or custom image URLs)
- Conversation persistence in
localStoragewith optional cross-flow history - Sound + visual notifications when a bot message arrives while the chat is closed
- Character limit with live counter
- Multiple layouts (floating popup or sidepanel, left or right)
- Developer-friendly debug mode with header tooltip, debug snapshot API, and config warnings
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| TypeScript | ^5.7.2 | Type-safe development |
| Tailwind CSS | ^3.4.17 | Utility-first styling (pre-compiled into Shadow DOM) |
| tsup | ^8.3.5 | Bundler (ESM + CJS + IIFE) |
| marked | ^14 | Markdown parser |
| DOMPurify | ^3 | HTML sanitization |
| Vitest | ^2.1.8 | Unit testing |
| Lucide | inline SVGs | Icon library (1000+ icons) |
Installation
npm / yarn / pnpm
npm install @nimbusai/webchat-sdk
# or
yarn add @nimbusai/webchat-sdk
# or
pnpm add @nimbusai/webchat-sdkimport { NimbusChat } from "@nimbusai/webchat-sdk";
const chat = new NimbusChat({
agent_version_id: "550e8400-e29b-41d4-a716-446655440000",
});
chat.open();CDN (script tag)
<script src="https://cdn.nimbus.ai/sdk/nimbus-chat.umd.global.js"></script>
<script>
NimbusChat.init({
agent_version_id: "550e8400-e29b-41d4-a716-446655440000",
});
</script>The CDN bundle exposes a global NimbusChat object with a singleton init() method — safe to call multiple times (subsequent calls return the existing instance).
Quick Start
const chat = new NimbusChat({
agent_version_id: "550e8400-e29b-41d4-a716-446655440000",
style: { position: "bottom-right" }
});The widget renders a floating chat bubble in the corner of the page. Click it to open the chat panel. The WebSocket connection is established lazily — only when the user opens the chat for the first time.
API Reference
Initialization
// NPM usage
const chat = new NimbusChat(config);
// CDN usage
const chat = NimbusChat.init(config);
const instance = NimbusChat.getInstance(); // returns the singleton or nullMethods
| Method | Description |
|--------|-------------|
| chat.open() | Show the chat widget |
| chat.close() | Hide the chat widget |
| chat.toggle() | Toggle widget visibility |
| chat.destroy() | Remove the widget from DOM and disconnect WebSocket. Not reusable afterwards. |
| chat.getDebugInfo() | Returns a full debug snapshot (config, connection state, storage, recent events). Useful in support tickets. |
Configuration Reference
All properties are optional except agent_version_id. The SDK normalizes the config and fills in sensible defaults for everything else.
Null handling. Passing
nullfor any top-level config object (resumeConversation,style,theme,bubble,userMessage,botMessage,header,input,sendButton,welcome,reconnect,waitForReply,isTypingIndicator,notifications) is equivalent to omitting it — defaults are applied in both cases. Sub-fields still require their declared types;nullon a nested string/boolean still throws.
Required
| Property | Type | Description |
|----------|------|-------------|
| agent_version_id | string (UUID) | Required. Identifies the bot/agent version this widget talks to. |
Core settings
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| dns | string | "api.nimbus.ai/api/v1/webchat" | Host (and path) of the WebSocket endpoint. Do not include the protocol — the SDK adds wss:// automatically. |
| flow_id | string (UUID) | none | Forces a specific flow_id. When set, the SDK always reuses it and the "New Chat" button is disabled. |
| test | boolean | false | Appends ?test=True to the WebSocket connection. |
| debug | boolean | false | Enables console logging, auto-opens the widget on init, surfaces the debug tooltip in the header, and emits config warnings. |
| renderMarkdown | boolean | true | Render message text as GitHub-flavored Markdown (sanitized via DOMPurify). |
| allowNewChat | boolean | false | Show the "New Chat" button in the header. Ignored when flow_id is set. |
| container | HTMLElement | none | Optional container to render the widget into (uses absolute positioning inside it). The container must have position: relative (or absolute/fixed) and overflow: hidden. |
style — layout & dimensions
style: {
position: "bottom-right", // "bottom-right" | "bottom-left" | "sidepanel-left" | "sidepanel-right"
width: "380px", // CSS width (e.g. "380px", "100%")
height: "560px", // CSS height — only used in popup mode
font: '"Inter Variable", sans-serif', // Global font family override
background: "#f3f1ef", // CSS color or image URL for the chat body
mobile: {
position: "bottom-right", // Override position on mobile
breakpoint: "480px" // Viewport width to trigger mobile mode
}
}theme — global colors
theme: {
primary: "#ffce1c", // Send button bg, header bg, message bubble bg
secondary: "#f3f1ef" // Foreground/accent color paired with primary
}bubble — floating trigger button
bubble: {
position: "bottom-right", // "bottom-right" | "bottom-left"
autoHide: false, // Hide bubble when chat is open
icon: {
img: "message-circle", // Lucide icon name OR image URL
size: { width: 35, height: 35 }
}
}header
header: {
icon: {
img: "https://example.com/logo.svg",
size: { width: 125, height: 19 }
},
text: {
display: true, // Set to `false` to hide the title entirely (e.g. when the logo already shows the brand)
value: "Support",
color: "#1e293b",
font: "Inter"
},
color: {
primary: "#f3f1ef", // Header background
secondary: "#ffce1c" // Close/new-chat icon color
}
}welcome — empty-state message
welcome: {
display: true,
preTitle: { value: "Welcome to :", text: { color: "#1e293b" } },
title: { value: " Nimbus Chat!", text: { color: "#1e293b" } },
subtitle: { value: "Send a message to start a conversation", text: { color: "gray" } }
}Automatically hidden once the first message lands.
userMessage / botMessage
userMessage: {
background: "#DCF8C6",
width: "80%",
text: { color: "#111B21", font: "", size: 13 },
icon: { img: "user", size: { width: 20, height: 20 } }
}
botMessage: {
background: "#FFFFFF",
width: "80%",
text: { color: "#111B21", font: "", size: 13 },
icon: { img: "bot", size: { width: 20, height: 20 } }
}Pass
icon: nullto hide the avatar; omit theiconkey entirely to use the default Lucide icon.
input
input: {
placeholder: "Ask Nimbus",
expandable: true, // Auto-expanding textarea (up to 8 lines)
text: {
color: "#1e293b",
font: ""
},
background: {
primary: "white", // Input field background
secondary: "#f3f1ef" // Container background
},
maxCharacters: {
limit: 10000,
text: { color: "gray", size: 12 }
}
}sendButton
sendButton: {
align: false, // true = input and button on the same row
icon: {
img: "send",
size: { width: 20, height: 20 }
}
}reconnect
reconnect: {
attempts: 5, // Maximum reconnection attempts
timeout: 5000 // Delay between attempts (ms)
}waitForReply — block input while waiting for the bot
waitForReply: {
timeout: 30000, // Max wait (ms) before re-enabling input. Default: 10000
firstReply: false // Wait for the bot's first message in a new conversation. Default: false
}Omit waitForReply entirely to disable input blocking.
isTypingIndicator
isTypingIndicator: {
position: "bottom", // "top" (header status area) | "bottom" (above input)
title: {
value: "AI Assistant is typing...",
text: { color: "#1e293b", font: "" }
}
}Independent from waitForReply. Omit to disable.
resumeConversation — message persistence in localStorage
resumeConversation: {
enabled: true, // master switch — default true
keepHistory: true // default true: keep messages of past flowIds (so the user can return to them)
// false: wipe past flowIds whenever the current flowId changes
}When enabled is true, the SDK persists messages and per-flow metadata under a single localStorage key. When the user reloads or reopens the widget, messages of the current flow_id are restored automatically.
Default behavior (omitting the field entirely) is { enabled: true, keepHistory: true } — fully on.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| enabled | boolean | true | Master switch for persistence. When false, no messages are saved in localStorage. |
| keepHistory | boolean | true | When true, messages of past flow_ids are preserved. When false, only the current flow's data is kept; switching flow wipes the rest. |
Clicking the in-app New Chat button always clears everything (regardless of
keepHistory) — it represents an explicit user-driven reset.
notifications — sound + visual badge
When a bot message arrives while the chat panel is closed, the SDK plays a sound and shows a counter badge on the bubble.
notifications: {
enabled: true, // Master switch
sound: {
enabled: true,
src: "https://example.com/ping.mp3" // Optional custom audio (.mp3, .wav, .ogg)
},
visual: {
enabled: true,
icon: null, // IconConfig | null (hide the badge icon)
color: {
primary: "#ef4444", // Badge background
secondary: "#ffffff" // Badge foreground
},
text: { color: "#ffffff", size: 11 }
}
}Icon System
The SDK supports two types of icons throughout all configuration:
Lucide icons (recommended)
icon: {
img: "message-circle",
size: { width: 24, height: 24 },
color: "#ffffff"
}Popular Lucide icons:
"message-circle"— chat bubble"send"— send arrow"x"— close"rotate-cw"— refresh / new chat"chevron-down"/"chevron-up"— arrows"user"— user avatar"bot"— bot avatar"info"— info indicator (used by the debug tooltip)
Browse all 1000+ Lucide icons →
Custom image URLs
icon: {
img: "https://example.com/logo.png",
size: { width: 32, height: 32 }
// color is ignored for image URLs
}Hiding icons
icon: null // Hide explicitly
// Omit the `icon` property entirely to keep the default Lucide icon.All six icon fields respect null to hide: bubble.icon, userMessage.icon, botMessage.icon, header.icon, sendButton.icon, notifications.visual.icon.
Shared Type Definitions
interface TextConfig {
display?: boolean;
value?: string;
color?: string;
font?: string;
size?: number; // pixels
}
interface IconConfig {
img?: string; // Lucide icon name OR image URL
size?: { width: number; height: number };
color?: string; // Applied only to Lucide icons (ignored for image URLs)
}
interface ColorPair {
primary?: string;
secondary?: string;
}
interface ChatMessage {
direction: "INBOUND" | "OUTBOUND"; // INBOUND = user, OUTBOUND = bot
content: string;
created_at: number; // epoch seconds
}Debug Mode
Enable comprehensive runtime diagnostics for development and support:
const chat = new NimbusChat({
agent_version_id: "your-uuid",
debug: true
});Debug mode provides:
- Colorized console logs — WebSocket lifecycle, message send/receive, session changes, perf metrics
- Debug tooltip in the header — hover the
iicon (left of the close button) to see Flow ID, created/last-message timestamps, SDK version, agent ID, DNS, connection state, reconnect attempts, message counts, stored flows + storage size - Auto-open — the widget opens automatically on init
- Config warnings — non-fatal misconfiguration warnings emitted to the console at init time
- Performance tracking — when
waitForReply.firstReply: true, logs the time-to-first-bot-message; always logs user→bot response latency
Programmatic debug snapshot
const info = chat.getDebugInfo();
// {
// sdkVersion: "1.0.9",
// config: ResolvedConfig,
// configWarnings: string[],
// connection: {
// state, reconnectAttempts, maxReconnectAttempts,
// lastCloseCode, lastCloseReason,
// lastConnectedAt, lastDisconnectedAt,
// dns, agentVersionId, testMode, url, fixedFlowId
// },
// session: { flowId, messageCount },
// storage: { enabled, keepHistory, activeFlow, sizeBytes, flows: [...] },
// recentEvents: [{ at, name, argsPreview }, ...] // ring buffer of last 50 events
// }Works with the singleton too: NimbusChat.getInstance().getDebugInfo().
Config warnings detected
When debug: true, the SDK warns about common misconfigurations without throwing:
dnscontaining a protocol (ws://,wss://,http://,https://) or a trailing slashflow_idset together withallowNewChat: true(the button is disabled)flow_idset together withresumeConversation.enabled: true(keepHistorybecomes a no-op)theme.primary/theme.secondaryvalues that don't look like valid CSS colorsresumeConversation.keepHistory: falsewhileenabled: false(no effect)
The warnings are also exposed in chat.getDebugInfo().configWarnings regardless of debug mode.
What gets persisted in localStorage
Two keys, both scoped to the host origin:
| Key | Shape | When written |
|---|---|---|
| nimbus_webchat_flow_id | Plain string (UUID) of the active flow | Whenever the server assigns or rotates the flow_id. Independent of resumeConversation. |
| nimbus_webchat_messages | { [flowId]: { created_at, last_message_at, messages: ChatMessage[] } } | Only when resumeConversation.enabled === true. created_at is set once per flow and never updated. last_message_at refreshes on every message. |
No personal data, tokens, or widget configuration are stored.
Development
Prerequisites
- Node.js >= 18
- npm >= 9
Setup
git clone https://github.com/nichemarket/chat-sdk.git
cd chat-sdk
npm installScripts
npm run build # Full build (CSS + TypeScript bundles)
npm run build:css # Compile Tailwind only
npm run build:ts # Compile TypeScript only
npm run type-check # tsc --noEmit
npm run dev # Watch mode
npm test # Run all tests
npm run test:watch # Watch mode testsCommon workflows
# Clean reinstall (for troubleshooting)
rm -rf node_modules package-lock.json && npm install && npm run build
# Local testing of the UMD bundle
npm run build && npx http-server -p 8081 -c-1
# then open one of the examples/*.html filesTest suite
The SDK uses Vitest with jsdom. Tests cover:
| File | Covers |
|---|---|
| tests/types/resolveConfig.test.ts | Config resolution, defaults, deep merging |
| tests/utils/validateConfig.test.ts | Required fields, type validation |
| tests/utils/warnConfig.test.ts | Non-fatal config warnings (dns protocol, flow_id mismatches, theme colors, ...) |
| tests/utils/PerformanceTracker.test.ts | Debug latency metrics (first AI message, response time) |
| tests/core/EventBus.test.ts | Pub/sub, on/off/once/emit, error isolation |
| tests/core/ChatSession.test.ts | Session ID, messages, setMessages, clear |
| tests/core/MessageStorage.test.ts | Per-flow localStorage persistence, metadata, clearOthers, corruption tolerance |
| tests/utils/DOMBuilder.test.ts | Element creation, props, chaining, fragments |
| tests/ui/ThemeManager.test.ts | CSS variables, theme colors, backgrounds |
| tests/utils/IconRenderer.test.ts | Lucide SVG rendering, URL images, unknown icons |
Output files
| Format | Path | Approximate size |
|---|---|---|
| ESM | dist/index.mjs | ~88 KB |
| CJS | dist/index.js | ~88 KB |
| IIFE/UMD | dist/nimbus-chat.umd.global.js | ~577 KB (unminified deps inlined) |
| Types | dist/index.d.ts | ~17 KB |
The SDK version is injected at build time via tsup (define: { __SDK_VERSION__ }) from package.json.
Project structure
chat-sdk/
├── src/
│ ├── core/
│ │ ├── ChatSession.ts # Flow + in-memory messages
│ │ ├── EventBus.ts # Pub/sub + debug ring buffer
│ │ ├── MessageStorage.ts # localStorage persistence (per-flow)
│ │ ├── NotificationManager.ts# Sound + badge counter
│ │ └── WebSocketManager.ts # Connection lifecycle + diagnostics
│ ├── icons/
│ │ └── lucide.ts # Lucide SVG resolution
│ ├── styles/ # Tailwind globals + compiled CSS string
│ ├── types/ # Config interfaces + message types + globals.d.ts
│ ├── ui/
│ │ ├── BaseWindow.ts # Shared window logic
│ │ ├── ChatWindow.ts # Popup variant
│ │ ├── SidepanelWindow.ts # Sidepanel variant
│ │ ├── ShadowContainer.ts # Shadow DOM + CSS injection
│ │ ├── ThemeManager.ts # CSS custom properties
│ │ ├── Header.ts # Header + debug tooltip
│ │ ├── MessageList.ts # Scrollable message container
│ │ ├── MessageBubble.ts # Single message (markdown rendering)
│ │ ├── InputArea.ts # Textarea + send button
│ │ └── ChatBubble.ts # Floating bubble + badge
│ ├── utils/ # DOMBuilder, IconRenderer, logger, validator, ...
│ ├── NimbusChat.ts # Orchestrator + getDebugInfo()
│ └── index.ts # Public exports
├── examples/
│ ├── cdn-usage.html # Light-theme CDN example
│ ├── cdn-usage-black.html # Dark-theme CDN example
│ └── npm-usage.html # npm/TS usage reference
├── dist/ # Build output
└── tests/Architecture
The SDK uses Shadow DOM (open mode) to fully isolate styles from the host page. Tailwind CSS is pre-compiled at build time and injected into the shadow root at runtime alongside CSS custom properties for theming.
Component graph
NimbusChat (orchestrator)
├── EventBus — typed pub/sub, optional ring buffer in debug
├── ChatSession — flow_id + in-memory messages
├── WebSocketManager — connect/reconnect, flow_id storage, diagnostics
├── MessageStorage — per-flow message persistence in localStorage
├── NotificationManager — sound + badge when chat is closed
└── UI components
├── ShadowContainer — Shadow DOM root + injected CSS
├── ThemeManager — CSS variables driven by theme config
├── ChatWindow — popup variant
├── SidepanelWindow — sidepanel variant
├── Header — title, status dot, new-chat, close, debug tooltip
├── MessageList — scrollable container + welcome screen
├── MessageBubble — single bubble (text or markdown)
├── InputArea — textarea + send button + character counter
└── ChatBubble — floating trigger + notification badgeConnection lifecycle
- The user opens the widget → SDK emits
ws:request-connect. - The WebSocket opens at
wss://{dns}?agent_version_id=...&flow_id=...(the storedflow_idis reused if available). - Server replies with
{ "type": "connected", "flow_id": "<uuid>" }. The SDK persists theflow_id. - If
resumeConversation.enabled, the SDK restores stored messages for thisflow_id. - Bidirectional messaging: user →
{ type: "message", direction: "inbound", content }, bot →{ type: "message", direction: "outbound", content }.
Wire schema
// → server
{ "type": "message", "direction": "inbound", "content": "Hello!" }
// ← server (handshake)
{ "type": "connected", "flow_id": "550e8400-e29b-41d4-a716-446655440000" }
// ← server (bot reply)
{ "type": "message", "direction": "outbound", "content": "Hi there!" }WebSocket close codes recognised
| Code | Meaning |
|---|---|
| 1000 | Normal closure |
| 4403 | Session unavailable (server-side) |
The reconnect handler retries up to reconnect.attempts times with reconnect.timeout ms between attempts.
Publishing
The package is published on the public npm registry as @nimbusai/webchat-sdk.
Prerequisites
You need an npm account that is a member of the @nimbusai organization with publish permission. Two-factor authentication is required.
1. Login to npm
npm login
# Then follow the browser prompt to authenticate.
# Verify with:
npm whoamiTo switch to a different registry temporarily:
npm login --registry=https://registry.npmjs.org/2. Build and test before publishing
npm install
npm run type-check
npm test
npm run buildAll three must be green before tagging a release.
3. Bump the version
# Patch release (1.0.9 → 1.0.10) — bug fixes
npm version patch
# Minor release (1.0.9 → 1.1.0) — new features, backwards-compatible
npm version minor
# Major release (1.0.9 → 2.0.0) — breaking changes
npm version majornpm version updates package.json, creates a git commit, and tags it (e.g. v1.0.10). It does not push anything yet.
The version is also baked into the bundle at build time via
tsup(__SDK_VERSION__), so you should re-runnpm run buildafter bumping if you want the dist artifacts to reflect the new version locally.
4. Push the tag
git push origin main
git push origin --tagsIf the repo uses a different default branch, replace main accordingly.
5. Publish to the public registry
# Public access is required for scoped packages on the free tier
npm publish --access publicFor a dry run (no upload, just verification):
npm publish --access public --dry-runOne-shot release flow
# From a clean working tree on the default branch:
npm install
npm run type-check && npm test && npm run build
npm version patch # or minor / major
npm publish --access public
git push origin main --follow-tagsCommon pitfalls
403 Forbidden— you are not authenticated (npm whoamishould return your username) or you lack publish permission on the@nimbusaiscope.E402 Payment Required— when publishing a scoped package without--access publicon the free tier.EEXIST/ version already published — npm forbids overwriting an existing version. Bump the version and republish.OTP required— you have 2FA enabled. Add--otp=<code>or follow the prompt.- Unpublishing — only possible within 72 hours of publishing, and not recommended. Prefer publishing a fixed patch version.
