Back to blogs

Don't Sleep on Convex

How I built real-time presence in minutes, not months

srexrgSreerag P
8 min read
Convex
Real-time

Look, I get it. You've probably heard about Convex. Maybe you've seen it mentioned in a tweet or two. But have you actually tried it? I just built a real-time presence system for an internal project I'm working on, and this thing is pretty solid.

Nard Dog

Why I Reach for Convex

Convex takes care of the chores that usually slow down real-time apps:

  • Built-in database with zero connection management
  • Server functions compiled from TypeScript (not SQL or bespoke RPC wiring)
  • Auto-synced client hooks that stream fresh data over WebSockets
  • Real-time subscriptions out of the box

All of that means I can ship features without writing glue code for transports, auth handshakes, or cache invalidation loops.

What You Can Build with Convex

Convex has a Components catalog with modular building blocks like durable job queues, sharded counters, presence, email integration, rate limiting, and more. You can drop in production-ready features without adding another third-party service. Each component bundles schema, server functions, and best practices, all sandboxed in your Convex project and fully TypeScript friendly. Check out the Convex catalog.

My Internal Live Presence Tracker

One of my internal projects needed a live indicator that shows who’s looking at a page right now. Think “X people are viewing this” pulling from real-time presence data. Convex turned a weeks-long WebSocket build into a quick afternoon experiment.

Convex Components Screenshot

The Old Way (What I Would Have Done)

If I were doing this the traditional way, I'd need:

  1. Set up WebSocket server (Socket.io, probably)
  2. Write connection handlers
  3. Manage heartbeats manually
  4. Track who's online/offline
  5. Handle reconnections
  6. Sync state across clients
  7. Deal with race conditions
  8. Write a bunch of boilerplate
  9. Debug why it's not working
  10. Finally get it working after 3 weeks
Penguins

The Convex Way (What I Actually Did)

I did it in like... an hour? Maybe two?

Step 1: Set Up Presence Component

First, I added the presence component to my Convex config:

import { defineApp } from "convex/server";
import presence from "@convex-dev/presence/convex.config";
 
const app = defineApp();
app.use(presence);
 
export default app;

That's it. Three lines. The presence system is now available.

Step 2: Create Presence Functions

Then I created my presence functions:

import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { Presence } from "@convex-dev/presence";
 
export const presence = new Presence(components.presence);
 
export const heartbeat = mutation({
  args: {
    roomId: v.string(),
    userId: v.string(),
    sessionId: v.string(),
    interval: v.number(),
  },
  handler: async (ctx, { roomId, userId, sessionId, interval }) => {
    if (!userId) {
      throw new Error("Valid user ID required for presence tracking");
    }
    return await presence.heartbeat(ctx, roomId, userId, sessionId, interval);
  },
});
 
export const list = query({
  args: { roomToken: v.string() },
  handler: async (ctx, { roomToken }) => {
    return await presence.list(ctx, roomToken);
  },
});
 
export const disconnect = mutation({
  args: { sessionToken: v.string() },
  handler: async (ctx, { sessionToken }) => {
    return await presence.disconnect(ctx, sessionToken);
  },
});

Look at that. I'm not managing WebSockets. I'm not writing connection logic. I'm just... using the presence API. It handles everything.

Step 3: Use It in React

Now for the frontend. This is where it gets really cool:

"use client";
 
import { api } from "../../convex/_generated/api";
import usePresence from "@convex-dev/presence/react";
 
export function PagePresence({ pageId, userId }) {
  const presenceState = usePresence(api.presence, pageId, userId);
  
  // That's it. presenceState automatically updates when someone joins/leaves
  // No manual subscriptions. No state management. Just... works.
  
  const onlineUsers = presenceState?.filter(user => user.online === true) || [];
  
  return (
    <div className="fixed top-5 right-5">
      <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
      <span>{onlineUsers.length} people viewing</span>
    </div>
  );
}

That usePresence hook? It automatically:

  • Subscribes to presence updates
  • Handles reconnections
  • Manages heartbeats
  • Updates your component when someone joins/leaves
  • All in real-time, over WebSocket

You don't write any of that code. You just use the hook. It's like magic, but it's actually just really good engineering.

good

The Magic Behind the Scenes

Here's what's happening when you use usePresence:

  1. Client subscribes: The hook sends a message to Convex saying "hey, I want to know who's in this room"
  2. Convex runs the query: It executes your list function, reads from the database
  3. Result comes back: You get the initial list of people
  4. Someone joins/leaves: Convex automatically detects the change
  5. Query reruns: Your list function runs again
  6. Update pushed: The new result is sent to all subscribed clients
  7. Component updates: React re-renders with the new data

All of this happens automatically. You don't manage subscriptions. You don't handle WebSocket messages. You don't sync state. Convex does it.

It's all transactional and consistent. If two people join at the exact same time, Convex handles it. If someone's connection drops, Convex knows. If you have multiple tabs open, they all stay in sync.

Why This Matters

Remember that shopping cart example from the Convex docs? The one where you have stock count and cart count, and they always add up correctly? That's the same principle here.

When someone joins a page, every component that's subscribed to that presence data updates at the same time. There's no moment where the count is wrong. There's no race condition. No need to refresh to see the real count.

Your UI is always showing the actual state of the database. Always. That's the reactive database model.

The Full Implementation

In my actual implementation, I show:

  • A live indicator (green pulsing dot)
  • Avatars of online users (up to 5, then "+X more")
  • A count like "3 / 10 people viewing this page"
  • A drawer that shows all users with online/offline status

All of this updates in real-time. When someone opens the page, they appear. When they close the tab, they disappear. When their connection drops, Convex knows and marks them offline.

And I built this in hours, not weeks.

The Real Talk

Look, I'm not here to sell you on Convex. But if you're building anything that needs:

  • Real-time updates
  • Type-safe database queries
  • Automatic state synchronization
  • Less boilerplate
  • Fewer bugs

Then you should at least check it out. Because the alternative is... well, it's a lot more code. A lot more debugging. A lot more "why isn't this working" moments.

I'm using it for presence right now, but honestly? I'm probably going to use it for more things. Chat? Probably. Live collaboration? Maybe. Real-time notifications? Definitely.

Try It Yourself

If you want to see this in action, just build a simple project with Convex. Set up presence, open it in multiple tabs, and watch it update in real-time.

No refresh needed. No polling. Just... works.

Final Thoughts

Convex isn't perfect. No tool is. But for real-time features? It's pretty damn good. And the developer experience is honestly kind of addictive. Once you've written a query function and seen it automatically sync to your React component, it's hard to go back.

So yeah. Don't sleep on Convex.

fin

References


Built with ❤️ using Convex, React, and way too much coffee.