Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Unread Tracking

Real-time unread message counter and read receipt tracker with watermark-based read positions, bulk subscriptions, and React hooks.

Upstream source
git clone https://github.com/TimpiaAI/convex-unread-tracking.git
cd convex-unread-tracking

This upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.

Description

Real-time unread message counter and read receipt tracker with watermark-based read positions, bulk subscriptions, and React hooks.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: TimpiaAI/convex-unread-tracking. License: See repository. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

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

Explore the demo ↗

Upstream overview

From the upstream package documentation.

A Convex component that tracks read positions and unread message counts per user per channel using watermark-based positioning. It provides real-time unread counters, subscription management, and React hooks with optimistic updates. The component handles message registration/unregistration for accurate counting and supports bulk operations via groups.

Use cases

From the upstream package documentation.

  • Building chat applications that need per-channel unread badges and read receipts across multiple conversations
  • Team collaboration tools where users subscribe to different channels and need aggregate unread counts across workspaces
  • Notification systems that track which messages users have seen and provide accurate unread counters in real-time
  • Forum or discussion platforms where users can mute specific senders or archive entire conversation threads
  • Multi-tenant applications where you need to bulk subscribe/unsubscribe entire teams to channels efficiently

How it works

From the upstream package documentation.

The component uses a watermark-based read position system where markReadUpTo() sets a timestamp boundary, and insertMessage() registers messages for counting within the same transaction as your message creation. Read positions are stored per user per channel, with support for explicit range overrides via markReadRange() and markOneUnread().

Setup involves registering the component in convex.config.ts, then creating wrapper functions that call the UnreadTracking API methods like getUnreadCount(), subscribe(), and getTotalUnreadCount(). The component handles subscription management, sender muting, and channel archiving automatically.

The React hooks provide optimistic UI updates - useMarkAsRead() immediately shows zero unread count while the mutation syncs, and useOptimisticUnreadCount() manages the client-side state. A cleanup utility runs via cron jobs to garbage collect old tracking data.

Upstream package FAQ

How does the watermark system work for read tracking?

The convex-unread-tracking component uses timestamps as watermarks to mark everything up to a specific time as read via markReadUpTo(). You can override this with explicit ranges using markReadRange() or mark individual messages unread with markOneUnread().

Can I track unread counts across multiple channels at once?

Yes, convex-unread-tracking provides getTotalUnreadCount() for aggregate counts across all user subscriptions and getSingleUnreads() for batch fetching per-channel counts. Both methods work with the subscription management system.

How do I handle team-wide channel subscriptions?

Use the group operations in convex-unread-tracking: addToGroup() to add users to a group, then subscribeAll() or unsubscribeAll() to manage entire team subscriptions to channels in bulk operations.

Does this component handle message cleanup automatically?

Yes, convex-unread-tracking includes a cleanup mutation designed for Convex cron jobs. Set up a daily cron to automatically garbage collect old tracking data and maintain database performance.

How do the React hooks handle offline scenarios?

The convex-unread-tracking React hooks use localStorage to persist optimistic state when offline. Call markRead() for immediate UI updates, then confirmRead() once the server mutation succeeds to sync the optimistic state.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Unread Tracking with Bijection.

Read: https://bijection.com/components/convex-unread-tracking/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from TimpiaAI/convex-unread-tracking, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

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