Skip to main content

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

  1. Initialize GUN: gun = GUN() creates a new GUN instance
  2. Get Chat Room: chat = gun.get("chat" + location.hash) creates/joins a room based on URL hash
  3. Send Messages: chat.set() adds messages to the set
  4. Receive Messages: chat.map().on() listens for all messages in real-time

Try It Out

  1. Save the code as chat.html
  2. Open it in multiple browser windows
  3. Add #room1 to the URL to join different rooms
  4. Type messages and see them sync instantly!
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

Build docs developers (and LLMs) love