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
Number of notifications to return (1-100)
Number of notifications to skip
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
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
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
Notification title (1-200 characters)
Notification message (1-2000 characters)
Notification type: system, bounty_comment, submission_received, submission_approved, submission_rejected, bounty_awarded, beta_application_approved, beta_application_rejected, or custom
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 content (1-2000 characters)
Message title (1-100 characters)
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
reportError
Report a client-side error (rate limited).
Access: Public (rate limited)
Error message (1-1000 characters)
Error location (max 200 characters)
Browser user agent (max 500 characters)
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.