-
Notifications
You must be signed in to change notification settings - Fork 24
🔔 Add Email Notifications for Resource Limit Warnings (with BYOD Support) #47
Description
📋 Description
Currently, when users exceed their database or storage limits, they only receive HTTP 403 errors when attempting operations. There's no proactive notification system to warn users before they hit these limits, which can lead to:
- Unexpected service disruptions
- Poor user experience
- Users not realizing they're approaching limits
Additional Challenge: For users with BYOD (Bring Your Own Database), the current system doesn't track usage at all, making limit warnings impossible.
This feature will implement an email notification system that:
- Alerts users when approaching resource limits
- Supports custom thresholds for BYOD users (user-defined limits)
- Calculates external database usage using MongoDB stats
- Allows users to configure alert preferences
🎯 Problem Statement
Current Behavior:
- Users only know about limit issues when operations fail (403 errors)
- No proactive warnings before hitting limits
- Email infrastructure exists but isn't used for limit notifications
Example scenario:
- User has 500MB storage limit
- They upload files and reach 490MB (98% usage)
- Next upload fails with 403 error ❌
- User is surprised and frustrated
Desired Behavior:
- User reaches 400MB (80% of 500MB limit)
- System sends email: "You're using 80% of your storage limit" ✅
- User gets advance warning and can take action
BYOD User Scenario:
- User connects their own MongoDB Atlas database
- User sets custom threshold: "Alert me at 2GB usage"
- System periodically calculates external DB size using MongoDB stats
- When 2GB is reached, email sent ✅
💡 Proposed Solution
Implement a threshold-based email notification system that:
- Monitors resource usage when users perform operations
- Calculates external DB usage for BYOD users using MongoDB
db.stats() - Supports configurable thresholds:
- Managed DB: Percentage-based (80%, 95% of plan limit)
- BYOD: Absolute value (user sets: "Alert at 2GB")
- Triggers email alerts at configured thresholds
- Prevents spam by sending max 1 email per threshold per 7 days
- User preferences UI to configure alert thresholds
✅ Acceptance Criteria
Backend Requirements
-
Storage Limit Monitoring
- Modify
backend/controllers/storage.controller.jsto check usage percentage - Trigger email when usage crosses 80% threshold
- Track when last notification was sent (avoid spamming)
- Modify
-
Database Limit Monitoring
- Modify
backend/controllers/data.controller.jsto check database size - For BYOD databases: Calculate usage using
db.stats()via external connection - For managed databases: Use existing tracking + percentage thresholds
- Trigger email based on user-configured thresholds
- Track notification history
- Modify
-
External Database Usage Calculation
- Create utility:
backend/utils/calculateExternalDbSize.js - Use MongoDB's
db.stats()to get database size - Cache results for 1 hour to avoid excessive queries
- Handle connection errors gracefully
- Create utility:
-
Email Template
- Create new email template:
backend/utils/emailTemplates/limitWarning.js - Include: current usage, limit, percentage, next steps
- Support both storage and database limit warnings
- Create new email template:
-
Notification Tracking & Preferences
- Add field to Project schema:
notificationSettingsandlastLimitNotification - Schema structure:
notificationSettings: { email: { enabled: { type: Boolean, default: true }, storage: { type: String, // 'percentage' or 'absolute' thresholds: [80, 95], // For percentage-based absoluteLimit: Number // For BYOD: alert at specific MB }, database: { type: String, // 'percentage' or 'absolute' thresholds: [80, 95], absoluteLimit: Number // For BYOD: alert at specific MB } } }, lastLimitNotification: { storage: { threshold80: Date, threshold95: Date, custom: Date }, database: { threshold80: Date, threshold95: Date, custom: Date } }, cachedUsageStats: { database: { size: Number, lastCalculated: Date }, storage: { size: Number, lastCalculated: Date } }
- Add field to Project schema:
-
Email Queue Integration
- Use existing BullMQ (
backend/queues/emailQueue.js) - Add job type:
limit-warning - Handle retry logic if email fails
- Use existing BullMQ (
Frontend Requirements
-
User Settings UI - Project Settings Page
- Add "Alert Preferences" section in project settings
- Toggle: "Enable email alerts for resource limits"
- For managed DB/storage: Percentage selector (50%, 80%, 95%)
- For BYOD: Absolute value input (e.g., "Alert me at: 2000 MB")
- Show current usage stats with progress bar
- Auto-detect if project uses BYOD and show appropriate UI
-
Visual Warnings (Optional Enhancement)
- Show warning banner in dashboard when approaching limits
- Color-coded: Yellow at 80%, Red at 95%
- For BYOD: Show "Last calculated: 2 hours ago"
Testing
-
Unit Tests
- Test threshold calculation logic
- Test notification timing (7-day cooldown)
- Test email template rendering
-
Integration Tests
- Test full flow: upload → threshold check → email sent
- Verify no duplicate emails within 7 days
🛠️ Technical Implementation Guide
Files to Modify/Create
1️⃣ Create External DB Usage Calculator
File: backend/utils/calculateExternalDbSize.js (NEW)
const connectionManager = require('./connection.manager');
const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in ms
const calculateExternalDbSize = async (project) => {
// Check cache first
const cached = project.cachedUsageStats?.database;
if (cached && (Date.now() - new Date(cached.lastCalculated)) < CACHE_DURATION) {
return cached.size;
}
// If external database (BYOD)
if (project.databaseConfig?.isExternal) {
try {
const connection = await connectionManager.getConnection(project._id);
const stats = await connection.db.stats();
const sizeInMB = Math.round(stats.dataSize / (1024 * 1024));
// Update cache
await project.updateOne({
'cachedUsageStats.database': {
size: sizeInMB,
lastCalculated: new Date()
}
});
return sizeInMB;
} catch (error) {
console.error('Failed to calculate external DB size:', error);
return cached?.size || 0; // Return cached value on error
}
}
// For internal/managed databases, use existing tracking
return project.databaseSizeMB || 0;
};
module.exports = { calculateExternalDbSize };2️⃣ Create Email Template
File: backend/utils/emailTemplates/limitWarning.js
const limitWarningTemplate = ({ userName, projectName, resourceType, currentUsage, limit, percentage, isBYOD }) => {
const alertTypeText = isBYOD
? `Your custom alert threshold of <strong>${limit}</strong> has been reached.`
: `You're currently using <strong>${percentage}%</strong> of your ${resourceType} limit.`;
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.warning-box { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
.stats { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 15px 0; }
.action-button { background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 15px; }
</style>
</head>
<body>
<div class="container">
<h2>⚠️ Resource Limit Alert</h2>
<p>Hi ${userName},</p>
<div class="warning-box">
<strong>Project:</strong> ${projectName}<br>
${alertTypeText}
</div>
<div class="stats">
<strong>Current ${resourceType} usage:</strong> ${currentUsage}<br>
${!isBYOD ? `<strong>Plan limit:</strong> ${limit}<br>` : ''}
${!isBYOD ? `<strong>Usage:</strong> ${percentage}%` : ''}
</div>
<p><strong>What you can do:</strong></p>
<ul>
<li>Delete unused ${resourceType === 'storage' ? 'files' : 'data'}</li>
${!isBYOD ? '<li>Upgrade your plan for higher limits</li>' : '<li>Increase your database capacity</li>'}
<li>Review and optimize your ${resourceType} usage</li>
${isBYOD ? '<li>Adjust alert thresholds in project settings</li>' : ''}
</ul>
<a href="https://urbackend.com/dashboard" class="action-button">Go to Dashboard</a>
<p style="color: #666; font-size: 12px; margin-top: 30px;">
${isBYOD ? 'This is a custom alert you configured.' : 'You will receive this alert once per threshold.'}
You can manage alert preferences in your project settings.
</p>
<p>Best regards,<br>urBackend Team</p>
</div>
</body>
</html>
`;
};
module.exports = limitWarningTemplate;3️⃣ Update Project Model
File: backend/models/Project.js
Add notification settings and tracking:
notificationSettings: {
type: Object,
default: {
email: {
enabled: true,
storage: {
type: 'percentage', // or 'absolute'
thresholds: [80, 95],
absoluteLimit: null // For BYOD: alert at specific MB
},
database: {
type: 'percentage',
thresholds: [80, 95],
absoluteLimit: null
}
}
}
},
lastLimitNotification: {
type: Object,
default: {
storage: { threshold80: null, threshold95: null, custom: null },
database: { threshold80: null, threshold95: null, custom: null }
}
},
cachedUsageStats: {
type: Object,
default: {
database: { size: 0, lastCalculated: null },
storage: { size: 0, lastCalculated: null }
}
}4️⃣ Create Notification Helper
File: backend/utils/limitNotificationHelper.js (NEW)
const emailQueue = require('../queues/emailQueue');
const limitWarningTemplate = require('./emailTemplates/limitWarning');
const { calculateExternalDbSize } = require('./calculateExternalDbSize');
const COOLDOWN_DAYS = 7;
const shouldSendNotification = (lastSent) => {
if (!lastSent) return true;
const daysSince = (Date.now() - new Date(lastSent)) / (1000 * 60 * 60 * 24);
return daysSince >= COOLDOWN_DAYS;
};
const checkAndNotify = async ({ project, resourceType, currentUsageMB, owner }) => {
// Check if notifications are enabled
const settings = project.notificationSettings?.email;
if (!settings?.enabled) return;
const resourceSettings = settings[resourceType];
const isBYOD = project.databaseConfig?.isExternal || project.storageConfig?.isExternal;
let shouldAlert = false;
let threshold = null;
let percentage = null;
// For BYOD with absolute limits
if (resourceSettings.type === 'absolute' && resourceSettings.absoluteLimit) {
if (currentUsageMB >= resourceSettings.absoluteLimit) {
shouldAlert = true;
threshold = 'custom';
}
}
// For managed resources with percentage thresholds
else if (resourceSettings.type === 'percentage') {
const limit = resourceType === 'storage'
? project.storageLimitMB
: project.databaseLimitMB;
percentage = (currentUsageMB / limit) * 100;
// Find highest crossed threshold
const thresholds = resourceSettings.thresholds || [80, 95];
for (const thresh of thresholds.sort((a, b) => b - a)) {
if (percentage >= thresh) {
shouldAlert = true;
threshold = `threshold${thresh}`;
break;
}
}
}
if (!shouldAlert) return;
// Check cooldown period
const lastSent = project.lastLimitNotification?.[resourceType]?.[threshold];
if (!shouldSendNotification(lastSent)) return;
// Send email
await emailQueue.add('limit-warning', {
email: owner.email,
userName: owner.name,
projectName: project.name,
resourceType,
currentUsage: `${currentUsageMB}MB`,
limit: resourceSettings.type === 'absolute'
? `${resourceSettings.absoluteLimit}MB`
: `${resourceType === 'storage' ? project.storageLimitMB : project.databaseLimitMB}MB`,
percentage: percentage ? Math.round(percentage) : null,
isBYOD
});
// Update last notification timestamp
const updatePath = `lastLimitNotification.${resourceType}.${threshold}`;
await project.updateOne({ [updatePath]: new Date() });
console.log(`✅ Limit alert sent for ${resourceType} on project ${project.name}`);
};
module.exports = { checkAndNotify };5️⃣ Modify Storage Controller
File: backend/controllers/storage.controller.js
Add notification check in upload function (around line 21):
const { checkAndNotify } = require('../utils/limitNotificationHelper');
const User = require('../models/User');
// After calculating current storage usage
const currentUsageMB = project.storageUsed || 0;
const owner = await User.findById(project.owner);
// Check and send notification if needed
await checkAndNotify({
project,
resourceType: 'storage',
currentUsageMB,
owner
});6️⃣ Modify Data Controller
File: backend/controllers/data.controller.js
Add notification check in POST/PUT operations (around line 34):
const { checkAndNotify } = require('../utils/limitNotificationHelper');
const { calculateExternalDbSize } = require('../utils/calculateExternalDbSize');
const User = require('../models/User');
// Calculate database size (works for both managed and BYOD)
const currentUsageMB = await calculateExternalDbSize(project);
const owner = await User.findById(project.owner);
await checkAndNotify({
project,
resourceType: 'database',
currentUsageMB,
owner
});7️⃣ Update Email Queue
File: backend/queues/emailQueue.js
Add new job processor for limit-warning:
emailQueue.process('limit-warning', async (job) => {
const { email, userName, projectName, resourceType, currentUsage, limit, percentage, isBYOD } = job.data;
const html = limitWarningTemplate({
userName,
projectName,
resourceType,
currentUsage,
limit,
percentage,
isBYOD
});
const subject = isBYOD
? `⚠️ ${resourceType} alert: ${currentUsage} reached`
: `⚠️ ${percentage}% ${resourceType} limit reached`;
await sendEmail({
to: email,
subject,
html
});
});8️⃣ Frontend: Project Settings UI
File: frontend/src/pages/ProjectSettings.jsx (or create new component)
Add alert configuration UI:
const AlertSettings = ({ project, onUpdate }) => {
const [settings, setSettings] = useState(project.notificationSettings?.email);
const isBYOD = project.databaseConfig?.isExternal;
return (
<div className="alert-settings">
<h3>📧 Email Alert Preferences</h3>
{/* Enable/Disable Toggle */}
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={settings.enabled}
onChange={(e) => setSettings({...settings, enabled: e.target.checked})}
/>
Enable email alerts for resource limits
</label>
{settings.enabled && (
<>
{/* Database Alerts */}
<div className="mt-4">
<h4>Database Alerts</h4>
{isBYOD ? (
<div>
<label>Alert when database size reaches:</label>
<input
type="number"
placeholder="e.g., 2000"
value={settings.database.absoluteLimit || ''}
onChange={(e) => setSettings({
...settings,
database: {
...settings.database,
type: 'absolute',
absoluteLimit: parseInt(e.target.value)
}
})}
/>
<span>MB</span>
</div>
) : (
<div>
<label>Alert at usage percentages:</label>
<select
multiple
value={settings.database.thresholds}
onChange={(e) => {
const values = Array.from(e.target.selectedOptions, opt => parseInt(opt.value));
setSettings({
...settings,
database: {...settings.database, thresholds: values}
});
}}
>
<option value={50}>50%</option>
<option value={80}>80%</option>
<option value={95}>95%</option>
</select>
</div>
)}
</div>
{/* Storage Alerts - Similar structure */}
<div className="mt-4">
<h4>Storage Alerts</h4>
{/* Similar to database alerts */}
</div>
{/* Current Usage Display */}
<div className="usage-stats mt-4">
<h4>Current Usage</h4>
<div className="progress-bar">
<div>Database: {project.cachedUsageStats?.database?.size || 0} MB</div>
<div>Storage: {project.cachedUsageStats?.storage?.size || 0} MB</div>
</div>
{isBYOD && (
<small>Last calculated: {new Date(project.cachedUsageStats?.database?.lastCalculated).toLocaleString()}</small>
)}
</div>
</>
)}
<button onClick={() => onUpdate(settings)}>Save Alert Settings</button>
</div>
);
};9️⃣ Frontend: API Endpoint for Settings
File: backend/routes/project.routes.js
Add endpoint to update notification settings:
router.patch('/projects/:id/notification-settings', authMiddleware, async (req, res) => {
try {
const project = await Project.findById(req.params.id);
if (project.owner.toString() !== req.user._id.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
project.notificationSettings = {
...project.notificationSettings,
email: req.body.email
};
await project.save();
res.json({ success: true, settings: project.notificationSettings });
} catch (error) {
res.status(500).json({ error: error.message });
}
});📊 Testing Checklist
Manual Testing
Managed Database/Storage:
- Upload file to reach 80% storage → verify email sent
- Upload more to reach 95% → verify second email sent
- Upload again within 7 days → verify NO duplicate email
- Insert data to reach 80% database limit → verify email sent
- Toggle notification preference OFF → verify no emails sent
BYOD (External Database):
- Set custom threshold: "Alert at 500MB"
- Insert data until 500MB → verify email sent with correct message
- Verify
db.stats()is called to calculate usage - Verify usage is cached for 1 hour
- Test connection failure handling (graceful degradation)
- Change threshold to 1000MB → verify new threshold is used
Automated Tests
- Write test for threshold calculation
- Write test for cooldown period logic
- Write test for email template rendering
- Mock BullMQ and verify job added to queue
🏷️ Labels
enhancementgood first issue❌ (This is intermediate)intermediate✅backendfrontendemailhelp wanted
📚 Resources
Existing Code to Reference:
- Email service:
backend/utils/emailService.js - Email queue:
backend/queues/emailQueue.js - Storage limit check:
backend/controllers/storage.controller.js(line 21) - Database limit check:
backend/controllers/data.controller.js(line 34) - BYOD connection manager:
backend/utils/connection.manager.js⭐ (critical for external DB access) - Database encryption:
backend/utils/encryption.js(how BYOD credentials are stored) - Resend API docs: https://resend.com/docs
- MongoDB
db.stats()docs: https://www.mongodb.com/docs/manual/reference/method/db.stats/
Similar Patterns in Codebase:
- OTP email template:
backend/utils/emailTemplates/(if exists) - Existing BullMQ jobs:
backend/queues/ - Dynamic connection handling: See how
injectModel.jsuses connection manager
🎓 Learning Outcomes
By completing this issue, you will learn:
- ✅ Job queue systems (BullMQ)
- ✅ Email template design
- ✅ Threshold-based monitoring (percentage vs absolute)
- ✅ Multi-tenancy patterns (handling BYOD vs managed resources)
- ✅ MongoDB stats and usage calculation (
db.stats()) - ✅ Connection pooling for external databases
- ✅ Caching strategies to reduce database calls
- ✅ Schema design for flexible configurations
- ✅ Preventing duplicate notifications (cooldown logic)
- ✅ Full-stack feature development
- ✅ Graceful error handling for external service failures
💬 Questions?
Feel free to ask questions in the comments if you need:
- Clarification on implementation approach
- Help understanding existing code structure
- Guidance on testing strategy
🚀 Estimated Time
Time to complete: 5-7 hours (for intermediate developer)
Breakdown:
- External DB usage calculator: 1 hour
- Backend notification logic: 2 hours
- Email template: 30 minutes
- Schema updates: 30 minutes
- Frontend UI (alert settings): 1.5 hours
- Testing (manual + automated): 1.5 hours
👥 Assignee
Looking for an intermediate-advanced contributor who has:
- Solid understanding of Node.js and Express
- Experience with MongoDB/Mongoose (especially
db.stats()and connection management) - Understanding of multi-tenancy patterns
- Comfortable with email systems and job queues
- (Bonus) Familiar with caching strategies
- (Bonus) Experience with external service integrations
Difficulty Level: ⭐⭐⭐ Intermediate-Advanced (upgraded from basic intermediate due to BYOD complexity)
Ready to tackle this? Drop a comment and get started! 🎯