| name | reminder-engine |
| description | Server-side cron scheduler that polls a reminders table and delivers appointment reminders via WebSocket to the browser, which then shows Web Notifications API alerts. Use when building or debugging reminder delivery in appointment or scheduling applications. |
reminder-engine
SQLite-backed reminder scheduler with WebSocket delivery and Web Notifications API display.
Architecture
- On appointment creation: insert reminder rows for each configured lead time (default: 1440 and 60 minutes before scheduled_at)
- Server: node-cron polls the reminders table every minute for due unsent reminders, broadcasts via WebSocket, marks sent
- Client: WebSocket listener receives event, requests notification permission if needed, shows browser notification
Server setup
import cron from 'node-cron';
import { WebSocketServer, WebSocket } from 'ws';
import type { Database } from 'better-sqlite3';
interface ReminderRow {
id: number;
appointment_id: number;
title: string;
scheduled_at: string;
}
export function startScheduler(db: Database, wss: WebSocketServer): void {
cron.schedule('* * * * *', () => {
const now = new Date().toISOString();
const due = db.prepare(
`SELECT r.id, r.appointment_id, a.title, a.scheduled_at
FROM reminders r
JOIN appointments a ON a.id = r.appointment_id
WHERE r.remind_at <= ? AND r.sent = 0 AND a.status = 'upcoming'`
).all(now) as ReminderRow[];
const markSent = db.prepare('UPDATE reminders SET sent = 1 WHERE id = ?');
for (const row of due) {
broadcast(wss, {
type: 'appointment_reminder',
appointmentId: row.appointment_id,
title: row.title,
scheduledAt: row.scheduled_at,
});
markSent.run(row.id);
}
});
}
function broadcast(wss: WebSocketServer, data: object): void {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
Inserting reminder rows on appointment creation
function scheduleReminders(
db: Database,
appointmentId: number,
scheduledAt: string,
leadMinsArray: number[]
): void {
const insert = db.prepare(
'INSERT INTO reminders (appointment_id, remind_at, lead_mins) VALUES (?, ?, ?)'
);
for (const leadMins of leadMinsArray) {
const remindAt = new Date(
new Date(scheduledAt).getTime() - leadMins * 60_000
).toISOString();
insert.run(appointmentId, remindAt, leadMins);
}
}
Client setup
export async function requestPermission(): Promise<boolean> {
if (!('Notification' in window)) return false;
if (Notification.permission === 'granted') return true;
if (Notification.permission === 'denied') return false;
const result = await Notification.requestPermission();
return result === 'granted';
}
interface ReminderEvent {
type: string;
appointmentId: number;
title: string;
scheduledAt: string;
}
export function connectReminders(
onReminder: (event: ReminderEvent) => void
): WebSocket {
const ws = new WebSocket(`ws:///ws`);
ws. = {
data = .(event. ) ;
(data. === ) {
(data);
}
};
ws;
}
(): {
(. !== ) ;
date = (scheduledAt).();
n = (, {
: ,
: ,
: ,
: ,
});
n. = { .(); n.(); };
}
React integration
import { useEffect } from 'react';
import { requestPermission, connectReminders, showAppointmentReminder } from './lib/notifications';
export function useReminders(): void {
useEffect(() => {
requestPermission();
const ws = connectReminders(({ title, scheduledAt }) => {
showAppointmentReminder(title, scheduledAt);
});
return () => ws.close();
}, []);
}
Cancelling reminders when appointment is updated
When an appointment is cancelled, deleted, or rescheduled, delete or mark the existing reminder rows as sent to prevent stale notifications.
function cancelReminders(db: Database, appointmentId: number): void {
db.prepare('UPDATE reminders SET sent = 1 WHERE appointment_id = ?').run(appointmentId);
}
function rescheduleReminders(
db: Database,
appointmentId: number,
newScheduledAt: string,
leadMinsArray: number[]
): void {
db.prepare('DELETE FROM reminders WHERE appointment_id = ? AND sent = 0').run(appointmentId);
scheduleReminders(db, appointmentId, newScheduledAt, leadMinsArray);
}
Troubleshooting
Reminders not firing
- Confirm node-cron is running: check server logs for cron tick messages
- Verify
remind_at values are in ISO 8601 UTC format in the database
- Check that the appointment status is still
'upcoming' (cancelled appointments are excluded)
WebSocket disconnects
- Add reconnect logic on
ws.onclose: use exponential backoff before reconnecting
- Verify the WebSocket server path
/ws matches the Express server configuration
Browser notifications not showing
Notification.permission must be 'granted' - call requestPermission() on user interaction
- On macOS: System Settings - Notifications - check the browser is allowed
- Some browsers block notifications on HTTP (non-localhost) origins