Embeddable Widget

A widget that breaks a customer's homepage layout is how you lose that customer. Deskly embeds via a sandboxed iframe, not injected DOM — the host page's CSS can never touch the widget, and the widget's CSS can never touch the host page.

The embed snippet

<script src="https://your-deploy-url.com/widget.js" data-bot-key="pk_..." async></script>

public/widget.js is a small, dependency-free loader:

  1. Reads data-bot-key off its own <script> tag
  2. Creates an <iframe> pointed at /widget/{publicKey} on your Deskly deployment
  3. Positions it as a fixed circular bubble (64px) in the bottom-right corner
  4. Appends it to document.body once the DOM is ready

No other JavaScript runs on the host page. The chat UI itself lives entirely inside the iframe.

Bubble → panel resize via postMessage

The iframe starts as a small circular bubble. When the visitor opens the chat, the widget page inside the iframe posts a message to its parent:

window.parent.postMessage(
  { source: "deskly-widget", type: "resize", state: "open" },
  "*"
);

The loader script listens for this on the host page and resizes the iframe element itself — full panel (380×600px) on desktop, full-screen on viewports under 480px wide. The loader validates event.origin matches the iframe's own origin before acting on the message, so an unrelated postMessage on the page can't trigger a resize.

Domain allowlist check

This is deliberately not a hard security boundary — read this carefully before assuming it protects against abuse:

  • At iframe load time, /widget/[publicKey]/page.tsx checks the Referer header (server-side, on the initial navigation) against the bot's allowed_domains. A bot with an empty domain list never renders, regardless of Referer.
  • This check only blocks when a Referer is present and doesn't match — it doesn't block when Referer is simply absent, since some browsers and privacy extensions strip it by default, and blocking on absence would break legitimate installs.
  • Once inside a legitimately-loaded iframe, every subsequent fetch() call to /api/widget/chat and /api/widget/config/[publicKey] is same-origin to your Deskly deployment, not to the customer's site. The Origin header on those requests is always your own domain — so origin checking on the chat API itself would be meaningless, and deliberately isn't done there.
  • A non-browser client (curl, a script) can send any Referer it wants. The domain check stops someone copying a customer's snippet onto their own site as a casual mistake; it does not stop a deliberate attacker from hitting /api/widget/chat directly with a valid publicKey.

The real protection against abuse is rate limiting and the owner's credit balance, not domain checking — see Abuse Protection.

Per-bot branding

GET /api/widget/config/[publicKey] returns displayName, welcomeMessage, and primaryColor for the widget UI to render — configured per bot in /dashboard/bots/[botId], with sane defaults ("Chat Assistant", "Hi! How can I help you today?", #0a0a0c) if left blank.

Testing an embed locally

  1. Create a bot in /dashboard/bots and add localhost (or your dev domain) to its allowed domains
  2. Drop the script tag into any static HTML file and open it in a browser pointed at your dev server
  3. If the bubble doesn't appear, check the browser console — the loader logs a clear error if data-bot-key is missing

See Troubleshooting for common embed issues.