Frequently Asked Questions
You can schedule SMS reminders using Node.js with Fastify, Plivo, and a scheduler like Toad Scheduler. The process involves setting up API endpoints to accept reminder details, storing them in a database, and using a scheduled task to periodically check and send due reminders via the Plivo API.
Plivo is a cloud communications platform that provides the SMS API for sending the actual messages. The Node.js application interacts with Plivo's API using the Plivo Node.js SDK, which handles sending messages to recipients at the scheduled times.
Fastify is a high-performance Node.js web framework known for its speed and extensibility. It offers built-in features for validation, logging, and plugin support, which simplifies development and improves the maintainability of the SMS reminder application.
While SQLite is suitable for smaller projects or this tutorial, you should consider using PostgreSQL or MySQL for larger-scale applications with more significant data storage needs. SQLite can become a bottleneck as data volume and concurrency increase.
Toad-scheduler, accessed via the @fastify/schedule plugin, is responsible for scheduling the task that checks for and sends due reminders. It allows defining simple interval-based or more complex cron-like schedules to trigger the reminder sending process periodically.
The provided code includes retry logic with exponential backoff specifically for 5xx server errors or network issues when interacting with the Plivo API. This ensures that temporary errors don't permanently prevent reminders from being sent.
Optimistic locking is a strategy to prevent race conditions when multiple instances of the scheduled task (or separate processes) might try to send the same reminder. It involves updating the reminder status to "sending" with a conditional check, ensuring that the update only happens if the status is still "pending".
The application avoids sending duplicate SMS messages by using optimistic locking. Before attempting to send, the task checks if the reminder status is "pending". If the status is already "sending", it skips the reminder and moves to the next one.
Environment variables are managed using a .env file in the root directory. Create a file named .env and add your Plivo Auth ID, Auth Token, Sender ID (Plivo phone number), and any other settings there. Ensure this .env file is NOT committed to version control.
better-sqlite3 is a fast and reliable Node.js client for interacting with SQLite databases. It's used to store reminder data, including recipient number, message content, and scheduled delivery time.
To send SMS using Plivo's Node.js SDK, initialize a Plivo Client with your credentials. Then, use client.messages.create(senderId, recipientNumber, messageText) to queue a message. The code example also demonstrates best practices for retries and error handling.
To run in development mode, use the command "npm run dev". This utilizes nodemon to automatically restart the server on file changes and pino-pretty to format logs in a readable way.
You can locate your Plivo Auth ID and Auth Token on the Plivo Console dashboard at https://console.plivo.com/dashboard/. These credentials are required to authenticate with the Plivo API for sending SMS messages.
SMS Scheduling with Plivo, Node.js & Fastify: Complete Tutorial
This guide provides a complete walkthrough for building a production-ready SMS scheduling and reminder application using Node.js, the Fastify web framework, and the Plivo messaging API. You'll learn how to set up the project, schedule tasks, interact with a database, send SMS messages via Plivo, and handle common production concerns like error handling, security, and deployment.
By the end of this tutorial, you will have a functional application capable of accepting requests to schedule SMS reminders, storing them, and automatically sending them out at the specified time using Plivo.
Project Overview and Goals
What We're Building:
We are creating a backend service that exposes an API to schedule SMS messages (reminders). The service will:
Problem Solved:
This system automates the process of sending timely SMS notifications or reminders, crucial for appointment confirmations, event alerts, subscription renewals, task deadlines, and more, without manual intervention.
Technologies Used:
@fastify/schedule(toad-scheduler): A Fastify plugin for scheduling recurring or one-off tasks within the application, used here to periodically check for due reminders.better-sqlite3: A simple, fast, and reliable SQLite3 client for Node.js, suitable for storing reminder data in this guide. For larger scale, consider PostgreSQL or MySQL.dotenv: For managing environment variables securely.pino-pretty: For development-friendly logging output.System Architecture:
Prerequisites:
curlor Postman for API testing.1. Setting up the Project
Let's initialize the project, install dependencies, and structure the application.
1. Create Project Directory:
Open your terminal and run:
2. Initialize Node.js Project:
This creates a
package.jsonfile.3. Install Dependencies:
fastify: The core web framework.@fastify/schedule: Fastify plugin for task scheduling.toad-scheduler: The underlying robust scheduling library.plivo: Plivo Node.js SDK for sending SMS.better-sqlite3: SQLite database driver.fastify-plugin: Utility for creating reusable Fastify plugins.dotenv: Loads environment variables from a.envfile.pino-pretty: Formats Fastify's logs nicely during development.@sinclair/typebox: TypeScript type builder for JSON Schema validation used in route definitions.4. Install Development Dependencies:
nodemon: Automatically restarts the server during development when file changes are detected.5. Configure
package.jsonScripts:Open
package.jsonand add/modify thescriptssection:"type": "module": Enables the use of ES Module syntax (import/export).start: Runs the application in production mode.dev: Runs the application in development mode usingnodemonfor auto-restarts andpino-prettyfor readable logs.6. Create Project Structure:
Create the following directories and files:
7. Create
.gitignore:Create a
.gitignorefile in the root directory to prevent sensitive files and unnecessary directories from being committed to version control:8. Configure Environment Variables (
.env):Create a
.envfile in the root directory. Important: Replace the placeholder values (YOUR_PLIVO_AUTH_ID,YOUR_PLIVO_AUTH_TOKEN,YOUR_PLIVO_PHONE_NUMBER) with your actual credentials obtained from your Plivo Console.PORT,HOST: Network configuration for the Fastify server.NODE_ENV: Determines environment-specific settings (e.g., logging).LOG_LEVEL: Controls the verbosity of logs.DATABASE_FILE: Path to the SQLite database file.PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN: Replace these with your Plivo API credentials found on the Plivo Console dashboard.PLIVO_SENDER_ID: Replace this with an SMS-enabled Plivo phone number you've purchased, found under Phone Numbers > Your Numbers on the Plivo Console. It must be in E.164 format (international standard for telephone numbering, limited to 15 digits including country code).9. Basic Logger Configuration:
Create
src/config/logger.js:This configures the Pino logger, using
pino-prettyfor readable logs in development and standard JSON logs in production.10. Basic Fastify App Setup:
Create
src/app.js:This sets up the basic Fastify application:
dotenv.loadEnvfunction for validation.buildAppfunction to register plugins and routes (we'll create these next)./healthcheck endpoint.Now you have a basic project structure and setup ready. Run
npm run devin your terminal. You should see log output indicating the server has started, listening on port 3000. You can accesshttp://localhost:3000/healthin your browser or viacurlto verify.2. Creating a Database Schema and Data Layer (Combined with Plugin Setup)
We'll set up the SQLite database and create the necessary table using a Fastify plugin.
1. Implement the Database Plugin (
src/plugins/db.js):fastify-plugin(fp) to make thedbdecorator globally available.app.js).reminderstable SQL.id: Primary key.phoneNumber: Recipient number (E.164 format).message: SMS content.reminderTime: Scheduled time in ISO 8601 format (UTC recommended). Storing asTEXT.status: Tracks the reminder state ('pending', 'sending', 'sent', 'failed').createdAt,updatedAt: Timestamps.plivoMessageUuid: Stores the ID returned by Plivo upon successful sending, useful for tracking.db.exec()runs theCREATE TABLE IF NOT EXISTSstatement, making it idempotent.updatedAton row updates.fastify.decorate('db', db)makes the database connection accessible viafastify.dborrequest.server.dbin handlers and other plugins.onClosehook ensures the database connection is closed when the Fastify server stops.try...catchblock to handle connection/initialization errors.2. Data Access:
With the plugin registered in
app.js, you can now access the database in your route handlers (created later) like this:We use prepared statements (
db.prepare(...)) which automatically sanitize inputs, preventing SQL injection vulnerabilities.3. Integrating with Necessary Third-Party Services (Plivo)
Now, let's create a service module to handle interactions with the Plivo API.
1. Create Plivo Service (
src/services/plivoService.js):authId_authToken)_ thesenderId(your Plivo number)_ and a logger instance. It initializes the PlivoClient. Includes robust error handling for initialization failures.sendSmsMethod (with Retries):to) and messagetext.for) with exponential backoff (initialDelay * Math.pow(2_ attempt - 1)) usingnode:timers/promises.setTimeout.this.client.messages.create().ETIMEDOUT_ECONNRESET). Does not retry on 4xx errors (like invalid number) as they won't succeed.nullafter exhausting retries or encountering a non-retriable error.2. Integrate into Scheduler Plugin (Update
src/plugins/scheduler.js):We need to instantiate
PlivoServiceand make it available to our scheduled task.PlivoServiceand Task Factory: Import the necessary modules.PlivoService: Create an instance, passing credentials and the Fastify logger (fastify.log).fastify.db, theplivoServiceinstance, andfastify.logto the task factory function (createSendRemindersTask). This makes them available within the task's logic.'db-connector'todependenciesto ensure the database plugin runs first.4. Implementing Core Functionality (The Reminder Task)
This task runs periodically, finds due reminders, and triggers sending them via the
PlivoService.1. Create the Send Reminders Task (
src/tasks/sendRemindersTask.js):createSendRemindersTaskto accept dependencies (db,plivoService,logger) via injection. This improves testability.AsyncTask: The core logic is wrapped intoad-scheduler'sAsyncTaskwhich provides structured execution and error handling.reminderstable for entries withstatus = 'pending'andreminderTime <= now().LIMITto process reminders in batches_ preventing the task from holding resources for too long if there are many due reminders.reminderTimeto process older ones first.statusto'sending'.WHERE id = ? AND status = 'pending'clause ensures that only one instance of the task (or another process) can successfully claim a specific reminder. IfupdateResult.changesis 0_ it means the status was already changed_ so the current task skips it.plivoService.sendSmsmethod (which now includes retry logic).plivoResponse_ it updates the reminderstatusto'sent'or'failed'.plivoMessageUuidif the message was sent successfully.try...catchblocks for database operations and the Plivo call. Attempts to mark the reminder as'failed'if an error occurs during processing.errorHandlerfunction passed toAsyncTaskcatches errors originating directly from thetaskLogicexecution itself (though internal try/catches handle most specific cases).5. Building a Complete API Layer
Let's define the API endpoints for managing reminders.
1. Create Reminder Routes (
src/routes/reminders.js):@sinclair/typeboxfor defining clear request body, parameters, and response schemas. This enables automatic validation and serialization by Fastify.phoneNumberformat (maximum 15 digits as per E.164 standard).format: 'date-time'forreminderTime.maxLength: 1600for GSM 03.38 7-bit encoded messages per Plivo's documentation. Note: Messages containing Unicode (UCS-2 16-bit) characters have a 737-character limit.POST /: Creates a new reminder. Includes validation to ensurereminderTimeis in the future. Returns201 Created.GET /:id: Retrieves a reminder by its ID. Returns404 Not Foundif it doesn't exist.DELETE /:id: Deletes a reminder only if its status is'pending'. Returns404 Not Foundor400 Bad Requestif the reminder doesn't exist or isn't pending.GET /: Lists reminders (basic implementation, limited to 50). Real applications should add pagination and filtering.try...catchblocks to handle database errors and returns appropriate HTTP status codes (400, 404, 500) with structured error messages.request.log) for contextual logging within handlers.fastify.dbinstance injected by the plugin and prepared statements for database interactions.