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.
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:
curl
or 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.json
file.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.env
file.pino-pretty
: Formats Fastify's logs nicely during development.4. Install Development Dependencies:
nodemon
: Automatically restarts the server during development when file changes are detected.5. Configure
package.json
Scripts:Open
package.json
and add/modify thescripts
section:""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 usingnodemon
for auto-restarts andpino-pretty
for readable logs.6. Create Project Structure:
Create the following directories and files:
7. Create
.gitignore
:Create a
.gitignore
file in the root directory to prevent sensitive files and unnecessary directories from being committed to version control:8. Configure Environment Variables (
.env
):Create a
.env
file 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.9. Basic Logger Configuration:
Create
src/config/logger.js
:This configures the Pino logger, using
pino-pretty
for 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
.loadEnv
function for validation.buildApp
function to register plugins and routes (we'll create these next)./health
check endpoint.Now you have a basic project structure and setup ready. Run
npm run dev
in your terminal. You should see log output indicating the server has started, listening on port 3000. You can accesshttp://localhost:3000/health
in your browser or viacurl
to 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 thedb
decorator globally available.app.js
).reminders
table 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 EXISTS
statement, making it idempotent.updatedAt
on row updates.fastify.decorate('db', db)
makes the database connection accessible viafastify.db
orrequest.server.db
in handlers and other plugins.onClose
hook ensures the database connection is closed when the Fastify server stops.try...catch
block 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.sendSms
Method (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 client errors.null
after exhausting retries or encountering a non-retriable error.2. Integrate into Scheduler Plugin (Update
src/plugins/scheduler.js
):We need to instantiate
PlivoService
and make it available to our scheduled task.PlivoService
and Task Factory: Import the necessary modules.PlivoService
: Create an instance, passing credentials and the Fastify logger (fastify.log
).fastify.db
, theplivoService
instance, andfastify.log
to the task factory function (createSendRemindersTask
). This makes them available within the task's logic.'db-connector'
todependencies
to 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
):createSendRemindersTask
to accept dependencies (db
,plivoService
,logger
) via injection. This improves testability.AsyncTask
: The core logic is wrapped intoad-scheduler
'sAsyncTask
which provides structured execution and error handling.reminders
table for entries withstatus = 'pending'
andreminderTime <= now()
.LIMIT
to process reminders in batches_ preventing the task from holding resources for too long if there are many due reminders.reminderTime
to process older ones first.status
to'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.changes
is 0_ it means the status was already changed_ so the current task skips it.plivoService.sendSms
method (which now includes retry logic).plivoResponse
_ it updates the reminderstatus
to'sent'
or'failed'
.plivoMessageUuid
if the message was sent successfully.try...catch
blocks for database operations and the Plivo call. Attempts to mark the reminder as'failed'
if an error occurs during processing.errorHandler
function passed toAsyncTask
catches errors originating directly from thetaskLogic
execution 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/typebox
for defining clear request body, parameters, and response schemas. This enables automatic validation and serialization by Fastify.phoneNumber
format.format: 'date-time'
forreminderTime
.POST /
: Creates a new reminder. Includes validation to ensurereminderTime
is in the future. Returns201 Created
.GET /:id
: Retrieves a reminder by its ID. Returns404 Not Found
if it doesn't exist.DELETE /:id
: Deletes a reminder only if its status is'pending'
. Returns404 Not Found
or400 Bad Request
if 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...catch
blocks 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.db
instance injected by the plugin and prepared statements for database interactions.