New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

convex-unread-tracking

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

convex-unread-tracking

Real-time unread message counter and read receipt tracker for Convex — tracks read positions, supports bulk subscriptions via groups, and provides accurate unread counts with optimistic client updates

latest
Source
npmnpm
Version
1.1.0
Version published
Weekly downloads
2.5K
51.17%
Maintainers
1
Weekly downloads
 
Created
Source

convex-unread-tracking

Real-time unread message counter and read receipt tracker for Convex.

Tracks read positions per user per channel, supports bulk subscriptions via groups, and provides accurate unread counts. Built as a Convex Component.

Features

  • Read position tracking — watermark-based with explicit unread/read range overrides
  • Message accounting — register/unregister messages for accurate unread counts
  • Unread counts — single channel, total across subscriptions, or batch fetch
  • Subscription management — subscribe/unsubscribe/mute senders/archive channels
  • Group operations — bulk subscribe/unsubscribe entire teams
  • React hooks — optimistic mark-as-read with localStorage offline support
  • Cleanup utility — cron-friendly mutation for garbage collection

Installation

npm install convex-unread-tracking

Setup

1. Register the component

// convex/convex.config.ts
import { defineApp } from "convex/server";
import unreadTracking from "convex-unread-tracking/convex.config";

const app = defineApp();
app.use(unreadTracking);

export default app;

2. Create your backend functions

// convex/unreads.ts
import { UnreadTracking } from "convex-unread-tracking";
import { components } from "./_generated/api.js";
import { mutation, query } from "./_generated/server.js";
import { v } from "convex/values";

const unreads = new UnreadTracking(components.unreadTracking);

export const sendMessage = mutation({
  args: { channelId: v.string(), userId: v.string(), text: v.string() },
  handler: async (ctx, args) => {
    const now = Date.now();
    const msgId = await ctx.db.insert("messages", { ...args, timestamp: now });

    // Register with unread tracking (same transaction)
    await unreads.insertMessage(ctx, {
      channelId: args.channelId,
      timestamp: now,
      authorId: args.userId,
    });

    return msgId;
  },
});

export const markRead = mutation({
  args: { userId: v.string(), channelId: v.string(), timestamp: v.number() },
  handler: async (ctx, args) => {
    await unreads.markReadUpTo(ctx, args);
  },
});

export const getUnreadCount = query({
  args: { userId: v.string(), channelId: v.string() },
  handler: async (ctx, args) => {
    return await unreads.getUnreadCount(ctx, args);
  },
});

3. Subscribe users to channels

export const subscribe = mutation({
  args: { userId: v.string(), channelId: v.string() },
  handler: async (ctx, args) => {
    await unreads.subscribe(ctx, args);
  },
});

API Reference

Read Position Tracking

MethodDescription
markReadUpTo(ctx, { userId, channelId, timestamp })Mark everything up to timestamp as read
markReadRange(ctx, { userId, channelId, start, end })Mark a specific range as read
markOneUnread(ctx, { userId, channelId, timestamp })Mark a single item as unread
getLastRead(ctx, { userId, channelId })Get read position for scroll anchor
getLastReads(ctx, { userId, channelIds })Batch fetch read positions

Message Accounting

MethodDescription
insertMessage(ctx, { channelId, timestamp?, authorId? })Register a message for unread counting
deleteMessage(ctx, { channelId, timestamp })Remove a message from tracking

Unread Counts

MethodDescription
getUnreadCount(ctx, { userId, channelId })Count for a single channel
getTotalUnreadCount(ctx, { userId })Aggregate across all subscriptions
getSingleUnreads(ctx, { userId, channelIds })Batch fetch per-channel counts

Subscription Management

MethodDescription
subscribe(ctx, { userId, channelId })Start tracking unreads
unsubscribe(ctx, { userId, channelId })Stop tracking (mute)
muteSender(ctx, { userId, targetUserId })Filter out a sender
unmuteSender(ctx, { userId, targetUserId })Remove sender filter
archive(ctx, { channelId })Stop all tracking for a channel

Group Operations

MethodDescription
addToGroup(ctx, { groupId, userId })Add user to a group
removeFromGroup(ctx, { groupId, userId })Remove from group
subscribeAll(ctx, { groupId, channelId })Subscribe entire group
unsubscribeAll(ctx, { groupId, channelId })Unsubscribe entire group

Cleanup

// convex/crons.ts
import { cronJobs } from "convex/server";
const crons = cronJobs();
crons.daily("cleanup unreads", { hourUTC: 3, minuteUTC: 0 },
  internal.unreads.cleanup);
export default crons;

React Hooks

import { useMarkAsRead, useOptimisticUnreadCount } from "convex-unread-tracking/react";

function Channel({ channelId }) {
  const { markRead, getPendingTimestamp, confirmRead } = useMarkAsRead();

  // Optimistic count — shows 0 immediately after marking read
  const serverCount = useQuery(api.unreads.getUnreadCount, { userId, channelId });
  const count = useOptimisticUnreadCount(serverCount, getPendingTimestamp(channelId) !== null);

  const handleMarkRead = async () => {
    markRead(channelId, Date.now()); // optimistic
    await markReadMutation({ userId, channelId, timestamp: Date.now() });
    confirmRead(channelId); // confirm sync
  };
}

Demo

Live Demo

License

MIT

Keywords

convex

FAQs

Package last updated on 09 Mar 2026

Related posts