DEV Community

Digiwares
Digiwares

Posted on

I Built a Group Chat That Draws Itself Into a Graph - Here's How

I Built a Group Chat That Draws Itself Into a Graph - Here's How

Ever been in a group chat where great ideas just... disappear? You're brainstorming with your team, someone drops a brilliant insight, and 30 messages later it's buried forever.

I got tired of scrolling through walls of text trying to find "that thing someone said earlier," so I built Qonvo — a group chat that visualizes conversations as an interactive graph in real-time.

The Problem

Traditional chat is linear. But conversations aren't.

We jump between topics, reference earlier points, and connect ideas across threads. Forcing all of this into a single scrolling column loses context and makes it hard to see how ideas relate.

The Solution

What if every message was a node, every reply was a connection, and you could see the entire conversation at once?

That's Qonvo. As you chat, a force-directed graph builds in real-time:

  • Messages → Nodes (color-coded by sender)
  • Replies → Solid edges (direct connections)
  • #Tags → Dashed edges (link related ideas across threads)

Click any node to focus it and see its connections. The graph updates live as people chat.

The Tech Stack

Built this as a solo project in about 2 weeks:

  • Frontend: Vanilla JS + D3.js (no React, no build step)
  • Backend: Node.js + Express + Socket.io
  • Persistence: Redis (Upstash) with 3-hour TTL
  • Hosting: Railway

Why Vanilla JS?

Honestly? Speed. I wanted to ship fast without wrestling with bundlers. The entire frontend is a single 1,800-line HTML file. Is it perfect? No. Does it work? Yes.

D3's force simulation handles the graph physics. Socket.io keeps everyone in sync. That's really it.

The Core Graph Logic

// Every message becomes a node
const node = {
  id: nanoid(12),
  userId: sender.id,
  name: sender.name,
  text: message,
  tags: extractTags(message), // #hashtags
  ts: Date.now()
};

// Replies create edges
if (replyTo) {
  edges.push({ 
    source: replyTo, 
    target: node.id, 
    type: "reply" 
  });
}

// Tags connect related messages
for (const tag of node.tags) {
  const lastWithTag = findLastMessageWithTag(tag);
  if (lastWithTag) {
    edges.push({ 
      source: lastWithTag.id, 
      target: node.id, 
      type: "tag",
      tag 
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

What I Learned

1. Ephemeral is a feature, not a bug

Rooms auto-delete after 3 hours. No accounts, no sign-ups, no data hoarding. Users actually love this — it makes the tool feel safe for quick, throwaway conversations.

2. The graph needs to be optional

On mobile, showing chat + graph doesn't work. So there's a toggle to open the graph as an overlay. Desktop shows both side-by-side.

3. Force simulations are tricky

Getting nodes to not overlap while keeping connected nodes close took a lot of tweaking:

simulation
  .force("charge", d3.forceManyBody().strength(-280))
  .force("collision", d3.forceCollide().radius(45))
  .force("link", d3.forceLink(edges).distance(90));
Enter fullscreen mode Exit fullscreen mode

Try It

Live: qonvo.xyz

There's a demo room with sample data, or create your own room and share the link.

No sign-up. No install. Just click and chat.

What's Next

  • Paid room extensions — Keep rooms alive for 7/30 days instead of 3 hours
  • Export to Markdown — Turn conversations into documentation
  • PWA support — Installable on mobile

Feedback Welcome

This is my first real "ship it and see" project. Would love to hear:

  • Is the visualization actually useful, or just a gimmick?
  • What would make you use this over a regular group chat?
  • Any UX issues I should fix?

Drop a comment or try the demo and let me know what breaks.


Building in public as a solo founder. Follow along: @digi_wares

Top comments (0)