Full chat room example¶
This example shows how to build a simple chat room using Django-AWS-API-Gateway-WebSockets.
The example includes:
an HTML client using
reconnecting-websocket;fetching historic messages after the WebSocket connects;
sending a chat message from the browser;
broadcasting messages to all users in the same room;
changing rooms by changing the WebSocket channel;
broadcasting a maintenance message to all connected users from the Django shell.
Overview¶
In this example, each chat room is represented by a WebSocket channel.
For example:
general
support
random
When a browser connects to:
wss://ws.example.com?channel=general
the connection is associated with the general channel.
When a user changes room, the browser closes the current WebSocket connection
and opens a new one with a different channel query string value.
Example assumptions¶
This example assumes:
the package is installed and configured;
an API Gateway WebSocket endpoint has been created;
your WebSocket endpoint is available at
wss://ws.example.com;WebSocket token protection is disabled for simplicity;
your Django app is called
chat;the WebSocket route selection key is
action;messages sent with
"action": "chat_message"are handled by thechat_messagemethod on the view;messages sent with
"action": "fetch_history"are handled by thefetch_historymethod on the view;messages sent with
"action": "change_channel"are handled by thechange_channelmethod on the view.
For production deployments, you should also enable authentication, authorisation, input validation, rate limiting, and WebSocket token protection.
Django model¶
Create a simple model to store historic chat messages.
# chat/models.py
from django.conf import settings
from django.db import models
class ChatMessage(models.Model):
room = models.CharField(max_length=100, db_index=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
)
username = models.CharField(max_length=150)
message = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]
def __str__(self):
return f"[{self.room}] {self.username}: {self.message[:50]}"
Create and apply migrations:
python manage.py makemigrations chat
python manage.py migrate
Django WebSocket view¶
Create a WebSocket view that can:
send historic messages to the current connection;
receive a chat message;
store the message;
broadcast the message to all active connections in the same channel.
# chat/views.py
from django.http import JsonResponse
from django.utils import timezone
from django_aws_api_gateway_websockets.models import WebSocketSession
from django_aws_api_gateway_websockets.views import WebSocketView
from .models import ChatMessage
class ChatWebSocketView(WebSocketView):
def fetch_history(self, request, *args, **kwargs):
room = self.websocket_session.channel_name
messages = ChatMessage.objects.filter(room=room).order_by("-created_at")[:50]
messages = reversed(messages)
self.websocket_session.send_message(
{
"type": "history",
"room": room,
"messages": [
{
"username": message.username,
"message": message.message,
"created_at": message.created_at.isoformat(),
}
for message in messages
],
}
)
return JsonResponse({"ok": True})
def change_channel(self, request, *args, **kwargs):
super().change_channel(request, *args, **kwargs)
return JsonResponse(
{
"ok": True,
"type": "channel_changed",
"channel": self.websocket_session.channel_name,
}
)
def chat_message(self, request, *args, **kwargs):
room = self.websocket_session.channel_name
message_text = str(self.body.get("message", "")).strip()
if not message_text:
return JsonResponse(
{
"ok": False,
"error": "Message cannot be empty.",
},
status=400,
)
user = request.user if request.user.is_authenticated else None
username = user.get_username() if user else "Anonymous"
message = ChatMessage.objects.create(
room=room,
user=user,
username=username,
message=message_text,
)
WebSocketSession.objects.filter(channel_name=room).send_message(
{
"type": "chat_message",
"room": room,
"username": message.username,
"message": message.message,
"created_at": message.created_at.isoformat(),
}
)
return JsonResponse({"ok": True})
def default(self, request, *args, **kwargs):
return JsonResponse(
{
"ok": False,
"error": "Unknown WebSocket action.",
},
status=400,
)
URL route¶
Add a URL pattern for API Gateway to call.
The slug parameter must be named route.
# chat/urls.py
from django.urls import path
from .views import ChatWebSocketView
urlpatterns = [
path(
"ws/chat/<slug:route>",
ChatWebSocketView.as_view(),
name="chat_websocket",
),
]
Include the app URLs from your project URL configuration if required.
# project/urls.py
from django.urls import include
from django.urls import path
urlpatterns = [
path("", include("chat.urls")),
]
API Gateway target endpoint¶
When creating the API Gateway record, set the target base endpoint to the URL
without the route slug.
For example, if your Django route is:
https://www.example.com/ws/chat/<slug:route>
then the API Gateway target base endpoint should be:
https://www.example.com/ws/chat/
The package appends the route value when configuring API Gateway.
HTML client¶
The following HTML page connects to a chat room, fetches historic messages when the connection opens, sends messages, and allows the user to change rooms.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebSocket chat room example</title>
<style>
body {
font-family: sans-serif;
margin: 2rem;
}
#messages {
border: 1px solid #ccc;
height: 300px;
overflow-y: auto;
padding: 1rem;
margin-bottom: 1rem;
}
.message {
margin-bottom: 0.5rem;
}
.system {
color: #b45309;
font-weight: bold;
}
.meta {
color: #666;
font-size: 0.8rem;
}
</style>
</head>
<body>
<h1>Chat room</h1>
<p>
Current room:
<strong id="current-room">general</strong>
</p>
<label for="room-select">Change room</label>
<select id="room-select">
<option value="general">general</option>
<option value="support">support</option>
<option value="random">random</option>
</select>
<hr>
<div id="messages"></div>
<form id="chat-form">
<input
id="message-input"
type="text"
placeholder="Type your message"
autocomplete="off"
required
>
<button type="submit">Send</button>
</form>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js"
integrity="sha512-B4skI5FiLurS86aioJx9VfozI1wjqrn6aTdJH+YQUmCZum/ZibPBTX55k5d9XM6EsKePDInkLVrN7vPmJxc1qA=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<script>
const websocketBaseUrl = "wss://ws.example.com";
const currentRoomElement = document.getElementById("current-room");
const roomSelect = document.getElementById("room-select");
const messagesElement = document.getElementById("messages");
const chatForm = document.getElementById("chat-form");
const messageInput = document.getElementById("message-input");
let currentRoom = roomSelect.value;
let socket = null;
function appendMessage(username, message, createdAt) {
const row = document.createElement("div");
row.className = "message";
const meta = document.createElement("div");
meta.className = "meta";
meta.textContent = `${username} · ${createdAt || ""}`;
const body = document.createElement("div");
body.textContent = message;
row.appendChild(meta);
row.appendChild(body);
messagesElement.appendChild(row);
messagesElement.scrollTop = messagesElement.scrollHeight;
}
function appendSystemMessage(message) {
const row = document.createElement("div");
row.className = "message system";
row.textContent = message;
messagesElement.appendChild(row);
messagesElement.scrollTop = messagesElement.scrollHeight;
}
function clearMessages() {
messagesElement.innerHTML = "";
}
function sendMessage(action, payload) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
appendSystemMessage("WebSocket is not connected.");
return;
}
socket.send(JSON.stringify({
action: action,
...payload
}));
}
function connectToRoom(roomName) {
if (socket) {
socket.close();
socket = null;
}
currentRoom = roomName;
currentRoomElement.textContent = roomName;
clearMessages();
appendSystemMessage(`Connecting to ${roomName}...`);
const websocketUrl = (
`${websocketBaseUrl}?channel=${encodeURIComponent(roomName)}`
);
socket = new ReconnectingWebSocket(websocketUrl, null, {
debug: false,
reconnectInterval: 3000,
maxReconnectInterval: 10000,
reconnectDecay: 1.5,
timeoutInterval: 5000,
maxReconnectAttempts: null
});
socket.onopen = function () {
appendSystemMessage(`Connected to ${roomName}. Fetching history...`);
sendMessage("fetch_history", {});
};
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
if (data.type === "history") {
clearMessages();
for (const message of data.messages) {
appendMessage(
message.username,
message.message,
message.created_at
);
}
appendSystemMessage(`Loaded history for ${data.room}.`);
return;
}
if (data.type === "chat_message") {
appendMessage(
data.username,
data.message,
data.created_at
);
return;
}
if (data.type === "system") {
appendSystemMessage(data.message);
return;
}
console.log("Unhandled WebSocket message:", data);
};
socket.onerror = function (event) {
console.error("WebSocket error:", event);
};
socket.onclose = function (event) {
appendSystemMessage(
`Disconnected from ${roomName}. Reconnecting if possible...`
);
console.log("WebSocket closed:", event.code, event.reason);
};
}
roomSelect.addEventListener("change", function () {
const newRoom = roomSelect.value;
if (socket && socket.readyState === WebSocket.OPEN) {
sendMessage("change_channel", {
channel: newRoom
});
currentRoom = newRoom;
currentRoomElement.textContent = newRoom;
clearMessages();
appendSystemMessage(`Changed to ${newRoom}. Fetching history...`);
sendMessage("fetch_history", {});
return;
}
connectToRoom(newRoom);
});
chatForm.addEventListener("submit", function (event) {
event.preventDefault();
const message = messageInput.value.trim();
if (!message) {
return;
}
sendMessage("chat_message", {
message: message
});
messageInput.value = "";
});
connectToRoom(currentRoom);
</script>
</body>
</html>
How fetching history works¶
When the WebSocket connection opens, the browser sends:
sendMessage("fetch_history", {});
This produces a JSON message like:
{
"action": "fetch_history"
}
API Gateway routes the request to Django. The Django view then calls the
fetch_history method, loads the most recent messages for the current channel,
and sends them back to only the current WebSocket session.
How broadcasting to the current room works¶
When a user sends a chat message, the browser sends:
sendMessage("chat_message", {
message: "Hello everyone"
});
The Django view stores the message and then sends it to every active
WebSocketSession in the same channel.
WebSocketSession.objects.filter(channel_name=room).send_message(
{
"type": "chat_message",
"room": room,
"username": message.username,
"message": message.message,
"created_at": message.created_at.isoformat(),
}
)
Because the queryset is filtered by channel_name, only users in the same
room receive the message.
How changing rooms works¶
A connection is associated with a WebSocket channel.
There are two common ways to change room:
close the current WebSocket connection and open a new one with a different
channelquery string value;keep the current WebSocket connection open and update the stored
WebSocketSession.channel_nameusing a WebSocket message.
The change_channel handler above uses the second approach.
The browser can send:
sendMessage("change_channel", {
channel: "support"
});
This produces a JSON message like:
{
"action": "change_channel",
"channel": "support"
}
The Django view updates the current WebSocketSession:
def change_channel(self, request, *args, **kwargs):
self.websocket_session.channel_name = self.body["channel"]
self.websocket_session.save()
return JsonResponse(
{
"ok": True,
"type": "channel_changed",
"channel": self.websocket_session.channel_name,
}
)
After this, messages broadcast to the new channel will include this connection.
For example, if the connection was originally in:
general
and the client sends:
{
"action": "change_channel",
"channel": "support"
}
then future broadcasts to support will include this connection, and future
broadcasts to general will not.
If WebSocket token protection is enabled and your application opens a new
WebSocket connection when changing room, request a fresh token before opening the
new connection. If you use change_channel on the existing connection, a new
WebSocket token is not required because the connection is not reopened.
Broadcasting to all channels from the Django shell¶
A systems administrator can send a message to every active WebSocket session from the Django shell.
Open the shell:
python manage.py shell
Then run:
from django_aws_api_gateway_websockets.models import WebSocketSession
WebSocketSession.objects.filter(connected=True).send_message(
{
"type": "system",
"message": "Maintenance will be performed in 5 minutes.",
}
)
This sends the message to every active connection, regardless of channel.
Broadcasting to one room from the Django shell¶
To send a message to only one room, filter by channel_name:
from django_aws_api_gateway_websockets.models import WebSocketSession
WebSocketSession.objects.filter(
connected=True,
channel_name="general",
).send_message(
{
"type": "system",
"message": "The general room will be restarted shortly.",
}
)
Using WebSocket tokens¶
The HTML example above keeps the WebSocket connection simple by omitting WebSocket token handling.
If WebSocket token protection is enabled, fetch a token before connecting and include it in the WebSocket URL.
For example:
wss://ws.example.com?ws_token=<token>&channel=general
When reconnecting or changing rooms, request a fresh token before opening the new WebSocket connection.
Security notes¶
For production use:
require authenticated users where appropriate;
validate message length and content;
check permissions before allowing users to join restricted rooms;
avoid trusting the room name without validation;
rate limit message sending;
use WebSocket token protection for authenticated sessions;
escape or sanitise rendered message content;
schedule cleanup for stale WebSocket sessions.