Skip to content

🔔 Add Email Notifications for Resource Limit Warnings (with BYOD Support) #47

@yash-pouranik

Description

@yash-pouranik

📋 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:

  1. Alerts users when approaching resource limits
  2. Supports custom thresholds for BYOD users (user-defined limits)
  3. Calculates external database usage using MongoDB stats
  4. 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:

  1. User has 500MB storage limit
  2. They upload files and reach 490MB (98% usage)
  3. Next upload fails with 403 error ❌
  4. User is surprised and frustrated

Desired Behavior:

  1. User reaches 400MB (80% of 500MB limit)
  2. System sends email: "You're using 80% of your storage limit" ✅
  3. User gets advance warning and can take action

BYOD User Scenario:

  1. User connects their own MongoDB Atlas database
  2. User sets custom threshold: "Alert me at 2GB usage"
  3. System periodically calculates external DB size using MongoDB stats
  4. When 2GB is reached, email sent ✅

💡 Proposed Solution

Implement a threshold-based email notification system that:

  1. Monitors resource usage when users perform operations
  2. Calculates external DB usage for BYOD users using MongoDB db.stats()
  3. Supports configurable thresholds:
    • Managed DB: Percentage-based (80%, 95% of plan limit)
    • BYOD: Absolute value (user sets: "Alert at 2GB")
  4. Triggers email alerts at configured thresholds
  5. Prevents spam by sending max 1 email per threshold per 7 days
  6. User preferences UI to configure alert thresholds

✅ Acceptance Criteria

Backend Requirements

  • Storage Limit Monitoring

    • Modify backend/controllers/storage.controller.js to check usage percentage
    • Trigger email when usage crosses 80% threshold
    • Track when last notification was sent (avoid spamming)
  • Database Limit Monitoring

    • Modify backend/controllers/data.controller.js to 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
  • 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
  • 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
  • Notification Tracking & Preferences

    • Add field to Project schema: notificationSettings and lastLimitNotification
    • 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 }
      }
  • Email Queue Integration

    • Use existing BullMQ (backend/queues/emailQueue.js)
    • Add job type: limit-warning
    • Handle retry logic if email fails

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

  • enhancement
  • good first issue ❌ (This is intermediate)
  • intermediate
  • backend
  • frontend
  • email
  • help 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.js uses 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! 🎯

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions