Skip to main content
The notifications router handles user notifications, Discord webhook integrations, and error reporting.

Queries

getAll

Get all notifications for the current user with pagination. Access: Protected
limit
number
default:"50"
Number of notifications to return (1-100)
offset
number
default:"0"
Number of notifications to skip
unreadOnly
boolean
default:"false"
Return only unread notifications
Response:
Array<{
  id: string;
  userId: string;
  type: string;
  title: string;
  message: string;
  read: boolean;
  data: Record<string, unknown> | null;
  createdAt: Date;
  updatedAt: Date;
}>
Example:
const notifications = await trpc.notifications.getAll.query({
  limit: 20,
  offset: 0,
  unreadOnly: true
});

notifications.forEach(notif => {
  console.log(`${notif.title}: ${notif.message}`);
});

getUnreadCount

Get the count of unread notifications for the current user. Access: Protected Response: number Example:
const unreadCount = await trpc.notifications.getUnreadCount.query();

if (unreadCount > 0) {
  document.title = `(${unreadCount}) Bounty`;
}

getStats (Admin)

Get notification statistics. Access: Admin only Response:
{
  stats: {
    sent: number;
  };
}

testWebhook (Admin)

Test the Discord webhook configuration. Access: Admin only

Mutations

markAsRead

Mark a specific notification as read. Access: Protected
id
string
required
Notification ID
Example:
await trpc.notifications.markAsRead.mutate({
  id: "notification-id"
});

markAllAsRead

Mark all notifications for the current user as read. Access: Protected Example:
const updated = await trpc.notifications.markAllAsRead.mutate();

console.log(`Marked ${updated.length} notifications as read`);

cleanup

Delete old read notifications. Access: Protected
daysToKeep
number
default:"30"
Number of days to keep notifications (1-365)
Example:
const deleted = await trpc.notifications.cleanup.mutate({
  daysToKeep: 30
});

console.log(`Deleted ${deleted.length} old notifications`);

sendToUser (Admin)

Send a notification to a specific user. Access: Admin only
userId
string
required
Recipient user ID
title
string
required
Notification title (1-200 characters)
message
string
required
Notification message (1-2000 characters)
type
string
default:"custom"
Notification type: system, bounty_comment, submission_received, submission_approved, submission_rejected, bounty_awarded, beta_application_approved, beta_application_rejected, or custom
data
object
Additional data payload
Example:
await trpc.notifications.sendToUser.mutate({
  userId: "user-uuid",
  title: "Welcome to Bounty!",
  message: "Thanks for joining. Check out our featured bounties.",
  type: "system",
  data: {
    action: "view_bounties",
    url: "/bounties"
  }
});

sendWebhook (Admin)

Send a message to Discord webhook. Access: Admin only
message
string
required
Message content (1-2000 characters)
title
string
Message title (1-100 characters)
context
object
Additional context data
type
string
default:"log"
Message type: log, info, warning, or error
Example:
await trpc.notifications.sendWebhook.mutate({
  title: "New Feature Deployed",
  message: "Dark mode is now available to all users",
  type: "info",
  context: {
    version: "1.2.0",
    timestamp: new Date().toISOString()
  }
});

sendError (Admin)

Send an error report to Discord webhook. Access: Admin only
error
string
required
Error message
context
object
Error context data
location
string
Error location/source

reportError

Report a client-side error (rate limited). Access: Public (rate limited)
error
string
required
Error message (1-1000 characters)
location
string
Error location (max 200 characters)
userAgent
string
Browser user agent (max 500 characters)
url
string
URL where error occurred (max 500 characters)
Client error reports are only sent to Discord in production environments to avoid spam during development.

Code Examples

Notification Center

import { trpc } from '@/lib/trpc';
import { useEffect } from 'react';

const NotificationCenter = () => {
  const { data: notifications, refetch } = trpc.notifications.getAll.useQuery({
    limit: 50,
    unreadOnly: false
  });
  
  const { data: unreadCount } = trpc.notifications.getUnreadCount.useQuery();
  const markAsReadMutation = trpc.notifications.markAsRead.useMutation();
  const markAllMutation = trpc.notifications.markAllAsRead.useMutation();

  const handleMarkAsRead = async (id: string) => {
    await markAsReadMutation.mutateAsync({ id });
    refetch();
  };

  const handleMarkAllAsRead = async () => {
    await markAllMutation.mutateAsync();
    refetch();
  };

  return (
    <div className="notification-center">
      <div className="header">
        <h2>Notifications ({unreadCount} unread)</h2>
        {unreadCount > 0 && (
          <button onClick={handleMarkAllAsRead}>
            Mark all as read
          </button>
        )}
      </div>
      
      <div className="notifications-list">
        {notifications?.map(notif => (
          <div 
            key={notif.id} 
            className={`notification ${notif.read ? 'read' : 'unread'}`}
            onClick={() => !notif.read && handleMarkAsRead(notif.id)}
          >
            <h4>{notif.title}</h4>
            <p>{notif.message}</p>
            <span className="time">
              {new Date(notif.createdAt).toLocaleString()}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
};

Real-time Notification Updates

import { trpc } from '@/lib/trpc';
import { useEffect } from 'react';
import { realtime } from '@bounty/realtime';

const useNotifications = () => {
  const utils = trpc.useContext();
  const { data: unreadCount } = trpc.notifications.getUnreadCount.useQuery();

  useEffect(() => {
    // Subscribe to real-time notification updates
    const unsubscribe = realtime.on('notifications.refresh', () => {
      // Invalidate and refetch notification queries
      utils.notifications.getUnreadCount.invalidate();
      utils.notifications.getAll.invalidate();
    });

    return () => unsubscribe();
  }, [utils]);

  return { unreadCount };
};

Error Reporter

import { trpc } from '@/lib/trpc';

const setupErrorReporting = () => {
  const reportError = trpc.notifications.reportError.useMutation();

  window.addEventListener('error', (event) => {
    reportError.mutate({
      error: event.message,
      location: event.filename,
      url: window.location.href,
      userAgent: navigator.userAgent
    });
  });

  window.addEventListener('unhandledrejection', (event) => {
    reportError.mutate({
      error: `Unhandled Promise Rejection: ${event.reason}`,
      url: window.location.href,
      userAgent: navigator.userAgent
    });
  });
};

Notification Badge

import { trpc } from '@/lib/trpc';

const NotificationBadge = () => {
  const { data: count } = trpc.notifications.getUnreadCount.useQuery(
    undefined,
    {
      refetchInterval: 30000 // Refetch every 30 seconds
    }
  );

  if (!count || count === 0) return null;

  return (
    <span className="notification-badge">
      {count > 99 ? '99+' : count}
    </span>
  );
};

Admin: Send System Notification

import { trpc } from '@/lib/trpc';

const sendSystemNotification = async (userId: string) => {
  await trpc.notifications.sendToUser.mutate({
    userId,
    title: "System Maintenance",
    message: "Scheduled maintenance on Sunday at 2 AM UTC. Expect 30 minutes of downtime.",
    type: "system",
    data: {
      scheduled: "2024-01-15T02:00:00Z",
      duration: "30 minutes"
    }
  });
};

Notification Types

The following notification types are available:
  • system - System-wide announcements
  • bounty_comment - Someone commented on your bounty
  • submission_received - New submission on your bounty
  • submission_approved - Your submission was approved
  • submission_rejected - Your submission was rejected
  • bounty_awarded - You were awarded a bounty
  • beta_application_approved - Beta access granted
  • beta_application_rejected - Beta access denied
  • custom - Custom notification type
Each type can have associated data in the data field for additional context.

Build docs developers (and LLMs) love