Your Django app is live, and support currently means a contact form wired to send_mail, plus the occasional user replying to a password reset email and hoping a human sees it. So you search for django live chat and every tutorial wants the same project: install Channels, migrate to ASGI, stand up Redis for the channel layer, write a consumer and a routing file, and then build the inbox where you would actually answer people. That is a multi-week side quest for a feature that is not your product. Here is the ten minute version instead: one script tag in templates/base.html, an AI that answers from your docs, and email fallback for when you are heads down.
Build it with Channels, or embed it?
Django makes real-time chat look buildable because Channels is genuinely good. The tutorial chat app with a WebSocket consumer, an ASGI application, and a Redis channel layer is a classic for a reason. But as a support channel, the socket is maybe 10 percent of the work. The other 90 percent never makes it into tutorials:
- →an inbox where you read, assign, and answer conversations (with auth, search, and state)
- →visitor identity and message persistence across sessions and devices
- →notifications for when you are not staring at the inbox, which is most of the time
- →email fallback for visitors who close the tab before you reply
- →spam filtering and all the abuse cases you have not met yet
If chat is your product, build it with Channels and enjoy every minute. If chat is just how customers reach you, embed a hosted widget and get back to your actual roadmap. The rest of this guide is the embed path with muro: a roughly 6 KB async widget on the front, a shared inbox with AI on the back. No pip install, no ASGI migration, no Redis, no new deployment surface.
Step 1: grab your widget ID
Sign up for muro (14-day free trial, no card needed), create a site for your app, and copy the widget ID from the install screen. Two minutes in the settings pays off later: set the widget color, position, agent name, and avatar so it looks native to your design, and pick from 12+ languages if your audience is not English-first.
Step 2: paste the snippet into templates/base.html
The install is one edit to the layout every page already extends. In a typical project that is templates/base.html, sitting in the project-level template directory you declared in the TEMPLATES setting (usually "DIRS": [BASE_DIR / "templates"]). If your project is organized differently, the right file is simply the one that contains your </head> tag and gets pulled in by {% extends "base.html" %} everywhere else. Paste this right before </head>:
html<script>
(function(w,d,s,o){
w.MuroChat=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
var j=d.createElement(s);j.async=1;j.src='https://muro.chat/widget.js';
d.getElementsByTagName('head')[0].appendChild(j);
})(window,document,'script','muro');
muro('init',{widgetId:'YOUR_WIDGET_ID'});
</script>Replace YOUR_WIDGET_ID, deploy, done. Every template that extends base.html now has chat: the landing page, the dashboard, the billing views, all of it. The script loads async so it does not block rendering, and because it is pure client-side JavaScript it does not care whether you serve WSGI under gunicorn or ASGI under uvicorn. Your requirements.txt does not change by a single line.
One widget ID per environment, none on staging
Hardcoding the widget ID works right up until your staging server starts feeding test chats into your production inbox. The Django-shaped fix is to read the ID from settings and expose it through a tiny context processor:
python# settings.py
import os
MURO_WIDGET_ID = os.environ.get("MURO_WIDGET_ID", "")
# core/context_processors.py
from django.conf import settings
def muro(request):
return {"MURO_WIDGET_ID": settings.MURO_WIDGET_ID}Register "core.context_processors.muro" in the context_processors list under OPTIONS in your TEMPLATES setting, then wrap the snippet in base.html:
html{% if MURO_WIDGET_ID %}
{# the muro snippet from step 2 goes here #}
{% endif %}Inside the snippet, change the init line to muro('init',{widgetId:'{{ MURO_WIDGET_ID }}'}) so the template fills in whatever the environment provides. Production sets the MURO_WIDGET_ID environment variable, staging leaves it unset, and the widget simply does not render there. If you would rather test the full flow before shipping, create a second site in muro instead (the Solo plan covers 2 projects) and give staging its own ID, so test conversations land in their own inbox.
Tidier: keep it in a partial
If you do not want vendor JavaScript inlined in your layout, put the guarded snippet in templates/partials/muro.html and drop {% include "partials/muro.html" %} before </head>. One file owns the whole integration, and future you knows exactly where to look when rotating IDs or removing it.
htmx and CSP wrinkles
Two real-world notes. If you sprinkle htmx over your Django templates, nothing special is needed: the widget loads once in the head, and htmx swaps fragments without full page reloads, so it just keeps working (including with hx-boost). And if you enforce a Content Security Policy through django-csp, allow https://muro.chat in your script sources, and in your connect sources too, since the widget calls home to deliver messages.
Now make it answer without you
Installing the widget is the easy part, same as the WebSocket consumer would have been. The reason to bother is what happens when a visitor types. In the muro dashboard, train the AI by importing a URL: paste your docs or FAQ address and it ingests the content. From then on it answers grounded on your published help articles and product context, not a general-purpose chatbot doing improv on your pricing page. Feed it:
- →your docs or help center URL (re-import when things change)
- →your FAQ page, especially pricing, billing, and account questions
- →product context: what the app does, what the plans include, where the limits are
The part that keeps it trustworthy: when the AI is unsure, it hands the conversation to a human. Refund requests, account problems, and visitors who are clearly angry get escalated instead of answered with confident hallucination. For a solo Django dev that is exactly the right split. The AI absorbs the how-do-I-rotate-my-API-key traffic, and you take the conversations where being human is the feature.
Reply-by-email, for when you are deep in a migration
The failure mode of every chat install is the abandoned bubble: someone writes, nobody answers, and the widget starts doing negative marketing for you. muro's answer for small teams is email. Conversations the AI hands off get forwarded to your inbox, you reply from your mail client, and the reply lands back in the visitor's chat window as if you had been sitting in the dashboard the whole time. No extra tab, no separate app to check.
Add a few saved replies for the questions you answer weekly, an automation rule or two for the repeat patterns, and the support surface of your Django app runs on minutes a day. And because you build things for a living, none of it is a black box: there is a full REST API and webhooks with per-site scoped keys, so you can pipe new conversations into your own admin, a Slack channel, or a management command that runs on cron. The API docs cover all of it.
Ship it
That is the whole job: create a site in muro, paste the snippet into templates/base.html before </head>, add the context processor so staging stays quiet, and point the AI at your docs. The admin excludes itself, every page that extends your base is covered, and your users get answers at 3 a.m. without you writing a single consumer. Poke at the widget in the live demo, then go make the edit to base.html.