Step 01
Before you start
This course picks up exactly where Add a web UI to your chatbot ended. You’ll reuse the same chatsite Django project, the same chat app, and the same .venv. Right now conversation history lives in request.session— which means it vanishes the moment the session cookie clears. We’ll move history into a Django model and let SQLite keep it for us.
- A working
gemini-chatbot/folder from the previous course, withchatsite/andchat/inside it. - The
.venvvirtual environment still present, andGEMINI_API_KEYin.env. - Comfort running
python manage.pycommands.
What you'll build
Message row in db.sqlite3for every chat turn, keyed by the browser’s Django session key. Reload the page and every turn you’ve ever sent replays straight from the database — no session cookie needed to remember the content.Step 02
Model a chat turn
A chat turn has four things worth remembering: who said it (user or model), what they said, when it happened, and which browser it belongs to. That maps cleanly to four fields on a single model.
chat/models.py
# chat/models.py
from django.db import models
class Message(models.Model):
USER = "user"
MODEL = "model"
ROLE_CHOICES = [
(USER, "User"),
(MODEL, "Model"),
]
session_key = models.CharField(max_length=40, db_index=True)
role = models.CharField(max_length=8, choices=ROLE_CHOICES)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]
def __str__(self):
return f"{self.role}: {self.content[:40]}"
Two details worth pausing on. session_key is indexed because every page load filters on it — it becomes the equivalent of a user id for anonymous visitors. And ordering = ["created_at"] paired with auto_now_add means every queryset replays turns in chronological order without any manual order_by() call.
Step 03
Generate and run the migration
A new model means a new table. Django writes the SQL for you — makemigrations generates a file describing the change, and migrate applies it to db.sqlite3.
terminal
(.venv) $ python manage.py makemigrations chat
Migrations for 'chat':
chat/migrations/0001_initial.py
+ Create model Message
(.venv) $ python manage.py migrate
Running migrations:
Applying chat.0001_initial... OK
Note
migrate you ran at the end of the previous course — Django just has a new migration to apply. The generated chat/migrations/0001_initial.py should be committed to git alongside the model.Step 04
Rewrite the views
The new views.py replaces every request.sessionread and write with an ORM call. The shape of the response JSON doesn’t change, so the template JavaScript you wrote in the previous course keeps working untouched.
chat/views.py
# chat/views.py
import json
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from . import gemini_client
from .models import Message
def _session_key(request):
"""Force-create a session key on first visit so anon users are stable."""
if not request.session.session_key:
request.session.save()
return request.session.session_key
def index(request):
key = _session_key(request)
history = Message.objects.filter(session_key=key)
return render(request, "chat/index.html", {"history": history})
@require_http_methods(["POST"])
def send_message(request):
data = json.loads(request.body)
message = data.get("message", "").strip()
if not message:
return JsonResponse({"error": "empty message"}, status=400)
key = _session_key(request)
prior = Message.objects.filter(session_key=key)
history = [{"role": m.role, "parts": [m.content]} for m in prior]
reply = gemini_client.get_reply(message, history)
Message.objects.create(session_key=key, role="user", content=message)
Message.objects.create(session_key=key, role="model", content=reply)
return JsonResponse({"reply": reply})
Three things changed. _session_key() calls request.session.save() on first visit so anonymous users get a stable id immediately. The history passed to Gemini is built by iterating the queryset into the {"role", "parts": [...]} shape the client expects. And the two Message.objects.create() calls at the bottom replace the old session-append pair.
Step 05
Show prior history on load
index now passes a history queryset into the template. Render it inside #log so a refresh immediately shows every prior turn — no JavaScript hydration needed.
chat/templates/chat/index.html
{# chat/templates/chat/index.html — replace the empty <div id="log"></div> #}
<div id="log">
{% for msg in history %}
<p class="msg {% if msg.role == 'user' %}user{% else %}bot{% endif %}">
{% if msg.role == 'user' %}You: {% else %}Gemini: {% endif %}{{ msg.content }}
</p>
{% endfor %}
</div>
Bonus
chat/admin.py and create a superuser with python manage.py createsuperuser to browse conversations at /admin/.chat/admin.py
# chat/admin.py
from django.contrib import admin
from .models import Message
@admin.register(Message)
class MessageAdmin(admin.ModelAdmin):
list_display = ("created_at", "session_key", "role", "content")
list_filter = ("role",)
search_fields = ("content", "session_key")
Step 06
Run it and verify
Start the server, chat for a few turns, then stop it with Ctrl-C and start it again. Refresh http://127.0.0.1:8000/ — the whole conversation replays from SQLite.
terminal
(.venv) $ python manage.py runserver
# ...chat for a few turns in the browser, then stop the server with Ctrl-C
(.venv) $ python manage.py runserver
# Refresh http://127.0.0.1:8000/ — your earlier conversation is still there.
To see the session_keyscoping in action, open your browser’s devtools, delete the sessionid cookie, and reload. The log is empty because the new session has no rows — the old conversation still exists in the database, just under a different key.
Step 07
Where to take it next
- Add accounts with
django.contrib.authand swapsession_keyfor auser = ForeignKey(User)column so history follows a person across browsers and devices. - Stream the replywith Server-Sent Events so tokens appear as they’re generated.
- Export or clear a conversation with a small admin-only view that filters
Messagebysession_key. - Deploy to Vercel — a full follow-up course walks through shipping this Django project live. SQLite is fine for a demo, but Postgres is the natural next step for anything that sees real traffic.