n8n's Chat Trigger node gives you a webhook that accepts a message and returns a reply. It does not give you a UI.
If you're embedding that workflow on a real product, "reply in JSON" isn't good enough. You need a chat window that matches your brand, handles sessions, and doesn't leak your webhook URL to every visitor who opens dev tools. Here's how to actually build that, and where most tutorials stop short.
What n8n's Chat Trigger gives you (and what it doesn't)
The Chat Trigger node turns a workflow into an HTTP endpoint that speaks a simple request/reply contract. That's it. There's no bundled widget, no session UI, no theming. It's a backend primitive, not a frontend.
That's the right design. It means the UI layer is a separate, swappable concern, and you get to decide whether that's a vendor widget, a self-hosted one, or something you write yourself. But it also means every n8n chat tutorial that stops at "paste this webhook URL into the demo widget" has skipped the parts that matter for a real deployment: theming, feature selection, and security.
The webhook contract your UI has to speak
Every n8n Chat Trigger workflow exposes a POST endpoint that expects roughly:
{ "chatInput": "user message", "sessionId": "abc-123" }
It returns something like { "output": "bot reply" }.
Before touching UI code, hit that endpoint with curl and confirm the shape. Theming, streaming, and intake forms all sit on top of this one contract, so get it right first, and you'll spend the rest of the build debugging UI, not guessing at payloads.
Don't hand-roll the chat window
You can build message bubbles, typing indicators, and auto-scroll from scratch, but it's a solved problem. Two solid starting points:
-
@n8n/chat, n8n's own Vue-based embeddable widget. -
ChatFlowGateUI, an open source Svelte port with the same
webhookUrlcontract, MIT licensed, deployable to Cloudflare Workers.
Either way, the pattern is the same: a loader script mounts a toggle button, which opens an iframe pointed at a hosted widget page. The iframe isolation matters. It keeps the host page's CSS from bleeding into your chat UI, and your chat UI's CSS from bleeding into the host page.
<script src="https://ui.chatflowgate.com/loader.bundle.iife.js"></script>
<script>
ChatflowGate.init({
webhookUrl: 'https://your-n8n-instance/webhook/chat',
});
</script>
That snippet mounts a working chat widget wired to your workflow. The rest of this post is about the decisions that turn it from a demo into something you'd actually ship: theming, which features to turn on, and how to stop your webhook URL from being public.
Theming and layout: matching your product without writing CSS
A themeable widget takes branding as config, not stylesheet overrides:
ChatflowGate.init({
webhookUrl: '...',
theme: {
primaryColor: '#0f766e',
avatarUrl: 'https://yoursite.com/bot-avatar.png',
toggleButtonIconUrl: 'https://yoursite.com/chat-icon.svg',
},
themePreset: 'modern', // or business, classic, glassmorphic, neobrutalist...
});
themePreset controls the shape language: border radius, shadows, fonts. It gives you a coherent look without picking twenty individual values by hand. Set theme.* on top of a preset for brand-specific colors and assets.
Layout is a separate decision from theme:
mode: 'window' | 'fullscreen'
window is the familiar floating bubble. fullscreen renders the same chat as a dedicated page, which is what you want for a /support route instead of a widget bolted onto every page. Same webhook, same options, different mount point.
Which features to turn on, and which to skip
Every option below adds a step to the visitor's flow, so don't enable all of them by default:
ChatflowGate.init({
webhookUrl: '...',
enableStreaming: true, // token-by-token replies instead of one blob
loadPreviousSession: true, // resume conversation on return visits
sessionTimeoutMinutes: 30, // drop stale sessions
intakeForm: {
enabled: true,
askPhone: false,
askMessage: true, // capture their first question upfront
},
allowFileUploads: true,
allowedFilesMimeTypes: 'image/*,application/pdf',
enableMessageActions: true, // let users repost/reuse a message
metadata: { plan: 'pro' }, // extra data sent with every webhook call
});
intakeForm deserves a deliberate decision rather than a default. Gating chat behind a name/email form gives your n8n workflow something to key a CRM lookup on.
But it's friction. For a support widget, skip it. For lead-gen, turn it on.
The trade-off most tutorials skip: exposing the raw webhook
If webhookUrl points directly at your n8n instance, anyone who opens dev tools can:
- Read your internal n8n hostname.
- Replay the webhook with arbitrary payloads, with no rate limit.
- Hit it from a script instead of a browser, which opens the door to bots and scrapers.
None of that is hypothetical. It's what "view source" gets you on any site that wires a widget straight to a bare webhook URL.
The fix is a thin server between the widget and n8n:
- The widget calls your endpoint (
/api/chat), not n8n directly. - Your server holds a signed session, rate-limits per visitor, and only then forwards to the real n8n webhook using server-side credentials.
- n8n's URL never appears in client-side JS.
That's the job of a chat gateway, purpose-built as a small reverse proxy rather than general API infrastructure (ChatFlowGate is one implementation). If you're rolling your own, a Cloudflare Worker or a couple of Express routes doing the same job is enough. You don't need a platform for this, just the one hop.
If you'd rather not build and run that gateway yourself, ChatFlowGate hosts this exact layer: a secured, rate-limited proxy in front of your n8n webhook, paired with the widget from this post. Point it at your Chat Trigger and skip the server you'd otherwise have to write and maintain.
Self-hosting for full control
If the hosted widget page isn't enough (custom auth, extra telemetry, a different bundler), override widgetUrl to point at your own deploy of the same widget route instead of the vendor's hosted one:
ChatflowGate.init({
webhookUrl: '...',
widgetUrl: 'https://your-domain.com/widget',
});
Your deploy has to understand the same ChatOptions shape the loader sends across. Build it from the same widget route or component the vendor ships. Reimplementing the contract from scratch is where self-hosted forks quietly drift out of sync with every new option the loader adds.
Putting it together
- Confirm the webhook contract with
curlbefore writing any UI code. - Embed an existing widget (
@n8n/chator ChatFlowGateUI). Don't build message UI from scratch. - Theme it through config:
theme,themePreset,mode. - Turn on only the features (streaming, intake form, uploads) your flow actually uses.
- Put a gateway between the widget and n8n before real users see it.
- Self-host the widget route only if you need control the hosted version can't give you.
That last step is the one that turns a working demo into something safe to embed on a public site, and it's the step most write-ups leave out entirely.
FAQ
Do I need to write my own chat UI for n8n's Chat Trigger?
No. n8n's Chat Trigger only provides the webhook; open source widgets like @n8n/chat or ChatFlowGateUI already speak that contract, so you configure one instead of building message UI from scratch.
Is it safe to put my n8n webhook URL directly in frontend JavaScript?
Not for production. A bare webhookUrl in client code exposes your n8n hostname and lets anyone replay the endpoint with no rate limit. Put a signed, rate-limited gateway in front of it first.
What's the difference between window and fullscreen mode?
window renders a floating chat bubble meant to sit on top of an existing page. fullscreen renders the same chat as a standalone page, better suited to a dedicated /support or /chat route.
Can I self-host the chat widget instead of using a hosted one?
Yes. Override widgetUrl to point at your own deployment of the widget route, as long as that deployment understands the same ChatOptions shape the loader sends.
Should I enable streaming responses?
Enable enableStreaming if your n8n workflow's LLM node supports token streaming and you want replies to appear incrementally. It's a UX improvement, not a requirement, so skip it if your workflow only returns a full response at once.
Top comments (0)