This guide details how to build a robust SMS marketing campaign application using Node.js, Express, and the MessageBird API. You'll create a system where users can subscribe (opt-in) and unsubscribe (opt-out) from SMS campaigns via text message, and an administrator can broadcast messages to all active subscribers through a simple web interface.
This application solves the common need for businesses to engage customers via SMS while respecting their preferences and complying with regulations requiring clear opt-in/out mechanisms. We'll use Node.js for the backend runtime, Express as the web framework, MongoDB for storing subscriber data, and MessageBird for SMS communication.
Key Features:
- User opt-in via
SUBSCRIBE
keyword. - User opt-out via
STOP
keyword. - Confirmation messages for subscription changes.
- Admin interface to send broadcast messages.
- Basic database storage for subscribers.
Technology Stack:
- Node.js: JavaScript runtime environment.
- Express: Web application framework for Node.js.
- MessageBird: Communications Platform as a Service (CPaaS) for sending/receiving SMS.
- MongoDB: NoSQL database for storing subscriber information.
- dotenv: Module to load environment variables from a
.env
file. (Note: Utilities likelocaltunnel
are useful for development webhook testing but are not part of the core production stack.)
System Architecture:
graph LR
UserMobile -- SMS (SUBSCRIBE/STOP) --> MessageBirdVMN[MessageBird Virtual Number]
MessageBirdVMN -- Webhook --> ExpressApp[Node.js/Express App]
ExpressApp -- DB Query/Update --> MongoDB[MongoDB Database]
ExpressApp -- Send Confirmation SMS --> MessageBirdAPI[MessageBird API]
MessageBirdAPI -- SMS --> UserMobile
AdminBrowser -- HTTP Request --> ExpressApp
ExpressApp -- DB Query (Get Subscribers) --> MongoDB
ExpressApp -- Send Broadcast SMS --> MessageBirdAPI
MessageBirdAPI -- SMS --> ActiveSubscribers[Subscribed User Mobiles]
Prerequisites:
- Node.js and npm: Install from nodejs.org.
- Git: For cloning the repository (optional).
- MessageBird Account: Sign up for free at messagebird.com.
- MongoDB Instance: A running MongoDB server (local or cloud-based like MongoDB Atlas). Get the connection string (URI).
- A Mobile Phone: For testing SMS sending and receiving.
1. Setting up the Project
Let's initialize the project, install dependencies, and set up the basic structure.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir node-messagebird-campaign cd node-messagebird-campaign
-
Initialize Node.js Project: This creates a
package.json
file.npm init -y
-
Install Dependencies: We need Express for the web server, the MessageBird SDK, the MongoDB driver, and
dotenv
for managing environment variables.npm install express messagebird mongodb dotenv
-
Create Project Files: Create the main application file, environment file example, and a
.gitignore
file.touch index.js .env .env.example .gitignore
-
Configure
.gitignore
: Prevent sensitive files and generated folders from being committed to version control. Add the following to.gitignore
:# Environment variables .env # Node dependencies node_modules/ # Log files *.log
-
Set up Environment Variables (
.env.example
and.env
): Define the structure in.env.example
. This file can be committed safely.# .env.example # MessageBird Credentials MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY MESSAGEBIRD_ORIGINATOR=YOUR_MESSAGEBIRD_VIRTUAL_NUMBER_OR_SENDER_ID # MongoDB Connection MONGODB_URI=YOUR_MONGODB_CONNECTION_STRING # Application Port PORT=8080
Now, create your actual
.env
file by copying.env.example
. Do not commit.env
to Git. Fill it with your actual credentials (we'll get these in the next steps).cp .env.example .env
MESSAGEBIRD_API_KEY
: Your Live API key from the MessageBird Dashboard.MESSAGEBIRD_ORIGINATOR
: The virtual mobile number (VMN) you purchase from MessageBird (in E.164 format, e.g.,+12025550135
) or an alphanumeric Sender ID (if supported in your target countries). We'll get this soon.MONGODB_URI
: Your MongoDB connection string (e.g.,mongodb://localhost:27017/sms_campaign
).PORT
: The port your application will run on (defaulting to 8080).
-
Basic Express Server (
index.js
): Set up the initial Express application structure.// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { MongoClient } = require('mongodb'); const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); const app = express(); const port = process.env.PORT || 8080; // Middleware to parse URL-encoded bodies (as sent by HTML forms) app.use(express.urlencoded({ extended: true })); // Middleware to parse JSON bodies (as sent by API clients or MessageBird webhooks) app.use(express.json()); let db; // Database connection variable const SUBSCRIBERS_COLLECTION = 'subscribers'; // Define collection name async function connectDB() { try { const client = new MongoClient(process.env.MONGODB_URI); await client.connect(); db = client.db(); // Assumes database name is in the URI or uses default 'test' console.log(""Successfully connected to MongoDB.""); // Ensure the unique index on 'number' exists await db.collection(SUBSCRIBERS_COLLECTION).createIndex({ number: 1 }, { unique: true }); console.log(`Index on 'number' in '${SUBSCRIBERS_COLLECTION}' collection ensured.`); } catch (err) { console.error(""Failed to connect to MongoDB or ensure index"", err); process.exit(1); // Exit if DB connection or initial setup fails } } // --- Routes Will Go Here --- // Start the server after connecting to the DB connectDB().then(() => { app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); }); });
This sets up the basic project structure, dependencies, environment variable handling, a simple Express server, and initial database connection logic including index creation.
2. Integrating with MessageBird
Now, let's configure MessageBird to send and receive SMS messages.
-
Get MessageBird API Key:
- Log in to your MessageBird Dashboard.
- Navigate to Developers > API access.
- If you don't have a LIVE API key, click ""Add access key"".
- Copy the Live API Key.
- Paste this key into your
.env
file asMESSAGEBIRD_API_KEY
.
-
Purchase a Virtual Mobile Number (VMN): You need a dedicated number to receive incoming messages (SUBSCRIBE/STOP).
- In the MessageBird Dashboard, go to Numbers.
- Click Buy a number.
- Select the desired country.
- Ensure the SMS capability is checked.
- Choose a number from the list.
- Select the billing duration and confirm the purchase.
- Copy the purchased number (in E.164 format, e.g.,
+12025550135
). - Paste this number into your
.env
file asMESSAGEBIRD_ORIGINATOR
. This will also be the sender ID for outgoing messages.- Note: Some countries support alphanumeric sender IDs (e.g., ""MyBrand""). If using one, ensure it's compliant and supported. For receiving, you must use a number.
-
Set up Localtunnel for Webhook Testing: MessageBird needs a publicly accessible URL to send incoming message notifications (webhooks). During development,
localtunnel
exposes your local server.- Install
localtunnel
globally:npm install -g localtunnel
- Run your Node.js application (make sure it's running on the port specified in
.env
, e.g., 8080):node index.js
- Open a new terminal window/tab in the same project directory.
- Start
localtunnel
, pointing it to your local application port:lt --port 8080
localtunnel
will output a public URL (e.g.,https://your-subdomain.loca.lt
). Copy this URL. You'll need it in the next step. This tunnel must remain running during testing. Note: This URL changes every time you restartlocaltunnel
unless you use advanced options.
- Install
-
Configure MessageBird Flow Builder for Incoming Messages: Connect your purchased VMN to your application's webhook endpoint.
- Go back to the Numbers section in the MessageBird Dashboard.
- Find the number you purchased and click the Flow icon (looks like connected dots) next to it, or click ""Attach flow"".
- Select Create Custom Flow.
- Give your flow a descriptive name (e.g., ""SMS Campaign Handler"").
- Choose SMS as the trigger. Click Next.
- The flow editor will open with the ""SMS"" step already added. Click the + button below it.
- Choose Forward to URL as the next step.
- Set the Method to POST.
- In the URL field, paste the
localtunnel
URL you copied, and append/webhook
(e.g.,https://your-subdomain.loca.lt/webhook
). This is the route we will define in Express to handle incoming messages. - Click Save.
- Click Publish (top right) to activate the flow.
Your flow should look like:
SMS
->Forward to URL
. Now, when someone sends an SMS to your VMN, MessageBird will forward the details to your application's/webhook
endpoint via a POST request.
3. Implementing Core Functionality (Subscriber Handling)
Let's build the database logic and the webhook handler to manage subscriptions.
-
Database Helper Functions (
index.js
): Add functions to interact with thesubscribers
collection in MongoDB.// index.js (Add these functions after connectDB definition) // const SUBSCRIBERS_COLLECTION = 'subscribers'; // Already defined near connectDB /** * Finds a subscriber by phone number. * @param {string} number - The subscriber's phone number in E.164 format. * @returns {Promise<object|null>} - The subscriber document or null if not found. */ async function findSubscriber(number) { if (!db) throw new Error(""Database not connected""); return db.collection(SUBSCRIBERS_COLLECTION).findOne({ number: number }); } /** * Adds a new subscriber or updates their status to subscribed. * @param {string} number - The subscriber's phone number. * @returns {Promise<object>} - The result of the update operation. */ async function addOrUpdateSubscriber(number) { if (!db) throw new Error(""Database not connected""); return db.collection(SUBSCRIBERS_COLLECTION).updateOne( { number: number }, { $set: { number: number, subscribed: true, updatedAt: new Date() } }, // Added updatedAt { upsert: true } // Creates the document if it doesn't exist ); } /** * Updates a subscriber's status (subscribed: true/false). * @param {string} number - The subscriber's phone number. * @param {boolean} status - The new subscription status (true/false). * @returns {Promise<object>} - The result of the update operation. */ async function updateSubscriberStatus(number, status) { if (!db) throw new Error(""Database not connected""); return db.collection(SUBSCRIBERS_COLLECTION).updateOne( { number: number }, { $set: { subscribed: status, updatedAt: new Date() } } // Added updatedAt ); } /** * Retrieves all active subscribers. * @returns {Promise<Array<object>>} - An array of active subscriber documents. */ async function getActiveSubscribers() { if (!db) throw new Error(""Database not connected""); return db.collection(SUBSCRIBERS_COLLECTION).find({ subscribed: true }).toArray(); } /** * Helper function to send SMS using MessageBird SDK (async/await). * @param {string} recipient - The recipient phone number. * @param {string} body - The message body. * @returns {Promise<object>} - The MessageBird API response object. */ async function sendSms(recipient, body) { const params = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: [recipient], body: body, }; try { console.log(`Sending SMS to ${recipient}: ""${body}""`); const response = await messagebird.messages.create(params); // Log essential parts of the response, not the whole object if too verbose console.log(`MessageBird response for ${recipient}: Status ${response.recipients.items[0].status}`); return response; } catch (error) { console.error(`MessageBird API Error sending to ${recipient}:`, error); // Rethrow or handle the error as needed by the caller throw error; } }
-
Webhook Handler (
index.js
): Create the/webhook
route to process incoming SMS messages from MessageBird.// index.js (Add this route definition before the connectDB().then(...)) app.post('/webhook', async (req, res) => { // 1. Extract necessary data from MessageBird webhook payload const { originator, payload } = req.body; if (!originator || payload === undefined) { // Check payload presence, even if empty string console.warn('Received incomplete webhook payload:', req.body); return res.sendStatus(400); // Bad Request - missing data } const command = payload.trim().toLowerCase(); console.log(`Received command '${command}' from ${originator}`); try { // 2. Find existing subscriber const subscriber = await findSubscriber(originator); // 3. Process commands: SUBSCRIBE and STOP if (command === 'subscribe') { if (subscriber && subscriber.subscribed) { console.log(`${originator} is already subscribed.`); // Optional: Send ""You're already subscribed"" message // await sendSms(originator, ""You are already subscribed!""); } else { await addOrUpdateSubscriber(originator); console.log(`Subscribed ${originator}`); await sendSms(originator, 'Thanks for subscribing! Text STOP anytime to unsubscribe.'); } } else if (command === 'stop') { if (!subscriber || !subscriber.subscribed) { console.log(`${originator} is not currently subscribed.`); // Optional: Send ""You weren't subscribed"" message // await sendSms(originator, ""You were not subscribed to this list.""); } else { await updateSubscriberStatus(originator, false); console.log(`Unsubscribed ${originator}`); await sendSms(originator, 'You have been unsubscribed. Text SUBSCRIBE to join again.'); } } else { // 4. Handle unknown commands (optional) console.log(`Ignoring unknown command '${command}' from ${originator}`); // Optional: Send a help message // await sendSms(originator, 'Unknown command. Text SUBSCRIBE or STOP.'); } // 5. Acknowledge receipt to MessageBird res.sendStatus(200); } catch (error) { console.error(`Error processing webhook for ${originator}:`, error); // Don't send error details back, just acknowledge receipt if possible, // but log it thoroughly for debugging. // Send 200 to MessageBird even if processing failed internally to prevent retries, // unless the error is recoverable and you want MessageBird to retry. // For simplicity here, we send 500, but consider 200 for non-retryable errors. res.sendStatus(500); // Internal Server Error } });
This route extracts the sender's number (
originator
) and message content (payload
), converts the command to lowercase, checks the database, updates the subscriber's status accordingly using the helper functions, and sends a confirmation SMS via the updatedsendSms
helper. It then sends a200 OK
status back to MessageBird to acknowledge receipt (or500
on internal error).
4. Building the Admin Interface for Broadcasting
Create a simple web form for the administrator to send messages to all active subscribers.
-
Admin Form Route (
index.js
): Add aGET
route to display the HTML form. (Remember to secure this route - see Section 7)// index.js (Add this route definition) app.get('/', async (req, res) => { // SECURE THIS ROUTE try { const activeSubscribers = await getActiveSubscribers(); const subscriberCount = activeSubscribers.length; // Simple HTML form - For production, use a template engine (EJS, Handlebars) res.send(` <!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <meta name=""viewport"" content=""width=device-width, initial-scale=1.0""> <title>SMS Campaign Admin</title> <style> body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto; } textarea { width: 100%; min-height: 100px; margin-bottom: 10px; box-sizing: border-box; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } button { padding: 10px 20px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 4px; } button:hover { background-color: #0056b3; } .status { margin-top: 20px; padding: 10px; background-color: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; } pre { background-color: #eee; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-wrap: break-word; border-radius: 4px; } label { display: block; margin-bottom: 5px; font-weight: bold; } </style> </head> <body> <h1>Send Broadcast SMS</h1> <p>Current active subscribers: <strong>${subscriberCount}</strong></p> <form action=""/send"" method=""POST""> <div> <label for=""message"">Message:</label> <textarea id=""message"" name=""message"" required></textarea> </div> <button type=""submit"">Send to Subscribers</button> </form> <div id=""status"" class=""status"" style=""display: none;""></div> <script> // Basic client-side feedback (optional) const form = document.querySelector('form'); const statusDiv = document.getElementById('status'); if (form && statusDiv) { form.addEventListener('submit', function(event) { statusDiv.style.display = 'block'; statusDiv.textContent = 'Sending... please wait.'; // Allow form submission to proceed }); } </script> </body> </html> `); } catch (error) { console.error(""Error loading admin page:"", error); res.status(500).send(""Error loading admin page.""); } });
-
Broadcast Sending Route (
index.js
): Add aPOST
route to handle the form submission and send the broadcast. (Remember to secure this route - see Section 7)// index.js (Add this route definition) app.post('/send', async (req, res) => { // SECURE THIS ROUTE const messageBody = req.body.message; if (!messageBody || messageBody.trim() === '') { return res.status(400).send(""Message body cannot be empty.""); } try { const subscribers = await getActiveSubscribers(); if (subscribers.length === 0) { return res.send(` <!DOCTYPE html><html lang=""en""><head><meta charset=""UTF-8""><title>Broadcast Result</title><style>body{font-family:sans-serif;padding:20px;}</style></head><body> <h1>Broadcast Result</h1> <p>No active subscribers to send to.</p> <a href=""/"">Back to Admin</a> </body></html> `); } const recipients = subscribers.map(sub => sub.number); console.log(`Attempting to send broadcast to ${recipients.length} recipients.`); // MessageBird API allows up to 50 recipients per request. // We need to batch the requests if we have more than 50 subscribers. const batchSize = 50; let successfulSends = 0; let failedSends = 0; const totalRecipients = recipients.length; for (let i = 0; i < totalRecipients; i += batchSize) { const batch = recipients.slice(i, i + batchSize); const params = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: batch, body: messageBody, }; try { console.log(`Sending batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(totalRecipients / batchSize)} (${batch.length} recipients)...`); const response = await messagebird.messages.create(params); // Simple check: if API call succeeded, assume messages are queued. // MessageBird response details actual status per recipient. const sentInBatch = response.recipients.totalSentCount || batch.length; // Estimate if specific count unavailable successfulSends += sentInBatch; console.log(`Batch ${Math.floor(i / batchSize) + 1} queued. Response indicates ~${sentInBatch} sent.`); } catch (batchError) { // Log the error but continue with other batches if possible console.error(`Error sending batch starting at index ${i}:`, batchError); failedSends += batch.length; // Assume the whole batch failed if API call errored } } console.log(`Broadcast finished. Attempted: ${totalRecipients}, Queued (estimated): ${successfulSends}, Failed Batches (estimated): ${failedSends}`); // Send feedback to the admin res.send(` <!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <title>Broadcast Result</title> <style> body { font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto; } pre { background-color: #eee; padding: 10px; border: 1px solid #ccc; white-space: pre-wrap; word-wrap: break-word; border-radius: 4px; } .error { color: red; font-weight: bold; } a { color: #007bff; text-decoration: none; } a:hover { text-decoration: underline; } </style> </head> <body> <h1>Broadcast Result</h1> <p>Message queuing initiated for approximately ${successfulSends} out of ${totalRecipients} active subscribers.</p> ${failedSends > 0 ? `<p class=""error"">Failed to queue messages for ${failedSends} recipients due to batch errors. Check server logs for details.</p>` : ''} <hr> <p><strong>Message Sent:</strong></p> <pre id=""message-sent""></pre> <a href=""/"">Back to Admin</a> <script> // Function to escape HTML (simple version) and display message function escapeHtml(unsafe) { return unsafe .replace(/&/g, ""&"") .replace(/</g, ""<"") .replace(/>/g, "">"") .replace(/""/g, """"") .replace(/'/g, ""'""); } // Use textContent to safely insert the message body into the <pre> tag const messageBody = ${JSON.stringify(messageBody)}; // Pass message body safely to script const preTag = document.getElementById('message-sent'); if (preTag) { preTag.textContent = messageBody; // textContent automatically handles escaping for display } </script> </body> </html> `); } catch (error) { console.error(""Error sending broadcast:"", error); res.status(500).send(` <!DOCTYPE html><html lang=""en""><head><meta charset=""UTF-8""><title>Broadcast Failed</title><style>body{font-family:sans-serif;padding:20px;}</style></head><body> <h1>Broadcast Failed</h1> <p>An internal error occurred while trying to send the broadcast. Check server logs.</p> <a href=""/"">Back to Admin</a> </body></html> `); } });
This route retrieves active subscribers, batches them (max 50 per request), uses the
async/await
pattern withmessagebird.messages.create
for each batch, and provides feedback. It now usestextContent
in the client-side script for safer display of the sent message.
5. Error Handling and Logging
Robust handling is crucial for production.
- Database Connection: The
connectDB
function handles initial connection errors. - Webhook Errors: The
/webhook
route usestry...catch
. Log errors server-side. Respond200 OK
to MessageBird unless you want retries for specific, transient errors. - Broadcast Errors: The
/send
route usestry...catch
for overall errors and within the batch loop. Log errors and provide admin feedback. - Logging:
console.*
is basic. Integrate a structured logging library like Winston or Pino:Conceptual Winston Setup:npm install winston
// logger.js (Example - configure further) const winston = require('winston'); const logger = winston.createLogger({ level: 'info', // Log info and above (warn, error) format: winston.format.combine( winston.format.timestamp(), winston.format.json() // Log in JSON format ), transports: [ // Write all logs with level `error` and below to `error.log` new winston.transports.File({ filename: 'error.log', level: 'error' }), // Write all logs with level `info` and below to `combined.log` new winston.transports.File({ filename: 'combined.log' }), ], }); // If not in production then log to the `console` if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), })); } module.exports = logger; // In index.js: // const logger = require('./logger'); // Replace console.log with logger.info, console.error with logger.error etc. // e.g., logger.info(`Subscribed ${originator}`); // logger.error(`Error processing webhook for ${originator}:`, error);
- Retries: For critical operations like sending SMS, implement retries for transient errors (network issues, temporary API limits). Libraries like
async-retry
can help. Conceptual Retry forsendSms
:(Full integration of advanced logging and retries requires careful setup beyond this core guide but is highly recommended.)// npm install async-retry // const retry = require('async-retry'); // const logger = require('./logger'); // Assuming logger setup // // async function sendSmsWithRetry(recipient, body) { // await retry(async bail => { // try { // // Try sending // await sendSms(recipient, body); // } catch (error) { // // If it's an error MessageBird won't recover from (e.g., invalid number), bail out // // This requires inspecting the error structure from the SDK // // Example check (adjust based on actual SDK error structure): // // if (error.statusCode === 400 && error.errors && error.errors[0].code === 21) { // // bail(new Error('Non-retryable error: Invalid recipient')); // // return; // // } // // Rethrow other errors to trigger retry // throw error; // } // }, { // retries: 3, // Number of retries // factor: 2, // Exponential backoff factor // minTimeout: 1000, // Initial delay ms // onRetry: (error, attempt) => { // logger.warn(`Retrying sendSms to ${recipient} (attempt ${attempt}) due to error:`, error.message); // } // }); // } // // Use sendSmsWithRetry instead of sendSms in webhook/broadcast logic
6. Database Schema and Data Layer
- Schema: We use a simple MongoDB document structure in the
subscribers
collection:{ ""_id"": ""ObjectId(...)"", ""number"": ""+12025550135"", ""subscribed"": true, ""updatedAt"": ""ISODate(...)"" }
_id
: Auto-generated by MongoDB.number
: Subscriber phone number (E.164 format) - Indexed, Unique.subscribed
: Boolean indicating opt-in status.updatedAt
: Timestamp of last status change.
- Indexing: A unique index on the
number
field is crucial for performance and data integrity. This is now automatically created or verified on application startup within theconnectDB
function. - Data Layer: The helper functions (
findSubscriber
,addOrUpdateSubscriber
, etc.) provide a basic data access layer, abstracting database operations.
7. Security Features
- Input Validation/Sanitization:
- Webhook: We
trim()
andtoLowerCase()
commands. Theoriginator
format is generally reliable from MessageBird. - Admin Form: The message body is used. The result page now uses
textContent
for safer display. Validate message length/content server-side.
- Webhook: We
- Protect Admin Endpoints: The
/
(admin form) and/send
(broadcast action) routes must be protected. Implement authentication and authorization. This is critical for production.- Strategies: Session-based auth (using
express-session
and a login form), JSON Web Tokens (JWT), Basic Authentication (less secure, suitable only for internal tools with HTTPS), or OAuth integration. - Conceptual Middleware:
// Example: Placeholder authentication middleware function ensureAuthenticated(req, res, next) { // Replace with actual authentication check (e.g., check session, validate JWT) // Example: Check if user is logged in via session // const isAuthenticated = req.session && req.session.userId; const isAuthenticated = false; // <<< --- IMPLEMENT YOUR ACTUAL LOGIC HERE --- if (isAuthenticated) { return next(); // User is authenticated, proceed } else { // User not authenticated // Option 1: Redirect to login page // res.redirect('/login'); // Option 2: Send 401 Unauthorized or 403 Forbidden res.status(401).send('Unauthorized: Please log in.'); } } // Apply the middleware to protected routes: app.get('/', ensureAuthenticated, async (req, res) => { /* ... route handler ... */ }); app.post('/send', ensureAuthenticated, async (req, res) => { /* ... route handler ... */ }); // You would also need routes for login/logout, user management etc.
- Strategies: Session-based auth (using
- Rate Limiting:
- Webhooks: Use
express-rate-limit
on/webhook
to prevent abuse. - Admin: Apply rate limiting to
/send
and login endpoints.
Conceptual Rate Limiting:npm install express-rate-limit
// const rateLimit = require('express-rate-limit'); // // const webhookLimiter = rateLimit({ // windowMs: 15 * 60 * 1000, // 15 minutes // max: 100, // Limit each IP to 100 requests per windowMs // message: 'Too many requests from this IP, please try again after 15 minutes', // standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers // legacyHeaders: false, // Disable the `X-RateLimit-*` headers // }); // // const adminLimiter = rateLimit({ // windowMs: 60 * 60 * 1000, // 1 hour // max: 50, // Limit each IP to 50 admin actions per hour // message: 'Too many admin actions from this IP, please try again after an hour', // standardHeaders: true, // legacyHeaders: false, // }); // // // Apply to specific routes // app.use('/webhook', webhookLimiter); // app.use('/', adminLimiter); // Apply to both GET / and POST /send // app.use('/send', adminLimiter); // Explicitly apply to /send as well if needed
- Webhooks: Use
- Environment Variables: Use
.env
for secrets; ensure it's in.gitignore
. - HTTPS: Enforce HTTPS in production (e.g., via a reverse proxy like Nginx/Caddy or PaaS features).
8. Handling Special Cases
- Case Insensitivity: Handled via
.toLowerCase()
for commands. - Duplicate Subscriptions:
addOrUpdateSubscriber
handles this gracefully viaupsert
. - Unsubscribe Non-subscriber: Logic checks state before unsubscribing.
- Unknown Commands: Ignored; optionally send a help message.
- Number Formatting: Assumes E.164 from MessageBird.
- Internationalization: Requires storing language preferences and loading localized templates/messages.
9. Performance Optimizations
- Database Indexing: Unique index on
number
is implemented. - Batch Sending: Implemented in
/send
(50 recipients/request). - Asynchronous Operations:
async/await
used for non-blocking I/O. - Load Testing: Use tools like
k6
,Artillery
to test/webhook
and/send
under load. - Caching: Consider Redis for caching active subscribers in high-read scenarios, ensuring proper cache invalidation on subscription changes.
10. Monitoring, Observability, and Analytics
- Health Checks: Add a robust health check endpoint. Using a database
ping
command is more reliable than just checking the connection variable.// index.js (Add/Update this route) app.get('/health', async (req, res) => { try { if (!db) { throw new Error('Database connection not established'); } // Ping the database admin database to check connection status await db.admin().ping(); res.status(200).send('OK'); } catch (error) { console.error(""Health check failed:"", error); res.status(503).send('Service Unavailable - DB Connection issue'); } });
- Logging: Centralized, structured logging (Section 5).
- Metrics: Use libraries like
prom-client
to expose Prometheus metrics (request counts, latency, errors). - Error Tracking: Integrate services like Sentry, Bugsnag, or Datadog APM for real-time error reporting and analysis.
- MessageBird Dashboard: Monitor SMS delivery rates, errors, and costs directly within the MessageBird platform.
11. Troubleshooting and Caveats
- Webhook Not Firing: Check
localtunnel
status/URL, ensure your Node.js server is running and accessible, check server logs for errors on startup or during requests, verify firewall settings, and review the MessageBird Flow Builder logs for errors or delivery attempts. - Messages Not Sending: Verify
MESSAGEBIRD_API_KEY
andMESSAGEBIRD_ORIGINATOR
in.env
, check MessageBird account balance/credits, ensure recipient numbers are valid E.164 format, check server logs for API errors from thesendSms
function, and look at MessageBird Dashboard logs for delivery status. - Database Errors: Ensure MongoDB is running and accessible, verify the
MONGODB_URI
is correct, check database server logs, and monitor database performance. The unique index prevents duplicate numbers but errors duringupsert
should be logged. - Localtunnel Instability:
localtunnel
URLs can change or tunnels can drop. For more stable development, consider ngrok or deploying to a staging environment with a permanent URL. For production, deploy to a hosting provider (PaaS like Heroku/Render, or IaaS like AWS/GCP/Azure) with a public IP/domain. - Rate Limits: Be aware of MessageBird API rate limits and your own application's rate limits (Section 7). Implement backoff/retry strategies (Section 5).
- Compliance: Ensure compliance with local regulations regarding SMS marketing, opt-in/out procedures, and messaging content (e.g., TCPA in the US, GDPR in the EU). This guide provides a technical foundation, not legal advice.