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:
- Reads
data-bot-keyoff its own<script>tag - Creates an
<iframe>pointed at/widget/{publicKey}on your Deskly deployment - Positions it as a fixed circular bubble (64px) in the bottom-right corner
- Appends it to
document.bodyonce 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.tsxchecks theRefererheader (server-side, on the initial navigation) against the bot'sallowed_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/chatand/api/widget/config/[publicKey]is same-origin to your Deskly deployment, not to the customer's site. TheOriginheader 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
Refererit 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/chatdirectly with a validpublicKey.
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
- Create a bot in
/dashboard/botsand addlocalhost(or your dev domain) to its allowed domains - Drop the script tag into any static HTML file and open it in a browser pointed at your dev server
- If the bubble doesn't appear, check the browser console — the loader logs a clear error if
data-bot-keyis missing
See Troubleshooting for common embed issues.