Document Version: 1.0
Last Updated: January 2026
Scope: Pushover, IFTTT Maker, notification flows, deduplication, acknowledgment flows
The Nightscout messaging subsystem enables critical alerts to reach caregivers through multiple channels. This audit examines notification generation, delivery mechanisms, and reliability considerations.
| Component | Purpose | Status |
|---|---|---|
| Internal Notifications | Alarm management | Core |
| Pushover | Push notifications | Integration |
| IFTTT Maker | Webhook automation | Integration |
| Apple Push (APN) | iOS notifications | Optional |
| WebSocket Alerts | Browser notifications | Core |
Plugin checks data
↓
requestNotify() or requestSnooze()
↓
Notification Manager (lib/notifications.js)
↓
Process notifications
↓
emit('notification', notify)
↓
┌───────────────────────────────────────────┐
│ Event Bus │
└───────┬─────────────┬─────────────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Pushover │ │ Maker │ │ WebSocket │
│ Plugin │ │ Plugin │ │ Broadcast │
└─────────────┘ └─────────────┘ └─────────────┘
{
level: 1, // 0=INFO, 1=WARN, 2=URGENT
title: 'Low Glucose', // Short title
message: 'BG is 65 mg/dL', // Detailed message
plugin: plugin, // Source plugin reference
group: 'default', // Notification group
isAnnouncement: false, // Is user announcement
// Optional fields
clear: false, // Is clear notification
debug: {}, // Debug information
pushoverSound: 'climb', // Custom sound
// Computed
notifyhash: 'abc123' // Deduplication hash
}| Level | Name | Constant | Use Case |
|---|---|---|---|
| -2 | None | NONE |
Internal only |
| -1 | Low | LOW |
Debug/trace |
| 0 | Info | INFO |
Informational |
| 1 | Warning | WARN |
Attention needed |
| 2 | Urgent | URGENT |
Immediate action |
Location: lib/notifications.js
Key Functions:
// Request a notification
notifications.requestNotify = function(notify) {
if (!notify.level || !notify.title || !notify.message || !notify.plugin) {
console.error('Incomplete notification');
return;
}
notify.group = notify.group || 'default';
requests.notifies.push(notify);
};
// Request a snooze
notifications.requestSnooze = function(snooze) {
snooze.group = snooze.group || 'default';
requests.snoozes.push(snooze);
};
// Process all pending notifications
notifications.process = function() {
// Find highest alarm per group
// Check for snoozing
// Emit or suppress
};Alarm Object:
var Alarm = function(level, group, label) {
this.level = level;
this.group = group;
this.label = label;
this.silenceTime = 30 * 60 * 1000; // 30 minutes default
this.lastAckTime = 0;
this.lastEmitTime = null;
};Alarm Processing:
- Collect all requested notifications
- Group by notification group
- Find highest priority per group
- Check if snoozed by any snooze request
- Check if silenced from previous ack
- Emit if not suppressed
When conditions return to normal:
function autoAckAlarms(group) {
for (var level = 1; level <= 2; level++) {
var alarm = getAlarm(level, group);
if (alarm.lastEmitTime) {
notifications.ack(alarm.level, group, 1); // 1ms silence
sendClear = true;
}
}
if (sendClear) {
ctx.bus.emit('notification', {
clear: true,
title: 'All Clear',
message: 'Auto ack\'d alarm(s)',
group: group
});
}
}Location: lib/server/pushnotify.js
function init(env, ctx) {
var receipts = new NodeCache({ stdTTL: 3600 });
var recentlySent = new NodeCache({ stdTTL: 900 });
pushnotify.emitNotification = function(notify) {
if (notify.clear) {
cancelPushoverNotifications();
sendMakerAllClear(notify);
return;
}
// Check deduplication
var key = notify.notifyhash || generateHash(notify);
if (recentlySent.get(key)) {
console.log('Skipping duplicate notification');
return;
}
// Send to providers
ctx.pushover.send(notify, callback);
ctx.maker.sendEvent(notify, callback);
};
}Strategy:
- Generate hash from notification content
- Cache recently sent hashes (15 minute TTL)
- Skip if hash exists in cache
Hash Generation:
function generateHash(notify) {
const crypto = require('crypto');
const hash = crypto.createHash('sha1');
hash.update(notify.title + notify.message);
return hash.digest('hex').substring(0, 16);
}For emergency priority notifications:
var receipts = new NodeCache({ stdTTL: 3600 });
// Store receipt from Pushover
receipts.set(receipt, notify);
// Periodic check
pushnotify.checkReceipts = function() {
receipts.keys().forEach(function(receipt) {
ctx.pushover.checkReceipt(receipt, function(err, result) {
if (result.acknowledged) {
// User acknowledged, remove from cache
receipts.del(receipt);
}
});
});
};Location: lib/plugins/pushover.js
Environment Variables:
PUSHOVER_API_TOKEN=your-app-token
PUSHOVER_USER_KEY=user-or-group-key
PUSHOVER_ALARM_KEY=key-for-alarms
PUSHOVER_ANNOUNCEMENT_KEY=key-for-announcements
BASE_URL=https://nightscout.example.com
var pushoverAPI = {
userKeys: env.extendedSettings.pushover.userKey.split(' '),
alarmKeys: (env.extendedSettings.pushover.alarmKey || userKey).split(' '),
announcementKeys: (env.extendedSettings.pushover.announcementKey || userKey).split(' '),
apiToken: env.extendedSettings.pushover.apiToken
};
function selectKeys(notify) {
if (notify.isAnnouncement) {
return pushoverAPI.announcementKeys;
} else if (ctx.levels.isAlarm(notify.level)) {
return pushoverAPI.alarmKeys;
}
return pushoverAPI.userKeys;
}| Nightscout Level | Pushover Priority | Behavior |
|---|---|---|
| INFO | 0 (Normal) | Normal push |
| WARN | 1 (High) | Bypasses quiet hours |
| URGENT | 2 (Emergency) | Repeats until ack'd |
pushover.send = function(notify, callback) {
var selectedKeys = selectKeys(notify);
selectedKeys.forEach(function(userKey) {
var msg = {
message: notify.message,
title: notify.title,
priority: mapPriority(notify.level),
sound: notify.pushoverSound || 'gamelan',
callback: env.base_url + '/api/v1/notifications/pushovercallback',
timestamp: Math.round(Date.now() / 1000)
};
if (msg.priority === 2) {
msg.retry = 120; // Retry every 2 minutes
msg.expire = 3600; // Expire after 1 hour
}
pushoverClient.send(msg, userKey, callback);
});
};Endpoint: POST /api/v1/notifications/pushovercallback
api.post('/notifications/pushovercallback', function(req, res) {
if (ctx.pushnotify.pushoverAck(req.body)) {
res.sendStatus(200);
} else {
res.sendStatus(500);
}
});Location: lib/plugins/maker.js
Environment Variables:
MAKER_KEY=your-ifttt-webhooks-key
MAKER_ANNOUNCEMENT_KEY=optional-separate-key
| Event Name | Trigger | Value1 | Value2 | Value3 |
|---|---|---|---|---|
ns-event |
Any event | Title | Message | Timestamp |
ns-allclear |
Alarm cleared | Title | Message | - |
ns-info |
INFO level | Title | Message | - |
ns-warning |
WARN level | Title | Message | - |
ns-urgent |
URGENT level | Title | Message | - |
ns-{plugin} |
Plugin event | Title | Message | - |
ns-{level}-{eventName} |
Specific event | Title | Message | - |
maker.sendEvent = function(notify, callback) {
if (!keys || keys.length === 0) return callback();
var events = [
'ns-event',
'ns-' + levelName(notify.level),
'ns-' + notify.plugin.name
];
if (notify.eventName) {
events.push('ns-' + levelName(notify.level) + '-' + notify.eventName);
}
events.forEach(function(event) {
keys.forEach(function(key) {
var url = 'https://maker.ifttt.com/trigger/' + event + '/with/key/' + key;
request.post({
url: url,
json: {
value1: notify.title,
value2: notify.message,
value3: Date.now()
}
}, callback);
});
});
};maker.sendAllClear = function(notify, callback) {
if (Date.now() - lastAllClear > 30 * 60 * 1000) {
lastAllClear = Date.now();
var key = keys[0];
var url = 'https://maker.ifttt.com/trigger/ns-allclear/with/key/' + key;
request.post({
url: url,
json: {
value1: notify.title,
value2: notify.message
}
}, callback);
}
};Location: lib/server/websocket.js, lib/api3/alarmSocket.js
Broadcast Flow:
ctx.bus.on('notification', function(notify) {
var event = mapLevelToEvent(notify.level);
if (notify.isAnnouncement) {
io.emit('announcement', notify);
} else if (notify.clear) {
io.emit('clear_alarm', {});
} else {
io.emit(event, notify); // 'alarm' or 'urgent_alarm'
}
});socket.on('alarm', function(alarm) {
// Show notification
showDesktopNotification(alarm);
// Play sound
playAlarmSound(alarm.level);
// Update UI
showAlarmModal(alarm);
});
socket.on('urgent_alarm', function(alarm) {
// More aggressive notification
showUrgentNotification(alarm);
playUrgentSound();
});
socket.on('clear_alarm', function() {
// Dismiss notifications
hideAlarmModal();
stopAlarmSound();
});function showDesktopNotification(alarm) {
if (Notification.permission === 'granted') {
new Notification(alarm.title, {
body: alarm.message,
icon: '/images/logo.png',
tag: 'nightscout-alarm-' + alarm.level,
requireInteraction: true
});
}
}| Source | Method | Scope |
|---|---|---|
| Web UI | WebSocket ack |
Local + server |
| Pushover | Callback POST | Server + cancel loop |
| API | GET /notifications/ack | Server |
// Client sends ack
socket.emit('ack', level, group, silenceTime);
// Server handles
socket.on('ack', function(level, group, silenceTime) {
ctx.notifications.ack(level, group, silenceTime);
// Broadcast clear to all clients
ctx.bus.emit('notification', {
clear: true,
title: 'All Clear',
message: 'Alarm acknowledged',
group: group
});
});Endpoint: GET /api/v1/notifications/ack
Parameters:
level- Alarm level (1 or 2)group- Notification grouptime- Silence duration (ms)
api.get('/notifications/ack',
ctx.authorization.isPermitted('notifications:*:ack'),
function(req, res) {
var level = Number(req.query.level);
var group = req.query.group || 'default';
var time = Number(req.query.time) || 1800000; // 30 min default
ctx.notifications.ack(level, group, time);
res.sendStatus(200);
}
);| Method | Default Duration | Configurable |
|---|---|---|
| Web UI | 30 minutes | Yes (button presets) |
| Pushover | Until expired | Implicit |
| API | 30 minutes | Yes (query param) |
| Failure | Impact | Mitigation |
|---|---|---|
| Pushover API down | No push notifications | Retry logic, alternative channel |
| IFTTT unavailable | No webhook events | Silent failure (acceptable) |
| Network partition | Delayed notifications | Queue locally, retry |
| Server crash | Lost in-memory state | Events reconstructed on reload |
Current State: Limited retry for Pushover, none for Maker
Recommendation:
async function sendWithRetry(fn, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (err) {
if (i === maxRetries - 1) throw err;
await sleep(delay * Math.pow(2, i));
}
}
}Current State: In-memory only
Recommendation:
- Add Redis queue for pending notifications
- Survive server restarts
- Enable horizontal scaling
console.info('EMITTING ALARM:', JSON.stringify(notify));
console.log('Skipping duplicate notification');
console.error('Pushover send failed:', err);| Metric | Type | Purpose |
|---|---|---|
notifications_emitted_total |
Counter | Total by level |
notifications_acknowledged_total |
Counter | Ack rate |
pushover_send_duration_ms |
Histogram | Latency |
pushover_failures_total |
Counter | Error rate |
maker_events_sent_total |
Counter | Volume |
| Condition | Threshold | Action |
|---|---|---|
| Pushover failure rate | >10% in 5min | Alert ops |
| Notification latency | >30s p99 | Warn |
| Queue depth | >100 | Scale |
| Issue | Impact | Recommendation |
|---|---|---|
| No message queue | Lost notifications on crash | Add Redis queue |
Deprecated request library |
Security risk | Migrate to axios |
| No retry logic for IFTTT | Silent failures | Add retry with backoff |
| Area | Current | Recommended |
|---|---|---|
| Dedup window | 15 minutes | Configurable |
| Retry strategy | None | Exponential backoff |
| Failure logging | Basic | Structured logging |
| Rate limiting | None | Per-channel limits |
Consider adding support for:
-
Twilio SMS:
- Critical for non-smartphone users
- Reliable delivery
-
Email:
- Summary/digest notifications
- Non-critical alerts
-
Slack/Discord:
- Team notifications
- Care team coordination
-
Apple Push (APN):
- Native iOS app support
- Already has dependency (
@parse/node-apn)
| Data | Risk | Mitigation |
|---|---|---|
| API keys | Exposure | Environment variables only |
| User keys | Exposure | Never log full keys |
| Health data in messages | Privacy | Minimal message content |
Pushover Callback:
- No signature verification
- Relies on obscure URL
- Consider adding HMAC signature
Current State: Deduplication only (15 min window)
Recommendation:
- Add per-minute rate limits per channel
- Prevent notification storms
- Log rate limit events