Django chat
Django chat without Channels, Daphne, or Redis
Most Django chat tutorials start by installing Channels, running Daphne next to your app, and setting up Redis. With TalkJS, you skip all three. The browser connects to TalkJS, which stores and delivers the messages, and your project adds one view that signs a token for request.user.
# chat/views.py
def talkjs_token(request):
if not request.user.is_authenticated:
return JsonResponse({"error": "login required"}, status=401)
token = jwt.encode({
"tokenType": "user",
"iss": settings.TALKJS_APP_ID,
"sub": str(request.user.pk),
"exp": int(time.time()) + 24 * 3600,
}, settings.TALKJS_SECRET_KEY, algorithm="HS256")
return JsonResponse({"token": token})

What does adding chat change in a Django project?
With Channels, you run the realtime part yourself, and your project changes before it stores a single message.
What each approach adds to a Django project
| Part of the project | With TalkJS | With Channels |
|---|---|---|
| Servers | With TalkJSThe WSGI server you run now | With ChannelsDaphne or another ASGI server, alone or beside your WSGI server with a proxy in front |
| Other services | With TalkJSNone | With ChannelsRedis, for channels_redis, the only official channel layer for production |
| Settings | With TalkJSYour TalkJS app ID and secret key | With Channelsdaphne and channels in INSTALLED_APPS, ASGI_APPLICATION, CHANNEL_LAYERS |
| The logged-in user | With TalkJSrequest.user in a normal view | With ChannelsAuthMiddlewareStack, then scope["user"] in each consumer |
| Database access | With TalkJSYour views as they are | With Channelsdatabase_sync_to_async around ORM calls in async consumers |
| History, offline notifications, moderation | With TalkJSIncluded | With ChannelsYours to build |
The WebSocket is the small part of the job. We keep every message with no retention limit. Email, SMS, and mobile push reach people who are offline (SMS is charged separately), and word filters and contact masking come on every plan. Development mode is free, and after launch you pay per monthly active user.
The view, the URL, and the template
Who talks to whom once the chat is in
/talkjs/token/Install PyJWT next to Django with pip install pyjwt, and add TALKJS_APP_ID and TALKJS_SECRET_KEY to your settings, read from the environment. The view signs a token for whoever is logged in:
# chat/views.py
import time
import jwt
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
def talkjs_token(request):
if not request.user.is_authenticated:
return JsonResponse({"error": "login required"}, status=401)
token = jwt.encode(
{
"tokenType": "user",
"iss": settings.TALKJS_APP_ID,
"sub": str(request.user.pk),
"exp": int(time.time()) + 24 * 3600,
},
settings.TALKJS_SECRET_KEY,
algorithm="HS256",
)
return JsonResponse({"token": token})
@login_required
def chat(request, other_user_id):
ids = sorted([str(request.user.pk), str(other_user_id)])
return render(request, "chat/chat.html", {
"app_id": settings.TALKJS_APP_ID,
"other_user_id": other_user_id,
"conversation_id": "dm_" + "_".join(ids),
})Add both to urls.py:
path("chat/<int:other_user_id>/", views.chat),
path("talkjs/token/", views.talkjs_token),The template, chat/templates/chat/chat.html, loads the chat from jsDelivr, passes the token to the session, and opens a conversation between the two users. Use your own display name field in place of get_username:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@talkjs/web-components@0.3.13/default.css">
<script type="importmap" nonce="{{ csp_nonce }}">
{"imports": {
"@talkjs/web-components": "https://cdn.jsdelivr.net/npm/@talkjs/web-components@0.3.13",
"@talkjs/core": "https://cdn.jsdelivr.net/npm/@talkjs/core@1.12.1"
}}
</script>
<script type="module" nonce="{{ csp_nonce }}">
import "@talkjs/web-components";
import { getTalkSession } from "@talkjs/core";
const session = getTalkSession({ appId: "{{ app_id }}", userId: "{{ request.user.pk }}" });
const res = await fetch("/talkjs/token/");
session.setToken((await res.json()).token);
session.currentUser.createIfNotExists({ name: "{{ request.user.get_username|escapejs }}" });
const conversation = session.conversation("{{ conversation_id }}");
conversation.createIfNotExists();
conversation.participant("{{ other_user_id }}").createIfNotExists();
</script>
<t-chatbox
style="width: 400px; height: 600px;"
app-id="{{ app_id }}"
user-id="{{ request.user.pk }}"
conversation-id="{{ conversation_id }}"
></t-chatbox>The other user needs a TalkJS user before they can join: create one when they sign up, from their own session or with the REST API. For a longer walkthrough with a full project, see our Django chat tutorial.
Once the chat loads with a token, turn on authentication in your TalkJS dashboard, so a browser without a valid token can’t connect. Tokens from this view last 24 hours; the advanced authentication guide covers longer sessions.
If you’ve turned on Django 6’s Content Security Policy
Django 6.0 added a built-in Content Security Policy. It’s off until you add ContentSecurityPolicyMiddleware and a SECURE_CSP setting. Once it’s on, a chat loaded from jsDelivr needs a policy that names jsDelivr as well as TalkJS.
Two policies, the same template
talkjs.comResult: No chat. The components and their stylesheet come from cdn.jsdelivr.netSECURE_CSP belowResult: A live chat, with no 'unsafe-inline'@talkjs/ web-components 0.3.13, November 2026.This is the setting that rendered the chat:
# settings.py
from django.utils.csp import CSP
MIDDLEWARE = [
# ...
"django.middleware.csp.ContentSecurityPolicyMiddleware",
]
# Add to TEMPLATES[0]["OPTIONS"]["context_processors"]:
# "django.template.context_processors.csp",
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE, "https://cdn.jsdelivr.net"],
"style-src": [CSP.SELF, "https://cdn.jsdelivr.net"],
"connect-src": [CSP.SELF, "https://*.talkjs.com", "wss://*.talkjs.com"],
"img-src": [CSP.SELF, "https://*.talkjs.com", "data:"],
"frame-src": [CSP.NONE],
}The context processor puts csp_nonce in your templates for the import map and the module script. The components don’t use a frame, so frame-src stays 'none'. File sharing and notification sounds need a few more sources, which the TalkJS CSP page lists. If you serve the TalkJS packages from your own static files instead of jsDelivr, CSP.SELF covers the scripts and the stylesheet.
Because implementing TalkJS was as fast and easy as it was, we were able to greenlight and move this project along very quickly. Had we not chosen this path, the feature would have either been delayed, or shelved due to the resources needed to implement it.
Ready-made chat UI you can customize
You style the pre-built chat UIs with properties, CSS, or a custom theme. Need a chat popup? Open a chatbox from a button of your own.
The TalkJS Inbox shows the chat history on the left, selected conversation on the right. It’s designed to act as the messaging center of your app, usually living on its own page.
The TalkJS Chatbox is the right half of the Inbox UI mode. It takes up less space and is designed to be embedded in your app near an order confirmation, a booking, or user profile.
The user’s conversations in one list. Put a Chatbox next to it to open the one they pick, or use the Inbox, which combines the two.