Database Design for Chat Applications: A Complete Beginner's Guide
2025-11-18 · Database Design · 8 min read
#database #postgresql #mongodb #chat #beginner
Building a chat application? This guide will teach you how to design your database from scratch. We'll cover users, conversations, and messages with clear examples you can use right away.

Understanding the Three Core Tables
Every chat application needs three main pieces of data:
- Users - The people using your app
- Conversations - Chat sessions or threads
- Messages - The actual chat messages
Think of it like this: A user can have many conversations, and each conversation can have many messages.
Step 1: The Users Table
This is the simplest table. It stores information about each person using your app.
CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(),email VARCHAR(255) UNIQUE NOT NULL,name VARCHAR(255) NOT NULL,password_hash VARCHAR(255) NOT NULL,created_at TIMESTAMPTZ DEFAULT NOW(),updated_at TIMESTAMPTZ DEFAULT NOW());
What each field means:
id- A unique identifier for each user (like a serial number)email- User's email (must be unique)name- User's display namepassword_hash- Encrypted password (never store plain passwords!)created_at- When the user signed upupdated_at- Last time user info was updated
Example data:
-- Insert a userINSERT INTO users (email, name, password_hash)VALUES ('john@example.com', 'John Doe', '$2b$10$...');-- Result in table:-- id: '550e8400-e29b-41d4-a716-446655440000'-- email: 'john@example.com'-- name: 'John Doe'-- created_at: '2024-11-18 10:00:00'
Step 2: The Conversations Table
This table stores each chat session. Each conversation belongs to one user.
CREATE TABLE conversations (id UUID PRIMARY KEY DEFAULT gen_random_uuid(),user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,title VARCHAR(255) NOT NULL,created_at TIMESTAMPTZ DEFAULT NOW(),updated_at TIMESTAMPTZ DEFAULT NOW());-- Index for faster queriesCREATE INDEX idx_conversations_user ON conversations(user_id);
What each field means:
id- Unique identifier for this conversationuser_id- Which user owns this conversation (links to users table)title- A name for the conversation (e.g., "Project Planning")created_at- When the conversation startedupdated_at- Last message time
The important part: REFERENCES users(id) means this conversation MUST belong to a user that exists. If you delete a user, ON DELETE CASCADE automatically deletes all their conversations too.
-- Create a conversation for JohnINSERT INTO conversations (user_id, title)VALUES ('550e8400-e29b-41d4-a716-446655440000', 'My First Chat');-- Result:-- id: '660e8400-e29b-41d4-a716-446655440001'-- user_id: '550e8400-e29b-41d4-a716-446655440000' (John's ID)-- title: 'My First Chat'
Step 3: The Messages Table
This is where the actual chat messages are stored. Each message belongs to one conversation.
CREATE TABLE messages (id UUID PRIMARY KEY DEFAULT gen_random_uuid(),conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system')),content TEXT NOT NULL,created_at TIMESTAMPTZ DEFAULT NOW(),sequence_number INTEGER NOT NULL);-- Index for faster queries (very important!)CREATE INDEX idx_messages_conversation ON messages(conversation_id, sequence_number);
What each field means:
id- Unique identifier for this messageconversation_id- Which conversation this message belongs torole- Who sent it: 'user' (human), 'assistant' (AI), or 'system' (app)content- The actual message textcreated_at- When the message was sentsequence_number- Message order (1, 2, 3, ...)
-- User sends a messageINSERT INTO messages (conversation_id, role, content, sequence_number)VALUES ('660e8400-e29b-41d4-a716-446655440001', 'user', 'Hello! How are you?', 1);-- AI respondsINSERT INTO messages (conversation_id, role, content, sequence_number)VALUES ('660e8400-e29b-41d4-a716-446655440001', 'assistant', 'I am doing well! How can I help you today?', 2);-- User respondsINSERT INTO messages (conversation_id, role, content, sequence_number)VALUES ('660e8400-e29b-41d4-a716-446655440001', 'user', 'Can you explain databases?', 3);
How They Connect: The Relationships
Here's how the tables link together:
Users↓ (one user has many conversations)Conversations↓ (one conversation has many messages)MessagesReal example:- John (user)- "Project Planning" (conversation)- "Let's discuss the timeline" (message 1)- "Sure, when should we start?" (message 2)- "Next Monday works" (message 3)- "Weekend Chat" (conversation)- "What are you doing this weekend?" (message 1)- "Going hiking!" (message 2)
Common Queries You'll Need
1. Get all conversations for a user
SELECT * FROM conversationsWHERE user_id = '550e8400-e29b-41d4-a716-446655440000'ORDER BY updated_at DESC;-- Returns all of John's conversations, newest first
2. Get all messages in a conversation
SELECT * FROM messagesWHERE conversation_id = '660e8400-e29b-41d4-a716-446655440001'ORDER BY sequence_number ASC;-- Returns all messages in order (1, 2, 3, ...)
3. Get conversation with all messages (JOIN)
SELECTc.id as conversation_id,c.title,c.created_at as conversation_created,m.id as message_id,m.role,m.content,m.created_at as message_createdFROM conversations cLEFT JOIN messages m ON c.id = m.conversation_idWHERE c.id = '660e8400-e29b-41d4-a716-446655440001'ORDER BY m.sequence_number ASC;-- Returns conversation info + all messages in one query
4. Create a new conversation with first message
-- Step 1: Create the conversationINSERT INTO conversations (user_id, title)VALUES ('550e8400-e29b-41d4-a716-446655440000', 'New Chat')RETURNING id;-- Returns: '770e8400-e29b-41d4-a716-446655440002'-- Step 2: Add first messageINSERT INTO messages (conversation_id, role, content, sequence_number)VALUES ('770e8400-e29b-41d4-a716-446655440002', 'user', 'Hello!', 1);
5. Add a new message to existing conversation
-- Get the next sequence numberSELECT MAX(sequence_number) FROM messagesWHERE conversation_id = '660e8400-e29b-41d4-a716-446655440001';-- Returns: 3-- Insert new message with sequence_number = 4INSERT INTO messages (conversation_id, role, content, sequence_number)VALUES ('660e8400-e29b-41d4-a716-446655440001', 'assistant', 'Great question!', 4);-- Update conversation timestampUPDATE conversationsSET updated_at = NOW()WHERE id = '660e8400-e29b-41d4-a716-446655440001';
MongoDB Version (Document Database)
With MongoDB, you can store conversations differently - all messages inside the conversation document:
// Users collection{_id: ObjectId("507f1f77bcf86cd799439011"),email: "john@example.com",name: "John Doe",passwordHash: "$2b$10$...",createdAt: ISODate("2024-11-18T10:00:00Z")}
// Conversations collection{_id: ObjectId("507f1f77bcf86cd799439012"),userId: ObjectId("507f1f77bcf86cd799439011"),title: "My First Chat",messages: [{_id: ObjectId("507f1f77bcf86cd799439013"),role: "user",content: "Hello! How are you?",createdAt: ISODate("2024-11-18T10:05:00Z"),sequenceNumber: 1},{_id: ObjectId("507f1f77bcf86cd799439014"),role: "assistant",content: "I'm doing well! How can I help?",createdAt: ISODate("2024-11-18T10:05:15Z"),sequenceNumber: 2},{_id: ObjectId("507f1f77bcf86cd799439015"),role: "user",content: "Can you explain databases?",createdAt: ISODate("2024-11-18T10:06:00Z"),sequenceNumber: 3}],createdAt: ISODate("2024-11-18T10:05:00Z"),updatedAt: ISODate("2024-11-18T10:06:00Z")}
MongoDB Queries:
// Get all conversations for a userdb.conversations.find({userId: ObjectId("507f1f77bcf86cd799439011")}).sort({ updatedAt: -1 });// Get one conversation with all messagesdb.conversations.findOne({_id: ObjectId("507f1f77bcf86cd799439012")});// Add a new message to conversationdb.conversations.updateOne({ _id: ObjectId("507f1f77bcf86cd799439012") },{$push: {messages: {_id: ObjectId(),role: "assistant",content: "Great question!",createdAt: new Date(),sequenceNumber: 4}},$set: { updatedAt: new Date() }});// Create new conversationdb.conversations.insertOne({userId: ObjectId("507f1f77bcf86cd799439011"),title: "New Chat",messages: [{_id: ObjectId(),role: "user",content: "Hello!",createdAt: new Date(),sequenceNumber: 1}],createdAt: new Date(),updatedAt: new Date()});
SQL vs MongoDB: Which Should You Use?
Choose PostgreSQL (SQL) if:
- You're new to databases (SQL is more standard and easier to learn)
- You need complex queries (analytics, reports, user stats)
- Conversations might have thousands of messages
- You want strong data integrity (relationships are enforced)
- You might add features like shared conversations, teams, etc.
Choose MongoDB if:
- You want faster development (no schema to define upfront)
- Most conversations will be under 1,000 messages
- You mainly fetch entire conversations at once
- You're comfortable with JavaScript/JSON
- You might need to scale to millions of users later
My Recommendation for Beginners:
Start with PostgreSQL. It's more structured, teaches you proper database design, and you won't hit limitations as your app grows. Plus, every developer should know SQL - it's been around for 50 years and will be around for 50 more.
Complete Working Example (Node.js + PostgreSQL)
Here's a complete example you can copy and use:
const { Pool } = require('pg');// Create connection poolconst pool = new Pool({user: 'your_username',host: 'localhost',database: 'chatapp',password: 'your_password',port: 5432,});module.exports = { pool };
const { pool } = require('./database');// Get all conversations for a userasync function getUserConversations(userId) {const result = await pool.query('SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC',[userId]);return result.rows;}// Get conversation with all messagesasync function getConversation(conversationId) {const result = await pool.query(`SELECTc.id, c.title, c.created_at, c.updated_at,json_agg(json_build_object('id', m.id,'role', m.role,'content', m.content,'created_at', m.created_at) ORDER BY m.sequence_number) as messagesFROM conversations cLEFT JOIN messages m ON c.id = m.conversation_idWHERE c.id = $1GROUP BY c.id`,[conversationId]);return result.rows[0];}// Create new conversationasync function createConversation(userId, title, firstMessage) {const client = await pool.connect();try {await client.query('BEGIN');// Create conversationconst convResult = await client.query('INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',[userId, title]);const conversation = convResult.rows[0];// Add first messageawait client.query('INSERT INTO messages (conversation_id, role, content, sequence_number) VALUES ($1, $2, $3, $4)',[conversation.id, 'user', firstMessage, 1]);await client.query('COMMIT');return conversation;} catch (e) {await client.query('ROLLBACK');throw e;} finally {client.release();}}// Add message to conversationasync function addMessage(conversationId, role, content) {const client = await pool.connect();try {await client.query('BEGIN');// Get next sequence numberconst seqResult = await client.query('SELECT COALESCE(MAX(sequence_number), 0) + 1 as next_seq FROM messages WHERE conversation_id = $1',[conversationId]);const nextSeq = seqResult.rows[0].next_seq;// Insert messageconst msgResult = await client.query('INSERT INTO messages (conversation_id, role, content, sequence_number) VALUES ($1, $2, $3, $4) RETURNING *',[conversationId, role, content, nextSeq]);// Update conversation timestampawait client.query('UPDATE conversations SET updated_at = NOW() WHERE id = $1',[conversationId]);await client.query('COMMIT');return msgResult.rows[0];} catch (e) {await client.query('ROLLBACK');throw e;} finally {client.release();}}module.exports = {getUserConversations,getConversation,createConversation,addMessage};
const {getUserConversations,getConversation,createConversation,addMessage} = require('./chatService');async function demo() {const userId = '550e8400-e29b-41d4-a716-446655440000';// Create a new conversationconsole.log('Creating conversation...');const conv = await createConversation(userId,'Learning Databases','Can you help me understand databases?');console.log('Created:', conv);// Add AI responseconsole.log('Adding AI response...');await addMessage(conv.id,'assistant','Of course! Let me explain step by step...');// Get full conversationconsole.log('Loading conversation...');const full = await getConversation(conv.id);console.log('Full conversation:', JSON.stringify(full, null, 2));// List all user conversationsconsole.log('All conversations:');const all = await getUserConversations(userId);console.log(all);}demo().catch(console.error);
Best Practices & Tips
1. Always use indexes
Indexes make queries fast. Without them, your app will be slow.
-- These indexes are CRITICAL for performanceCREATE INDEX idx_conversations_user ON conversations(user_id);CREATE INDEX idx_messages_conversation ON messages(conversation_id, sequence_number);
2. Use transactions for related operations
When creating a conversation + first message, wrap in a transaction so if one fails, both are rolled back.
3. Add pagination for long conversations
-- Get last 50 messagesSELECT * FROM messagesWHERE conversation_id = $1ORDER BY sequence_number DESCLIMIT 50;-- Get messages 51-100 (for infinite scroll)SELECT * FROM messagesWHERE conversation_id = $1ORDER BY sequence_number DESCLIMIT 50 OFFSET 50;
4. Consider soft deletes
Instead of actually deleting data, mark it as deleted:
-- Add deleted_at columnALTER TABLE conversations ADD COLUMN deleted_at TIMESTAMPTZ;ALTER TABLE messages ADD COLUMN deleted_at TIMESTAMPTZ;-- "Delete" a conversationUPDATE conversations SET deleted_at = NOW() WHERE id = $1;-- Only show non-deleted conversationsSELECT * FROM conversationsWHERE user_id = $1 AND deleted_at IS NULL;
Common Mistakes to Avoid
❌ Storing all messages in one TEXT field
Don't store all messages as JSON in a single column. Use proper tables!
❌ Not using foreign keys
Always use REFERENCES to link tables. It prevents orphaned data.
❌ Fetching all messages every time
Use LIMIT and OFFSET to paginate. Don't load 10,000 messages at once!
❌ Storing passwords in plain text
Always hash passwords with bcrypt before storing. Never store them plain!
Next Steps
You now understand the core database structure! Here's what to learn next:
- Add authentication - Learn about JWT tokens, sessions, password hashing
- Add file uploads - Store images/files that users send in chat
- Add search - Let users search their message history
- Add real-time updates - Use WebSockets so messages appear instantly