Learn how to add real-time stock alerts to your web app with this easy, step-by-step guide. Stay updated and boost user engagement!

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Stock Alerts Matter for Your Platform
Adding stock alerts to your web application isn't just a nice-to-have feature—it's a powerful engagement tool that keeps users coming back. When implemented well, stock alerts transform your platform from a passive information portal into an active financial companion that works for your users even when they're not logged in.
Core Components You'll Need
Before you can alert on price movements, you need reliable market data. Your options include:
Here's how you might set up a connection with a REST API:
// services/stockDataService.js
class StockDataService {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.example-stock-provider.com/v1';
}
async getCurrentPrice(symbol) {
try {
const response = await fetch(`${this.baseUrl}/stocks/${symbol}/quote?apiKey=${this.apiKey}`);
const data = await response.json();
// Handle potential API-specific structure
return {
symbol: data.symbol,
price: data.lastPrice,
timestamp: data.lastUpdated,
};
} catch (error) {
console.error(`Failed to fetch price for ${symbol}:`, error);
throw error;
}
}
}
module.exports = StockDataService;
Pro Tip: Implement a caching layer to reduce API calls and costs. Most stock prices don't need millisecond-level updates for alert purposes.
Your database needs to efficiently store alert conditions and associate them with users:
CREATE TABLE stock_alerts (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
symbol VARCHAR(10) NOT NULL,
alert_type VARCHAR(20) NOT NULL, -- 'price_above', 'price_below', 'percent_change', etc.
threshold DECIMAL(10, 2) NOT NULL,
frequency VARCHAR(20) DEFAULT 'once', -- 'once', 'always', 'daily'
status VARCHAR(20) DEFAULT 'active', -- 'active', 'triggered', 'paused'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_triggered_at TIMESTAMP NULL,
notification_methods JSONB DEFAULT '{"email": true, "push": false, "sms": false}'
);
-- Index for faster querying of active alerts
CREATE INDEX idx_active_alerts ON stock_alerts(status) WHERE status = 'active';
-- Index for finding a user's alerts quickly
CREATE INDEX idx_user_alerts ON stock_alerts(user_id);
The alert engine is the heart of your system. It needs to:
Here's a sample implementation using Node.js:
// services/alertEngine.js
const StockDataService = require('./stockDataService');
const NotificationService = require('./notificationService');
const db = require('../database');
class AlertEngine {
constructor() {
this.stockData = new StockDataService(process.env.STOCK_API_KEY);
this.notifier = new NotificationService();
}
async processAlerts() {
// Get all active alerts
const activeAlerts = await db.query(
'SELECT * FROM stock_alerts WHERE status = $1',
['active']
);
// Group alerts by symbol to minimize API calls
const alertsBySymbol = this.groupAlertsBySymbol(activeAlerts);
// Process each symbol's alerts
for (const [symbol, alerts] of Object.entries(alertsBySymbol)) {
try {
// Get current price once per symbol
const { price, timestamp } = await this.stockData.getCurrentPrice(symbol);
// Check each alert against the current price
for (const alert of alerts) {
await this.evaluateAlert(alert, price, timestamp);
}
} catch (error) {
console.error(`Error processing alerts for ${symbol}:`, error);
}
}
console.log(`Alert engine run completed at ${new Date().toISOString()}`);
}
groupAlertsBySymbol(alerts) {
return alerts.reduce((acc, alert) => {
if (!acc[alert.symbol]) {
acc[alert.symbol] = [];
}
acc[alert.symbol].push(alert);
return acc;
}, {});
}
async evaluateAlert(alert, currentPrice, timestamp) {
let isTriggered = false;
// Evaluate based on alert type
switch (alert.alert_type) {
case 'price_above':
isTriggered = currentPrice >= alert.threshold;
break;
case 'price_below':
isTriggered = currentPrice <= alert.threshold;
break;
case 'percent_change':
// Implementation for percent change would require previous price
break;
}
if (isTriggered) {
await this.triggerAlert(alert, currentPrice, timestamp);
}
}
async triggerAlert(alert, price, timestamp) {
// Prepare notification data
const notificationData = {
userId: alert.user_id,
symbol: alert.symbol,
alertType: alert.alert_type,
threshold: alert.threshold,
currentPrice: price,
timestamp: timestamp
};
// Send notifications through selected channels
if (alert.notification_methods.email) {
await this.notifier.sendEmail(notificationData);
}
if (alert.notification_methods.push) {
await this.notifier.sendPushNotification(notificationData);
}
if (alert.notification_methods.sms) {
await this.notifier.sendSMS(notificationData);
}
// Update alert status based on frequency
if (alert.frequency === 'once') {
await db.query(
'UPDATE stock_alerts SET status = $1, last_triggered_at = $2 WHERE id = $3',
['triggered', new Date(), alert.id]
);
} else {
await db.query(
'UPDATE stock_alerts SET last_triggered_at = $1 WHERE id = $2',
[new Date(), alert.id]
);
}
}
}
module.exports = AlertEngine;
For most applications, you'll want to check alert conditions at regular intervals. The frequency depends on your users' needs and your resources:
Using a job scheduler like node-cron:
// scheduler.js
const cron = require('node-cron');
const AlertEngine = require('./services/alertEngine');
const alertEngine = new AlertEngine();
// Run every 5 minutes during market hours (9:30 AM - 4:00 PM ET), Monday to Friday
cron.schedule('*/5 9-16 * * 1-5', async () => {
// Skip if outside of 9:30 AM - 4:00 PM Eastern time
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
// Convert to ET for comparison (simplified example)
if ((hours === 9 && minutes < 30) || hours < 9 || hours >= 16) {
return;
}
console.log('Running alert check...');
await alertEngine.processAlerts();
}, {
timezone: "America/New_York"
});
// Additionally run once at server start to process any missed alerts
alertEngine.processAlerts().catch(console.error);
Scaling Tip: As your user base grows, consider moving to a distributed task queue like Bull or Celery, which can handle hundreds of thousands of alerts across multiple worker nodes.
The alert creation UI should be intuitive while offering enough flexibility. Here's a React component example:
// components/StockAlertForm.jsx
import React, { useState } from 'react';
import axios from 'axios';
const StockAlertForm = ({ userId, onSuccess }) => {
const [alertData, setAlertData] = useState({
symbol: '',
alertType: 'price_above',
threshold: '',
frequency: 'once',
notificationMethods: {
email: true,
push: false,
sms: false
}
});
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('/api/alerts', {
userId,
...alertData
});
if (response.status === 201) {
onSuccess(response.data);
// Reset form
setAlertData({
symbol: '',
alertType: 'price_above',
threshold: '',
frequency: 'once',
notificationMethods: {
email: true,
push: false,
sms: false
}
});
}
} catch (error) {
console.error('Failed to create alert:', error);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setAlertData(prev => ({ ...prev, [name]: value }));
};
const handleNotificationChange = (method) => {
setAlertData(prev => ({
...prev,
notificationMethods: {
...prev.notificationMethods,
[method]: !prev.notificationMethods[method]
}
}));
};
return (
<form onSubmit={handleSubmit} className="stock-alert-form">
<h3>Create Stock Price Alert</h3>
<div className="form-group">
<label htmlFor="symbol">Stock Symbol</label>
<input
type="text"
id="symbol"
name="symbol"
value={alertData.symbol}
onChange={handleChange}
placeholder="AAPL"
required
/>
</div>
<div className="form-group">
<label htmlFor="alertType">Alert Type</label>
<select
id="alertType"
name="alertType"
value={alertData.alertType}
onChange={handleChange}
required
>
<option value="price_above">Price Above</option>
<option value="price_below">Price Below</option>
<option value="percent_change">Percent Change</option>
</select>
</div>
<div className="form-group">
<label htmlFor="threshold">Threshold Value</label>
<input
type="number"
id="threshold"
name="threshold"
value={alertData.threshold}
onChange={handleChange}
step="0.01"
required
/>
</div>
<div className="form-group">
<label htmlFor="frequency">Alert Frequency</label>
<select
id="frequency"
name="frequency"
value={alertData.frequency}
onChange={handleChange}
>
<option value="once">Once</option>
<option value="always">Every time condition is met</option>
<option value="daily">Once per day</option>
</select>
</div>
<div className="form-group">
<label>Notification Methods</label>
<div className="checkbox-group">
<label>
<input
type="checkbox"
checked={alertData.notificationMethods.email}
onChange={() => handleNotificationChange('email')}
/>
Email
</label>
<label>
<input
type="checkbox"
checked={alertData.notificationMethods.push}
onChange={() => handleNotificationChange('push')}
/>
Push Notification
</label>
<label>
<input
type="checkbox"
checked={alertData.notificationMethods.sms}
onChange={() => handleNotificationChange('sms')}
/>
SMS
</label>
</div>
</div>
<button type="submit" className="submit-button">Create Alert</button>
</form>
);
};
export default StockAlertForm;
A flexible notification service should support multiple channels:
// services/notificationService.js
const nodemailer = require('nodemailer');
const twilio = require('twilio');
const webpush = require('web-push');
const db = require('../database');
class NotificationService {
constructor() {
// Set up email transport
this.emailTransporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
// Set up SMS client
this.smsClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
// Configure web push
webpush.setVapidDetails(
'mailto:[email protected]',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
}
async sendEmail({ userId, symbol, alertType, threshold, currentPrice }) {
// Get user email
const { email } = await db.query('SELECT email FROM users WHERE id = $1', [userId])
.then(results => results[0]);
if (!email) {
throw new Error(`No email found for user ${userId}`);
}
// Format alert message based on type
const alertMessage = this.formatAlertMessage(alertType, symbol, threshold, currentPrice);
// Send email
await this.emailTransporter.sendMail({
from: '"Stock Alert Service" <[email protected]>',
to: email,
subject: `Stock Alert: ${symbol} ${alertType === 'price_above' ? 'Above' : 'Below'} ${threshold}`,
html: `
<div style="font-family: Arial, sans-serif; padding: 20px; max-width: 600px;">
<h2>Stock Alert Triggered</h2>
<p>${alertMessage}</p>
<p>Current price: <strong>$${currentPrice.toFixed(2)}</strong></p>
<p>Alert threshold: <strong>$${threshold.toFixed(2)}</strong></p>
<div style="margin-top: 30px;">
<a href="https://yourdomain.com/alerts" style="padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 4px;">
Manage Your Alerts
</a>
</div>
</div>
`
});
}
async sendSMS({ userId, symbol, alertType, threshold, currentPrice }) {
// Get user phone number
const { phone } = await db.query('SELECT phone FROM users WHERE id = $1', [userId])
.then(results => results[0]);
if (!phone) {
throw new Error(`No phone number found for user ${userId}`);
}
// Format alert message
const alertMessage = this.formatAlertMessage(alertType, symbol, threshold, currentPrice);
// Send SMS
await this.smsClient.messages.create({
body: `${alertMessage} Current price: $${currentPrice.toFixed(2)}`,
from: process.env.TWILIO_PHONE_NUMBER,
to: phone
});
}
async sendPushNotification({ userId, symbol, alertType, threshold, currentPrice }) {
// Get user's push subscription
const { push_subscription } = await db.query(
'SELECT push_subscription FROM user_notifications WHERE user_id = $1',
[userId]
).then(results => results[0]);
if (!push_subscription) {
throw new Error(`No push subscription found for user ${userId}`);
}
const subscription = JSON.parse(push_subscription);
// Format alert message
const alertMessage = this.formatAlertMessage(alertType, symbol, threshold, currentPrice);
// Send push notification
await webpush.sendNotification(subscription, JSON.stringify({
title: `${symbol} Alert Triggered`,
body: alertMessage,
icon: '/stock-icon.png',
data: {
url: `/stocks/${symbol}`
}
}));
}
formatAlertMessage(alertType, symbol, threshold, currentPrice) {
switch (alertType) {
case 'price_above':
return `${symbol} is now trading above your alert threshold of $${threshold.toFixed(2)}.`;
case 'price_below':
return `${symbol} is now trading below your alert threshold of $${threshold.toFixed(2)}.`;
case 'percent_change':
const direction = currentPrice > threshold ? 'up' : 'down';
return `${symbol} has moved ${direction} by ${Math.abs(threshold)}%.`;
default:
return `Alert triggered for ${symbol}.`;
}
}
}
module.exports = NotificationService;
Ensure reliability with comprehensive testing:
// tests/alertEngine.test.js
const AlertEngine = require('../services/alertEngine');
const StockDataService = require('../services/stockDataService');
const NotificationService = require('../services/notificationService');
const db = require('../database');
// Mock dependencies
jest.mock('../services/stockDataService');
jest.mock('../services/notificationService');
jest.mock('../database');
describe('AlertEngine', () => {
let alertEngine;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Create instance with mocked dependencies
alertEngine = new AlertEngine();
});
test('should trigger alert when price is above threshold', async () => {
// Mock database response
db.query.mockResolvedValueOnce([{
id: 1,
user_id: 123,
symbol: 'AAPL',
alert_type: 'price_above',
threshold: 150.00,
frequency: 'once',
status: 'active',
notification_methods: { email: true, push: false, sms: false }
}]);
// Mock current price
alertEngine.stockData.getCurrentPrice.mockResolvedValueOnce({
symbol: 'AAPL',
price: 155.75,
timestamp: '2023-01-15T12:34:56Z'
});
// Run the engine
await alertEngine.processAlerts();
// Verify notification was sent
expect(alertEngine.notifier.sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
userId: 123,
symbol: 'AAPL',
alertType: 'price_above',
threshold: 150.00,
currentPrice: 155.75
})
);
// Verify alert status was updated
expect(db.query).toHaveBeenCalledWith(
'UPDATE stock_alerts SET status = $1, last_triggered_at = $2 WHERE id = $3',
['triggered', expect.any(Date), 1]
);
});
test('should not trigger alert when condition is not met', async () => {
// Mock database response
db.query.mockResolvedValueOnce([{
id: 2,
user_id: 123,
symbol: 'AAPL',
alert_type: 'price_below',
threshold: 130.00,
frequency: 'once',
status: 'active',
notification_methods: { email: true }
}]);
// Mock current price (above threshold, so alert shouldn't trigger)
alertEngine.stockData.getCurrentPrice.mockResolvedValueOnce({
symbol: 'AAPL',
price: 155.75,
timestamp: '2023-01-15T12:34:56Z'
});
// Run the engine
await alertEngine.processAlerts();
// Verify no notification was sent
expect(alertEngine.notifier.sendEmail).not.toHaveBeenCalled();
// Verify alert status was not updated
expect(db.query).not.toHaveBeenCalledWith(
expect.stringContaining('UPDATE stock_alerts SET status'),
expect.anything()
);
});
});
Once you have the basics working, consider these enhancements:
As your user base grows, keep these scaling factors in mind:
Stock alerts are mission-critical features that users depend on for financial decisions. A missed alert could mean a missed opportunity or unwanted loss for your users. By following the architecture outlined above and thoroughly testing your implementation, you'll create a dependable alert system that becomes an essential part of your users' investment toolkit.
Remember, the true measure of your alert system isn't just whether it works—it's whether your users trust it enough to rely on it for their investment decisions. Build that trust through reliability, transparency, and thoughtful user experience.
Explore the top 3 practical use cases for adding stock alerts to enhance your web app’s user experience.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â