The simplest way to build an instant messaging app with Django ๐ŸŒฎ

Published: December 16, 2023

I wrote this tutorial to show you the simplest way to add async, real-time events with Django. This includes:

  • no heavy dependencies. No Redis. No extra Django channels installation.
  • reading from the Django database in real-time ๐ŸŽ๏ธ. We use the new async Django features
  • using the lightest available setup (one pip command to install Daphne).
    • Daphne is fully-integrated into Django.
    • Daphne is very easy to deploy in production: 2 lines (see here).
  • fast to do โฐ I want to learn things as fast as possible. You probably do too.

Our finished product will look like this:

I've made a easy-to-follow video guide (featuring me ๐Ÿ‡๐Ÿฟ) that goes along with the step-by-step instructions. Here's the video:

Motivation:

Whenever I've looked online in the past for a nice example about adding real-time asynchronous events with Django, I've only found lengthy, complex articles - weighed down with dependencies and heavy services to install (like Redis).

Lots of steps.

Not any more.

This guide shows you the simplest way to add real-time events to Django ๐ŸŒฎ

Let's go ๐Ÿš€

0. Setup Django and Daphne

pip install django daphne
django-admin startproject core .
python manage.py startapp sim

Note: Make sure to use >=Django 4.2 . Otherwise, you will get errors like TypeError: async_generator object is not iterable when using async views. (Thanks to Daniel for pointing this out in the Youtube comments)

Add 'daphne' and your app to INSTALLED_APPS in core/settings.py

# core/settings.py
INSTALLED_APPS = [
'daphne', # Add this at the top.
# ...
'sim',
# ...
]

Set ASGI_APPLICATION in core/settings.py

  • Add this line anywhere in the file.
# core/settings.py
ASGI_APPLICATION = 'core.asgi.application'

2. Add async and sync Django views to stream data

  • Add the below to sim/views.py:
from datetime import datetime
import asyncio
from typing import AsyncGenerator
from django.shortcuts import render, redirect
from django.http import HttpRequest, StreamingHttpResponse, HttpResponse
from . import models
import json
import random
def lobby(request: HttpRequest) -> HttpResponse:
if request.method == 'POST':
username = request.POST.get('username')
if username:
request.session['username'] = username
else:
names = [
"Horatio", "Benvolio", "Mercutio", "Lysander", "Demetrius", "Sebastian", "Orsino",
"Malvolio", "Hero", "Bianca", "Gratiano", "Feste", "Antonio", "Lucius", "Puck", "Lucio",
"Goneril", "Edgar", "Edmund", "Oswald"
]
request.session['username'] = f"{random.choice(names)}-{hash(datetime.now().timestamp())}"
return redirect('chat')
else:
return render(request, 'lobby.html')
def chat(request: HttpRequest) -> HttpResponse:
if not request.session.get('username'):
return redirect('lobby')
return render(request, 'chat.html')
def create_message(request: HttpRequest) -> HttpResponse:
content = request.POST.get("content")
username = request.session.get("username")
if not username:
return HttpResponse(status=403)
author, _ = models.Author.objects.get_or_create(name=username)
if content:
models.Message.objects.create(author=author, content=content)
return HttpResponse(status=201)
else:
return HttpResponse(status=200)
async def stream_chat_messages(request: HttpRequest) -> StreamingHttpResponse:
"""
Streams chat messages to the client as we create messages.
"""
async def event_stream():
"""
We use this function to send a continuous stream of data
to the connected clients.
"""
async for message in get_existing_messages():
yield message
last_id = await get_last_message_id()
# Continuously check for new messages
while True:
new_messages = models.Message.objects.filter(id__gt=last_id).order_by('created_at').values(
'id', 'author__name', 'content'
)
async for message in new_messages:
yield f"data: {json.dumps(message)}\n\n"
last_id = message['id']
await asyncio.sleep(0.1) # Adjust sleep time as needed to reduce db queries.
async def get_existing_messages() -> AsyncGenerator:
messages = models.Message.objects.all().order_by('created_at').values(
'id', 'author__name', 'content'
)
async for message in messages:
yield f"data: {json.dumps(message)}\n\n"
async def get_last_message_id() -> int:
last_message = await models.Message.objects.all().alast()
return last_message.id if last_message else 0
return StreamingHttpResponse(event_stream(), content_type='text/event-stream')

3. Add URLs for your async and sync views

  • Create sim/urls.py and add the following:
from django.urls import path
from . import views
urlpatterns = [
path('lobby/', views.lobby, name='lobby'),
path('', views.chat, name='chat'),
path('create-message/', views.create_message, name='create-message'),
path('stream-chat-messages/', views.stream_chat_messages, name='stream-chat-messages'),
]

Update core/urls.py to include the app's URLs

# core/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sim.urls')),
]

4. Add your templates, including an EventSource script to receive your server-sent events from Django

  • Create a directory named templates in the sim directory.
  • Create a file named chat.html in the templates directory.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat</title>
</head>
<body>
<div class="header">
<h1>Welcome {{ request.session.username }}</h1>
</div>
<div class="container">
<div class="messages">
<div id="sse-data"></div>
</div>
<form
x-cloak
@submit.prevent="submit"
x-data="{state: 'composing', errors: {}}"
>
<div>
<textarea
name="content"
@input="state = 'composing'"
autofocus
placeholder="Your next message..."
></textarea>
<button class="button">Send</button>
</div>
<div x-show="state === 'error'">
<p>Error sending your message โŒ</p>
</div>
</form>
<form action="/lobby/" method="get">
<button type="submit">Return to Lobby</button>
</form>
</div>
<script>
let eventSource
const sseData = document.getElementById('sse-data')
function startSSE() {
eventSource = new EventSource('/stream-chat-messages/')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
const messageHTML = `
<div class="message-box">
<div class="message-author">${data.author__name}</div>
<div class="message-content">${data.content}</div>
</div>`
sseData.innerHTML += messageHTML
}
}
// On load, start SSE if the browser supports it.
if (typeof EventSource !== 'undefined') {
startSSE()
} else {
sseData.innerHTML =
"Whoops! Your browser doesn't receive server-sent events."
}
</script>
<script>
function submit(event) {
event.preventDefault()
const formData = new FormData(event.target)
const endpointUrl = '/create-message/'
fetch(endpointUrl, {
method: 'post',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}',
},
})
.then((response) => {
this.state = response.ok ? 'success' : 'error'
return response.json()
})
.then((data) => {
this.errors = data.errors || {}
})
}
</script>
<script
defer=""
src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"
></script>
</body>
</html>

Add a lobby, where users choose a name

  • Create a file named lobby.html in the templates directory.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sign-in Page</title>
<style>
body {
font-family: 'Helvetica Neue', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #e8eff1;
margin: 0;
color: #333;
}
.sign-in-container {
background: #ffffff;
padding: 40px 50px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 300px;
}
.sign-in-container h2 {
text-align: center;
margin-bottom: 30px;
font-size: 24px;
color: #0a3d62;
}
.sign-in-container form {
display: flex;
flex-direction: column;
}
.sign-in-container input {
margin-bottom: 15px;
padding: 15px;
border: 1px solid #ced6e0;
border-radius: 6px;
font-size: 16px;
}
.sign-in-container button {
padding: 15px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.sign-in-container button:hover {
background-color: #27ae60;
}
</style>
</head>
<body>
<div class="sign-in-container">
<h2>Enter your chat name:</h2>
<form method="post">
{% csrf_token %}
<input type="text" name="username" placeholder="Username" required />
<button type="submit">Join the chat</button>
</form>
</div>
</body>
</html>

6. Create Django models to store data to send in real-time

  • Add the below to sim/models.py:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=500)
