This guide provides a step-by-step walkthrough for building an application capable of scheduling SMS messages to be sent at a future time using Node.js, Express, and the Vonage Messages API. We'll cover everything from initial project setup and core scheduling logic using node-cron
(for simplicity) to API implementation, error handling, security considerations, and deployment.
By the end of this guide, you will have a functional API endpoint that accepts SMS scheduling requests and sends the messages at the specified times using an in-memory approach. Crucially, Section 6 discusses using a database and job queue, which is the recommended approach for production environments requiring persistence and reliability.
(Guide last updated: [Current Date])
Project overview and goals
Goal: To create a Node.js service that exposes an API endpoint for scheduling SMS messages to be sent via the Vonage Messages API at a specified future date and time.
Problem Solved: Automates the process of sending timely reminders, notifications, or messages without requiring real-time intervention. Enables `set and forget`
SMS delivery for various use cases like appointment reminders, event notifications, or timed marketing messages.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications.
- Express: A minimal and flexible Node.js web application framework providing features for web and mobile applications, including routing and middleware.
- Vonage Messages API: A powerful API enabling communication across multiple channels, including SMS. We'll use it to send the scheduled messages.
@vonage/server-sdk
: The official Vonage Node.js SDK for easy interaction with the Vonage APIs.node-cron
: A simple cron-like task scheduler for Node.js, used for triggering SMS sends at the scheduled time. (Note: This guide usesnode-cron
with in-memory storage for foundational understanding. For production robustness and persistence across restarts, a database-backed job queue is strongly recommended – see Section 6).dotenv
: A zero-dependency module that loads environment variables from a.env
file intoprocess.env
.
System Architecture:
+---------+ +-----------------+ +-------------------+ +-----------------+ +--------------+
| User/ |------>| Node.js/Express|----->| Scheduling |----->| Vonage Messages |----->| User's Phone |
| Client | | API Server | | Logic (node-cron) | | API | | (SMS) |
+---------+ +-----------------+ +-------------------+ +-----------------+ +--------------+
(1. POST /schedule) (2. Validate & Schedule) (3. Trigger Send) (4. Send SMS) (5. Receive SMS)
(Note: The diagram shows node-cron
for scheduling logic, which is suitable for this initial guide. However, for production systems needing persistence, replace this with a database and job queue as detailed in Section 6.)
Prerequisites:
- Node.js and npm: Installed on your development machine. Download Node.js
- Vonage API Account: Sign up for free to get API credentials and a virtual number. Vonage Sign Up
- Vonage Virtual Number: Purchase an SMS-capable number from the Vonage Dashboard.
- Vonage CLI (Optional but Recommended): For easier application and number management. Install via
npm install -g @vonage/cli
. ngrok
(Optional but Recommended): For testing webhooks locally (like delivery status updates). ngrok Sign Up
1. Setting up the project
Let's initialize the project, install dependencies, and configure the basic structure.
1.1. Create Project Directory:
Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-sms-scheduler
cd vonage-sms-scheduler
1.2. Initialize npm Project:
Initialize a new Node.js project using npm. The -y
flag accepts default settings.
npm init -y
This creates a package.json
file.
1.3. Install Dependencies:
Install the necessary npm packages.
npm install express @vonage/server-sdk node-cron dotenv
express
: Web server framework.@vonage/server-sdk
: Vonage Node.js SDK.node-cron
: Task scheduler.dotenv
: Environment variable loader.
1.4. Project Structure:
Create the following basic structure:
vonage-sms-scheduler/
node_modules/
.env
# Stores sensitive credentials (DO NOT COMMIT)private.key
# Vonage application private key (DO NOT COMMIT)server.js
# Main application filepackage.json
package-lock.json
.gitignore
# Specifies files/folders ignored by Git
1.5. Configure .gitignore
:
Create a .gitignore
file in the root directory to prevent committing sensitive files and node_modules
.
node_modules
.env
private.key
*.log
1.6. Set Up Vonage Application and Credentials:
You need a Vonage Application to authenticate API requests using an Application ID and a Private Key.
-
Using Vonage CLI (Recommended):
- Configure CLI (if first time):
(Find Key/Secret in your Vonage Dashboard)
vonage config:set --apiKey=YOUR_API_KEY --apiSecret=YOUR_API_SECRET
- Create Application: Run the command below. Provide a name (e.g.,
SMSSchedulerApp
). EnableMessages
. You'll be asked for Status and Inbound webhook URLs. For now, you can provide placeholders or set upngrok
(see step 1.7) and use those URLs.This command will:# Example using ngrok URLs (replace YOUR_NGROK_SUBDOMAIN with your actual ngrok forwarding subdomain) # vonage apps:create ""SMSSchedulerApp"" --messages_status_url=https://YOUR_NGROK_SUBDOMAIN.ngrok.io/webhooks/status --messages_inbound_url=https://YOUR_NGROK_SUBDOMAIN.ngrok.io/webhooks/inbound --keyfile=private.key # Example with placeholders if not using ngrok yet vonage apps:create ""SMSSchedulerApp"" --messages_status_url=http://example.com/status --messages_inbound_url=http://example.com/inbound --keyfile=private.key
- Output an
Application created: YOUR_APPLICATION_ID
. Save this ID. - Generate a
private.key
file in your current directory. Keep this secure.
- Output an
- Purchase & Link Number:
- Search for an SMS-capable number (e.g., in the US):
vonage numbers:search US --features=SMS
- Buy a number:
vonage numbers:buy YOUR_CHOSEN_NUMBER US
- Link the number to your app:
vonage apps:link --number=YOUR_VONAGE_NUMBER YOUR_APPLICATION_ID
- Search for an SMS-capable number (e.g., in the US):
- Configure CLI (if first time):
-
Using Vonage Dashboard:
- Navigate to ""Applications"" -> ""Create a new application"".
- Give it a name (e.g.,
SMSSchedulerApp
). - Click ""Generate public and private key"". Save the downloaded
private.key
file into your project directory. - Note the Application ID displayed.
- Enable the ""Messages"" capability.
- Enter webhook URLs for ""Inbound URL"" and ""Status URL"". Use
ngrok
URLs (Step 1.7) or placeholders for now (e.g.,http://example.com/webhooks/inbound
,http://example.com/webhooks/status
). - Click ""Generate new application"".
- Go to ""Numbers"" -> ""Your numbers"". Click ""Link"" next to your desired Vonage number and select the application you just created.
1.7. Configure Environment Variables:
Create a .env
file in the project root and add your Vonage credentials and configuration.
# Vonage API Credentials
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_FROM_STEP_1_6
VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
# Vonage Number
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # The number linked to your app
# Server Configuration
PORT=3000
- Replace placeholders with your actual credentials.
- Ensure
VONAGE_PRIVATE_KEY_PATH
points to the location of yourprivate.key
file.
1.8. Set Up ngrok
(Optional - for Webhooks):
If you want to receive message status updates locally:
- Download and install
ngrok
. - Authenticate your
ngrok
client (if needed). - Run
ngrok
to expose your local server (which will run on port 3000 as defined in.env
).ngrok http 3000
ngrok
will display a ""Forwarding"" URL likehttps://random-subdomain.ngrok.io
. Therandom-subdomain
part is what you need. Note this subdomain.- Update Vonage Application: Go back to your Vonage Application settings in the dashboard or use the CLI (
vonage apps:update YOUR_APP_ID --messages_status_url=https://YOUR_NGROK_SUBDOMAIN.ngrok.io/webhooks/status --messages_inbound_url=https://YOUR_NGROK_SUBDOMAIN.ngrok.io/webhooks/inbound
) to set the Status and Inbound URLs using yourngrok
forwarding address (replaceYOUR_NGROK_SUBDOMAIN
with the actual subdomain from thengrok
output).
2. Implementing core functionality
Now, let's write the code for the Express server, scheduling logic, and Vonage integration.
server.js
require('dotenv').config(); // Load .env variables into process.env
const express = require('express');
const cron = require('node-cron');
const { Vonage } = require('@vonage/server-sdk');
const { SMS } = require('@vonage/messages');
const fs = require('fs'); // To read the private key file
// --- Basic Server Setup ---
const app = express();
const port = process.env.PORT || 3000;
// Middleware to parse JSON bodies
app.use(express.json());
// Middleware to parse URL-encoded bodies
app.use(express.urlencoded({ extended: true }));
// --- Vonage Client Initialization ---
let vonage;
try {
// Read private key from file specified in .env
const privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH);
vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKey,
// Optionally add apiKey and apiSecret if needed for other functionalities,
// but App ID + Private Key is preferred for Messages API authentication
// apiKey: process.env.VONAGE_API_KEY,
// apiSecret: process.env.VONAGE_API_SECRET
});
console.log(""Vonage client initialized successfully."");
} catch (error) {
console.error(""Error initializing Vonage client:"", error);
console.error(""Ensure VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH are set correctly in .env and the key file exists."");
process.exit(1); // Exit if Vonage client can't be initialized
}
// --- In-Memory Storage for Scheduled Jobs (PRODUCTION WARNING) ---
// WARNING: This simple object will lose all scheduled jobs if the server restarts.
// This is suitable for demonstration purposes only. For production, use a database
// (e.g., PostgreSQL, MongoDB) and a persistent job queue (e.g., BullMQ with Redis,
// Agenda with MongoDB) instead. See Section 6 for details.
const scheduledJobs = {}; // Use an object to store cron jobs by a unique ID
// --- Helper Function: Send SMS via Vonage ---
async function sendScheduledSms(to, text, scheduleId) {
console.log(`[${new Date().toISOString()}] Attempting to send SMS for schedule ID: ${scheduleId}`);
try {
const resp = await vonage.messages.send(
new SMS({
to: to,
from: process.env.VONAGE_NUMBER,
text: text,
})
);
console.log(`[${new Date().toISOString()}] Message sent successfully for schedule ID: ${scheduleId}. Vonage Message UUID: ${resp.messageUuid}`);
// Optional: Update status in a database here (in a production setup)
} catch (error) {
console.error(`[${new Date().toISOString()}] Error sending SMS for schedule ID: ${scheduleId}`, error?.response?.data || error.message);
// Optional: Log error details and update status in a database (in a production setup)
} finally {
// Clean up the completed/failed job from memory
if (scheduledJobs[scheduleId]) {
scheduledJobs[scheduleId].stop(); // Stop the cron job instance
delete scheduledJobs[scheduleId];
console.log(`[${new Date().toISOString()}] Cleaned up cron job for schedule ID: ${scheduleId}`);
}
}
}
// --- Helper Function: Validate Schedule Request ---
function validateScheduleRequest(body) {
const { to, message, sendAt } = body;
const errors = [];
if (!to || typeof to !== 'string' || !/^\d{10,15}$/.test(to)) {
errors.push(""Invalid 'to' phone number. Must be a string of 10-15 digits."");
}
if (!message || typeof message !== 'string' || message.trim().length === 0) {
errors.push(""Invalid 'message'. Must be a non-empty string."");
}
if (!sendAt || typeof sendAt !== 'string') {
errors.push(""Invalid 'sendAt'. Must be an ISO 8601 formatted date-time string."");
}
let sendAtDate;
if (!errors.some(e => e.includes('sendAt'))) {
sendAtDate = new Date(sendAt);
if (isNaN(sendAtDate.getTime())) {
errors.push(""Invalid 'sendAt' date format. Please use ISO 8601 (e.g., 2024-12-31T14:30:00Z)."");
} else if (sendAtDate <= new Date()) {
errors.push(""'sendAt' must be a future date and time."");
}
}
return { isValid: errors.length === 0_ errors_ sendAtDate };
}
// --- API Endpoint: Schedule SMS ---
app.post('/schedule'_ (req_ res) => {
const { to, message, sendAt } = req.body;
// 1. Validate Input
const validation = validateScheduleRequest(req.body);
if (!validation.isValid) {
console.warn(`[${new Date().toISOString()}] Schedule request failed validation:`, validation.errors);
return res.status(400).json({ success: false, errors: validation.errors });
}
const { sendAtDate } = validation;
// 2. Generate a unique ID for the job
const scheduleId = `sms_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
// 3. Convert future date to cron syntax
// node-cron format: second minute hour dayOfMonth month dayOfWeek
const cronTime = `${sendAtDate.getSeconds()} ${sendAtDate.getMinutes()} ${sendAtDate.getHours()} ${sendAtDate.getDate()} ${sendAtDate.getMonth() + 1} *`;
// Note: The '*' for dayOfWeek means it runs regardless of the day, respecting the date.
// *** CRITICAL TIMEZONE WARNING ***
// By default, node-cron uses the SERVER's local timezone for scheduling.
// This can lead to unexpected behavior if the server timezone differs from the
// intended schedule timezone (e.g., the one implied in 'sendAt').
// For reliable scheduling, ensure your server runs in UTC, or explicitly set
// the 'timezone' option in cron.schedule (e.g., timezone: ""Etc/UTC"").
// See Section 8 (Handling special cases) for a more detailed discussion.
console.log(`[${new Date().toISOString()}] Scheduling SMS ID: ${scheduleId} for ${sendAtDate.toISOString()} (Cron: ${cronTime}, Server Timezone)`);
try {
// 4. Schedule the job using node-cron
const task = cron.schedule(cronTime, () => {
// This function executes when the cron time is reached
sendScheduledSms(to, message, scheduleId);
// NOTE: The sendScheduledSms function handles stopping/deleting the job from memory
}, {
scheduled: true,
// timezone: ""Etc/UTC"" // Uncomment and use if your server is not in UTC
});
// 5. Store the job reference (in memory - see WARNING above)
scheduledJobs[scheduleId] = task;
// 6. Send Success Response
res.status(202).json({
success: true,
message: ""SMS scheduled successfully."",
scheduleId: scheduleId,
scheduledTimeUTC: sendAtDate.toISOString(),
recipient: to
});
} catch (error) {
console.error(`[${new Date().toISOString()}] Error scheduling job ID ${scheduleId}:`, error);
res.status(500).json({ success: false, message: ""Internal server error during scheduling."" });
}
});
// --- API Endpoint: Receive Vonage Status Webhook (Optional) ---
app.post('/webhooks/status', (req, res) => {
console.log(`[${new Date().toISOString()}] Received Status Webhook:`);
console.log(JSON.stringify(req.body, null, 2)); // Log the full status update
// Process the status update (e.g., update database record in production)
const { message_uuid, status, timestamp, to, from, error } = req.body;
console.log(` -> Status for Message ${message_uuid} to ${to}: ${status} at ${timestamp}`);
if (error) {
console.error(` -> Error details: ${error['error-code']} - ${error['error-code-label']}`);
}
// Vonage expects a 200 OK response to acknowledge receipt
res.status(200).send('OK');
});
// --- API Endpoint: Receive Vonage Inbound Webhook (Optional) ---
// Useful if you want to handle replies, but not strictly needed for scheduling
app.post('/webhooks/inbound', (req, res) => {
console.log(`[${new Date().toISOString()}] Received Inbound Webhook:`);
console.log(JSON.stringify(req.body, null, 2)); // Log the incoming message
// Process the inbound message if needed
res.status(200).send('OK');
});
// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Start Server ---
// Check if the module is run directly
if (require.main === module) {
app.listen(port, () => {
console.log(`SMS Scheduler server listening on http://localhost:${port}`);
console.log(`Ensure Vonage Messages API is set as default SMS API in your Vonage Dashboard settings.`);
console.log(`Scheduling API endpoint: POST http://localhost:${port}/schedule`);
// Add the production warning here as well
console.warn(""**********************************************************************"");
console.warn(""WARNING: Running with IN-MEMORY scheduling via node-cron."");
console.warn(""Scheduled jobs WILL BE LOST if the server restarts or crashes."");
console.warn(""This setup is for demonstration only. Use a persistent database"");
console.warn(""and job queue (see Section 6) for production environments."");
console.warn(""**********************************************************************"");
});
} else {
// Export the app instance for testing purposes (without starting the listener)
module.exports = app;
}
// --- Graceful Shutdown (Example) ---
const cleanup = (signal) => {
console.log(`${signal} signal received: closing HTTP server`);
// Perform cleanup, like stopping active cron jobs if needed, closing DB connections etc.
Object.values(scheduledJobs).forEach(job => {
try {
job.stop();
} catch (e) {
// Ignore errors if job was already stopped or invalid
}
});
console.log('Stopped active in-memory cron jobs.');
// Ideally, persist pending jobs to DB before exiting here in a production setup.
// Close server connections if app.listen was called directly
// This part is tricky if the server isn't started directly in this file for testing
// For simplicity here, we just exit. Test runners handle server shutdown.
process.exit(0);
};
process.on('SIGTERM', () => cleanup('SIGTERM'));
process.on('SIGINT', () => cleanup('SIGINT'));
Explanation:
- Initialization: Loads
.env
, imports modules, sets up Express. - Vonage Client: Initializes the Vonage SDK using the Application ID and Private Key from
.env
. Includes error handling if initialization fails. - In-Memory Storage: Declares
scheduledJobs
to holdnode-cron
task instances. Crucially includes a strong warning about its limitations and unsuitability for production. sendScheduledSms
Function: Anasync
function that takes recipient, message text, and a unique ID. It usesvonage.messages.send
with theSMS
class. It logs success or failure and includes logic to stop and remove the completed/failedcron
job from thescheduledJobs
object.validateScheduleRequest
Function: Checks ifto
,message
, andsendAt
are present and in the correct format (basic phone number regex, non-empty string, valid future ISO 8601 date). Returns validation status and errors./schedule
Endpoint (POST):- Parses the request body.
- Calls
validateScheduleRequest
. Returns 400 if invalid. - Generates a simple unique
scheduleId
. - Calculates the
cronTime
string based on the validatedsendAtDate
. Adds a critical note about timezones and links to Section 8. - Uses
cron.schedule
to create the task. The callback function passed to it callssendScheduledSms
. - Stores the
cron
task instance inscheduledJobs
(in-memory) using thescheduleId
. - Returns a
202 Accepted
response indicating the request was accepted for processing.
/webhooks/status
Endpoint (POST): (Optional) A simple endpoint to receive and log delivery status updates from Vonage. Responds with 200 OK./webhooks/inbound
Endpoint (POST): (Optional) Placeholder for handling incoming SMS replies./health
Endpoint (GET): A basic health check.- Server Start: Conditionally starts the Express server (
app.listen
) only if the script is run directly (require.main === module
). Otherwise, it exports theapp
instance for testing. Includes a prominent warning about in-memory storage when started directly. - Graceful Shutdown: Includes basic signal handlers (
SIGTERM
,SIGINT
) to attempt stopping active in-memory cron jobs before exiting.
3. Building the API layer
The server.js
file already implements the core API endpoint (POST /schedule
).
API Endpoint Documentation:
- Endpoint:
POST /schedule
- Description: Schedules an SMS message to be sent at a future time (using in-memory scheduler in this example).
- Request Body:
application/json
(Note: Recipient phone number (E.164 format recommended).{ ""to"": ""14155550100"", ""message"": ""Your appointment is tomorrow at 10:00 AM."", ""sendAt"": ""2024-12-25T10:00:00Z"" }
sendAt
is the desired send time in ISO 8601 format (UTC 'Z' or timezone offset recommended - see Section 8).) - Success Response (202 Accepted):
{ ""success"": true, ""message"": ""SMS scheduled successfully."", ""scheduleId"": ""sms_1713612345678_abc12"", ""scheduledTimeUTC"": ""2024-12-25T10:00:00.000Z"", ""recipient"": ""14155550100"" }
- Error Response (400 Bad Request):
{ ""success"": false, ""errors"": [ ""Invalid 'sendAt'. Must be an ISO 8601 formatted date-time string."", ""'sendAt' must be a future date and time."" ] }
- Error Response (500 Internal Server Error):
{ ""success"": false, ""message"": ""Internal server error during scheduling."" }
Testing with curl
:
Replace placeholders with your data and ensure the server is running (node server.js
). Adjust the sendAt
time to be a few minutes in the future.
curl -X POST http://localhost:3000/schedule \
-H ""Content-Type: application/json"" \
-d '{
""to"": ""YOUR_TEST_PHONE_NUMBER"",
""message"": ""Hello from the scheduler! This is a test."",
""sendAt"": ""2024-10-27T23:59:00Z""
}'
You should receive a 202 Accepted
response. Check the server logs and your phone at the scheduled time.
4. Integrating with Vonage
This was covered in Steps 1.6 (Setup) and 2 (Implementation). Key points:
- Credentials: Use Application ID and Private Key for authentication via
@vonage/server-sdk
. - Configuration: Store credentials securely in
.env
and load usingdotenv
. Never commit.env
orprivate.key
. - SDK Usage: Instantiate
new Vonage(...)
and usevonage.messages.send(new SMS(...))
to send messages. - Vonage Dashboard Settings: Ensure the ""Messages API"" is set as the default for SMS under ""API Settings"" -> ""SMS Settings"" in your Vonage Dashboard. This setting ensures that when you use the Messages API SDK to send an SMS, it uses the appropriate backend infrastructure.
- Webhooks: Configure Status and Inbound URLs in the Vonage Application settings to receive delivery updates and replies (requires
ngrok
or a public URL).
5. Error handling and logging
- Vonage Client Init:
try...catch
block during initialization ensures the app exits if basic Vonage setup fails. - API Request Validation: The
validateScheduleRequest
function provides specific feedback on invalid input (400 Bad Request). - Scheduling Errors:
try...catch
aroundcron.schedule
catches errors during the scheduling process itself (e.g., invalid cron syntax derived from the date, though less likely with date object conversion). Returns 500 Internal Server Error. - SMS Sending Errors:
try...catch
withinsendScheduledSms
handles errors from the Vonage API during the actual send attempt (e.g., invalid number, insufficient funds, API issues). It logs detailed errors from the Vonage response if available. - Logging: Uses
console.log
andconsole.error
for basic logging. For production, replace with a structured logger like Pino or Winston for better log management, filtering, and integration with log aggregation services. - Webhook Errors: The webhook endpoints currently just log incoming data. Production systems should include
try...catch
blocks for any processing logic within the webhook handlers. - Retry Mechanisms: This simple implementation doesn't include automatic retries for failed SMS sends. A production system using a job queue (like BullMQ) could configure automatic retries with exponential backoff directly within the queue settings. For the
node-cron
approach, you would need to implement retry logic manually (e.g., rescheduling the job with a delay upon failure, potentially tracking retry counts in a database).
6. Creating a database schema (Production Recommendation)
As highlighted multiple times, the in-memory scheduledJobs
object is unsuitable for production due to lack of persistence. A database combined with a robust job queue system is essential for reliability.
Why a Database and Job Queue?
- Persistence: Scheduled jobs survive server restarts and crashes.
- Scalability: Allows multiple server instances (workers) to process jobs from the queue.
- State Management: Track job status (pending, processing, sent, failed, retried).
- Querying/Auditing: Enables checking scheduled jobs, history, and troubleshooting.
- Reliable Scheduling: Job queues handle polling the database and triggering workers, decoupling scheduling logic from the main API server.
- Retry Logic: Built-in support for retries, backoff strategies, etc.
Example Schema (PostgreSQL):
CREATE TYPE sms_status AS ENUM ('pending', 'processing', 'sent', 'failed', 'cancelled');
CREATE TABLE scheduled_sms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Or SERIAL PRIMARY KEY
recipient_number VARCHAR(20) NOT NULL,
message_body TEXT NOT NULL,
send_at TIMESTAMPTZ NOT NULL, -- Store with timezone (UTC recommended)
status sms_status NOT NULL DEFAULT 'pending',
vonage_message_uuid VARCHAR(50) NULL, -- Store Vonage ID upon successful send
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_attempt_at TIMESTAMPTZ NULL,
retry_count INT NOT NULL DEFAULT 0,
last_error TEXT NULL -- Store error message on failure
);
-- Index for efficient querying of pending jobs by workers
CREATE INDEX idx_scheduled_sms_pending ON scheduled_sms (status, send_at)
WHERE status = 'pending';
-- Optional: Index for looking up jobs by Vonage Message ID from webhooks
CREATE INDEX idx_scheduled_sms_vonage_uuid ON scheduled_sms (vonage_message_uuid);
Implementation Steps (Conceptual):
- Choose DB & Job Queue: Select a database (PostgreSQL, MySQL, MongoDB) and a suitable job queue library (e.g., BullMQ with Redis, Agenda with MongoDB). Choose an ORM/query builder (e.g., Prisma, Sequelize, TypeORM).
- Define Schema/Model: Create the table/collection using migrations or model definitions.
- Modify
/schedule
Endpoint: Instead of usingnode-cron
, this endpoint should:- Validate the request.
- Insert a record into the
scheduled_sms
table withstatus = 'pending'
and thesend_at
time. - Optionally, if using a queue like BullMQ, add the job details (like the DB record ID) to the queue with the specified
delay
calculated fromsend_at
.
- Implement a Worker Process: Create a separate Node.js process (the ""worker"").
- Worker Logic (using a Job Queue like BullMQ):
- Configure the worker to connect to the job queue (e.g., Redis).
- Define a processor function that activates when a job becomes ready (based on its scheduled time/delay).
- Inside the processor:
- Fetch the full job details from the database using the ID stored in the job payload.
- Mark the job as
processing
in the database. - Call the
sendScheduledSms
function (modified to accept job data and potentially update the DB). - Update the job status in the database to
sent
(storevonage_message_uuid
) orfailed
(log error, incrementretry_count
) based on the Vonage API response. Handle retries according to queue configuration.
- Modify Status Webhook: When a status update is received, look up the job in the database using the
message_uuid
and update its status accordingly.
This guide focuses on the simpler node-cron
approach for initial understanding, but transitioning to a DB-backed queue is essential for building a reliable, production-ready SMS scheduler.
7. Adding security features
- Input Validation: Implemented in
validateScheduleRequest
to prevent invalid data. Consider more robust libraries likejoi
orclass-validator
for complex validation schemas. - Secure Credentials:
.env
and.gitignore
prevent leaking API keys and private keys. Ensure the server environment securely manages these variables (e.g., using platform secrets management). - Rate Limiting: Protect the
/schedule
endpoint from abuse. Use middleware likeexpress-rate-limit
.npm install express-rate-limit
// In server.js, after app initialization const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: 'Too many requests from this IP, please try again after 15 minutes' }); // Apply the rate limiting middleware to API routes // Apply specifically to /schedule or globally if desired app.use('/schedule', limiter);
- Webhook Security: While basic status/inbound webhooks don't require complex signing by default, be aware that anyone knowing your webhook URL could potentially send fake data. For higher security needs involving sensitive actions triggered by webhooks, Vonage often uses JWTs for signed callbacks (check specific API documentation). You might implement checks based on expected source IPs if applicable. Consider using a library to verify signatures if provided by Vonage for your webhook type.
- Helmet: Use the
helmet
middleware for setting various security-related HTTP headers (like Content Security Policy, X-Frame-Options, etc.).npm install helmet
// In server.js, near the top const helmet = require('helmet'); app.use(helmet());