Build a Real-Time Chat Application
Learn how to build a real-time chat application using GUN. This tutorial covers everything from basic messaging to advanced features like timestamps and user management.What You’ll Build
- Real-time message synchronization
- User nicknames and identity
- Message timestamps
- Automatic scrolling
- Multi-user support
- Offline-first with automatic sync
Quick Start: Minimal Chat
Here’s the simplest possible chat application in just 21 lines:<!DOCTYPE html>
<ul id='list'></ul>
<form id='form'>
<input id='who' placeholder='name'>
<input id='what' placeholder='say'>
<input type='submit' value='send'>
</form>
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script>
gun = GUN();
chat = gun.get("chat" + location.hash);
view = document;
form.onsubmit = (eve) => {
chat.set(who.value + ': ' + what.value);
eve.preventDefault();
what.value = "";
}
chat.map().on(function show(data, id){
(view.line = view.getElementById(id) || view.createElement("li")).id = id;
list.appendChild(view.line).innerText = data;
window.scroll(0, list.offsetHeight);
});
</script>
How It Works
- Initialize GUN:
gun = GUN()creates a new GUN instance - Get Chat Room:
chat = gun.get("chat" + location.hash)creates/joins a room based on URL hash - Send Messages:
chat.set()adds messages to the set - Receive Messages:
chat.map().on()listens for all messages in real-time
Try It Out
- Save the code as
chat.html - Open it in multiple browser windows
- Add
#room1to the URL to join different rooms - Type messages and see them sync instantly!
Full-Featured Chat App
Here’s a more complete example with better UX:<!DOCTYPE html>
<html>
<head>
<title>GUN Chat</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.chat-messages {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
height: 400px;
overflow-y: auto;
background: #f9f9f9;
margin-bottom: 20px;
}
.message {
margin-bottom: 15px;
padding: 10px;
background: white;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.message-author {
font-weight: bold;
color: #0066cc;
}
.message-time {
color: #666;
font-size: 0.85em;
}
.chat-form {
display: flex;
gap: 10px;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
#name-input {
flex: 1;
}
#message-input {
flex: 3;
}
button {
padding: 10px 20px;
background: #0066cc;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0052a3;
}
</style>
</head>
<body>
<h1>GUN Chat</h1>
<div class="chat-messages" id="messages"></div>
<form class="chat-form" id="chat-form">
<input id="name-input" placeholder="Your name" required>
<input id="message-input" placeholder="Type a message..." required>
<button type="submit">Send</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script>
// Initialize GUN
const gun = GUN([
'http://localhost:8765/gun',
'https://gun-manhattan.herokuapp.com/gun'
]);
// Get chat reference
const chat = gun.get('chat/' + (location.hash.slice(1) || 'lobby'));
// Get DOM elements
const form = document.getElementById('chat-form');
const nameInput = document.getElementById('name-input');
const messageInput = document.getElementById('message-input');
const messagesDiv = document.getElementById('messages');
// Load saved username
const savedName = localStorage.getItem('chatName');
if (savedName) {
nameInput.value = savedName;
}
// Send message
form.addEventListener('submit', (e) => {
e.preventDefault();
const username = nameInput.value.trim();
const message = messageInput.value.trim();
if (!username || !message) return;
// Save username
localStorage.setItem('chatName', username);
// Create message object
const msg = {
who: username,
what: message,
when: Gun.state() // GUN's vector clock timestamp
};
// Add to chat
chat.set(msg);
// Clear input
messageInput.value = '';
messageInput.focus();
});
// Receive messages
const messages = {};
chat.map().once((msg, id) => {
if (!msg || !msg.who || !msg.what) return;
// Store message
messages[id] = msg;
// Render all messages sorted by timestamp
renderMessages();
});
function renderMessages() {
// Sort messages by timestamp
const sorted = Object.entries(messages)
.sort(([, a], [, b]) => (a.when || 0) - (b.when || 0));
// Clear container
messagesDiv.innerHTML = '';
// Render each message
sorted.forEach(([id, msg]) => {
const div = document.createElement('div');
div.className = 'message';
div.id = id;
const time = new Date(msg.when);
const timeStr = time.toLocaleTimeString();
div.innerHTML = `
<div class="message-header">
<span class="message-author">${escapeHtml(msg.who)}</span>
<span class="message-time">${timeStr}</span>
</div>
<div class="message-content">${escapeHtml(msg.what)}</div>
`;
messagesDiv.appendChild(div);
});
// Auto-scroll to bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Focus message input on load
messageInput.focus();
</script>
</body>
</html>
React Chat Component
Here’s a production-ready React chat component:import React, { Component } from 'react';
import Gun from 'gun/gun';
const formatMsgs = msgs => Object.keys(msgs)
.map(key => ({ key, ...msgs[key] }))
.filter(m => Boolean(m.when) && m.key !== '_')
.sort((a, b) => a.when - b.when)
.map(m => ((m.whenFmt = new Date(m.when).toLocaleString()), m));
export default class Chat extends Component {
constructor({ gun }) {
super();
this.gun = gun.get('chat');
this.state = {
newMsg: '',
name: (document.cookie.match(/alias\=(.*?)(\&|$|\;)/i) || [])[1] || '',
msgs: {},
};
}
componentWillMount() {
const tmpState = {};
this.gun.map().val((msg, key) => {
tmpState[key] = msg;
this.setState({ msgs: Object.assign({}, this.state.msgs, tmpState) });
});
}
send = e => {
e.preventDefault();
const who = this.state.name || 'user' + Gun.text.random(6);
this.setState({ name: who });
document.cookie = ('alias=' + who);
const when = Gun.time.is();
const key = `${when}_${Gun.text.random(4)}`;
this.gun.path(key).put({
who,
when,
what: this.state.newMsg,
});
this.setState({ newMsg: '' });
}
render() {
const msgs = formatMsgs(this.state.msgs);
return (
<div className="chat-container">
<ul className="chat-messages">
{msgs.map(msg =>
<li key={msg.key}>
<b>{msg.who}:</b> {msg.what}
<span className="time">{msg.whenFmt}</span>
</li>
)}
</ul>
<form onSubmit={this.send}>
<input
value={this.state.name}
className="name-input"
placeholder="Your name"
onChange={e => this.setState({ name: e.target.value })}
/>
<input
value={this.state.newMsg}
className="message-input"
placeholder="Type a message..."
onChange={e => this.setState({ newMsg: e.target.value })}
/>
<button onClick={this.send}>Send</button>
</form>
</div>
);
}
}
// Usage:
// const gun = Gun();
// <Chat gun={gun} />
Key Concepts
Using Sets for Messages
// Add to set (no duplicates by ID)
chat.set({ text: 'Hello' });
// Listen to all items in set
chat.map().on(message => {
console.log(message);
});
Message Timestamps
// GUN's vector clock timestamp
const timestamp = Gun.state();
// JavaScript timestamp
const jsTime = Date.now();
// Use for sorting
const when = Gun.state();
messages.sort((a, b) => a.when - b.when);
Room/Channel Support
// Use URL hash for rooms
const room = location.hash.slice(1) || 'lobby';
const chat = gun.get('chat/' + room);
// Or user input
const room = prompt('Enter room name');
const chat = gun.get('chat/' + room);
Preventing XSS Attacks
// Always escape user content
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Use textContent instead of innerHTML
element.textContent = userMessage;
Advanced Features
Private Messages
See User Authentication for encrypted private messaging.Message Deletion
// "Delete" by setting to null
chat.get(messageId).put(null);
// Filter out null messages
chat.map().on((msg, id) => {
if (msg === null) {
// Remove from UI
document.getElementById(id)?.remove();
}
});
Typing Indicators
// Ephemeral data (not persisted)
const presence = gun.get('presence/' + room);
messageInput.addEventListener('input', () => {
presence.get(username).put({
typing: true,
lastSeen: Date.now()
});
});
// Clear after delay
let typingTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
presence.get(username).put({ typing: false });
}, 1000);
});
Message Reactions
// Add reaction to message
function addReaction(messageId, emoji) {
chat.get(messageId).get('reactions').get(emoji).put({
count: 1,
users: [currentUser]
});
}
// Listen for reactions
chat.get(messageId).get('reactions').map().on((reaction, emoji) => {
console.log(`${emoji}: ${reaction.count}`);
});
Next Steps
User Authentication
Add user login and private messaging
Todo App
Learn CRUD operations
P2P Networking
Understand mesh networking
API Reference
Explore the full API