class Message(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
  • Run migrations to create the database table for the new model.
python manage.py makemigrations
python manage.py migrate

7. Run ๐Ÿƒโ€โ™€๏ธ

  • Run the Django app:
python manage.py runserver

You should see something similar to the examples given below. Note that the Daphne server is working:

Django version 4.2.7, using settings 'core.settings'
Starting ASGI/Daphne version 4.0.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Access your Django application to see the realtime server-sent events

  • Visit http://127.0.0.1:8000/ in your web browser.

8. Bonus: Add styling to your chat interface

  • Add styling to the chat.html template to include the chat interface and styling. Here's the full template:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chat</title>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #e8eff1;
margin: 0;
padding: 0;
color: #333;
}
.header {
color: #022c22;
font-size: 14px;
text-align: center;
}
.container {
max-width: 60%;
margin: auto;
}
.messages {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
font-size: 16px;
height: 50vh;
overflow-y: scroll;
}
.message {
border-bottom: 1px solid #ced6e0;
padding: 15px 0;
}
.message:last-child {
border-bottom: none;
}
form {
display: flex;
flex-direction: column;
}
textarea,
input,
button {
margin-bottom: 15px;
padding: 15px;
border: 1px solid #ced6e0;
border-radius: 6px;
font-size: 16px;
}
.button {
background-color: #2ecc71;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
.button:hover {
background-color: #27ae60;
}
.message-box {
background: rgba(247, 248, 245, 0.42);
border-left: 4px solid rgba(51, 177, 104, 0.42);
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.message-author {
font-weight: bold;
margin-bottom: 5px;
}
.message-content {
font-size: 16px;
line-height: 1.4;
}
textarea {
background: #f8f9fa;
border: 1px solid #ced4da;
box-sizing: border-box;
width: 100%;
padding: 12px 20px;
border-radius: 6px;
min-height: 100px;
font-size: 16px;
line-height: 1.5;
resize: none;
outline: none;
}
</style>
<style>
[x-cloak] {
display: none !important;
}
</style>
</head>
<body>
<div class="header">
<h1>Welcome {{ request.session.username }}</h1>
</div>
<div class="container">
<div class="messages">
<div id="sse-data"></div>
</div>
<form
x-cloak
@submit.prevent="submit"
x-data="{state: 'composing', errors: {}}"
>
<div>
<textarea
name="content"
@input="state = 'composing'"
autofocus
placeholder="Your next message..."
></textarea>
<button class="button">Send</button>
</div>
<div x-show="state === 'error'">
<p>Error sending your message โŒ</p>
</div>
</form>
<form action="/lobby/" method="get">
<button type="submit">Return to Lobby</button>
</form>
</div>
<script>
let eventSource
const sseData = document.getElementById('sse-data')
function startSSE() {
eventSource = new EventSource('/stream-chat-messages/')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
const messageHTML = `
<div class="message-box">
<div class="message-author">${data.author__name}</div>
<div class="message-content">${data.content}</div>
</div>`
sseData.innerHTML += messageHTML
}
}
// On load, start SSE if the browser supports it.
if (typeof EventSource !== 'undefined') {
startSSE()
} else {
sseData.innerHTML =
"Whoops! Your browser doesn't receive server-sent events."
}
</script>
<script>
function submit(event) {
event.preventDefault()
const formData = new FormData(event.target)
const endpointUrl = '/create-message/'
fetch(endpointUrl, {
method: 'post',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}',
},
})
.then((response) => {
this.state = response.ok ? 'success' : 'error'
return response.json()
})
.then((data) => {
this.errors = data.errors || {}
})
}
</script>
<script
defer=""
src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"
></script>
</body>
</html>

9. Check out your real-time Django instant messenger app with server-sent events

  • Visit http://127.0.0.1:8000/ in your web browser.
  • Open several tabs and see the updates in realtime๐ŸŽ‰ ๐ŸŒฎ

Congratulations. You've built a real-time instant messenger app with Django and server-sent events, using the latest async Django features. ๐Ÿ”‹

Next steps? Deploy ๐Ÿš€

If enough people are interested, I'll write my next guide to show you how to fully deploy this app online.

Edit: Thanks for the interest ๐Ÿ™‚ Here's the guide: How to deploy a Django instant messenger app with real-time events

P.S Want to ship better features with AI?
Join my free weekly newsletter

Each week, I share bite-sized learnings and AI news that matters - so you can build better software in practice.

No spam guaranteed ยท Unsubscribe whenever