NestJS SMS Scheduling Tutorial: Build Automated Reminders with Plivo & Cron Jobs
Complete guide to building automated SMS scheduling in NestJS with Plivo API. Learn cron jobs, TypeORM integration, retry logic, and production-ready SMS reminder systems for Node.js applications.
Build a production-ready SMS scheduling system using Plivo's SMS API with NestJS. This complete tutorial walks you through creating automated scheduled reminders, implementing reliable cron jobs with @nestjs/schedule, and managing SMS delivery with proper error handling for appointment reminders, payment notifications, and event alerts.
What you'll learn: Integrate Plivo SDK v4.x with NestJS v10.x for automated SMS scheduling, set up TypeORM v0.3.x with PostgreSQL for reminder storage, implement timezone-aware cron jobs using @nestjs/schedule v4.x, handle message queuing and retry logic, and deploy a scalable reminder system with transaction-based locking for distributed environments.
Prerequisites: Node.js v18 LTS or v20 LTS, PostgreSQL v14+, and a Plivo account with API credentials (sign up here).
Why Use NestJS and Plivo for Scheduled SMS Reminders?
Scheduled SMS reminders are essential for modern applications – whether you're building appointment scheduling systems, subscription renewal alerts, payment reminders, or event notifications. Automated SMS delivery ensures your users never miss critical information.
NestJS provides a robust framework with built-in dependency injection and task scheduling support, making it ideal for complex SMS scheduling logic. Combined with Plivo's reliable SMS API and competitive pricing (starting at $0.0040 per SMS in the US), you can build enterprise-grade reminder systems with:
Timezone-aware scheduling – Send messages at the right local time for each recipient
Database persistence – Track every reminder's status and delivery history
Scalable architecture – Run multiple instances without duplicate sends using transaction-based locking
Type safety – Leverage TypeScript for compile-time validation and better developer experience
This tutorial guides you through building a complete NestJS SMS scheduling system from scratch, with production-ready patterns you can deploy immediately.
How to Set Up Your NestJS Development Environment for SMS Scheduling
Install Node.js and Initialize Your NestJS Project
Start by ensuring you have Node.js v18 LTS or v20 LTS installed. These versions provide the best stability and long-term support as of October 2025. If you need to install or upgrade Node.js, download it from nodejs.org or use a version manager like nvm.
Bash
# Verify your Node.js version
node --version
# Create a new NestJS project
npx @nestjs/cli new plivo-reminders
# Navigate to your project directorycd plivo-reminders
When prompted, choose npm as your package manager for consistency with this tutorial.
Install Required Dependencies for SMS Scheduling
Install all necessary packages for scheduling, database management, and SMS delivery:
Bash
# Core dependencies
npm install @nestjs/schedule @nestjs/typeorm @nestjs/config typeorm pg plivo dotenv
# Development dependencies for TypeScript support
npm install --save-dev @types/node @types/cron
Package overview:
@nestjs/schedule v4.x – Provides cron job decorators and scheduler registry
@nestjs/typeorm – NestJS integration for TypeORM v0.3.x
@nestjs/config – Environment variable management
typeorm v0.3.x – Object-relational mapping with PostgreSQL support
pg – PostgreSQL client driver
plivo v4.x – Official Plivo SDK for SMS operations
dotenv – Environment variable loader
Configure Environment Variables for Plivo Integration
Create a .env file in your project root to store sensitive configuration:
Bash
# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=your_db_user
DB_PASSWORD=your_db_password
DB_DATABASE=plivo_reminders
# Plivo credentials (find these in your Plivo console at https://console.plivo.com/)
PLIVO_AUTH_ID=your_auth_id
PLIVO_AUTH_TOKEN=your_auth_token
PLIVO_PHONE_NUMBER=+14155552671
# Scheduling configuration
REMINDER_CHECK_CRON_TIME=*/5 * * * *
Cron expression syntax: The value */5 * * * * means "run every 5 minutes." The five positions represent: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–6, Sunday = 0). Learn more at crontab.guru.
Security note: Never commit your .env file to version control. Add it to your .gitignore file immediately.
Set Up PostgreSQL Database
Create a dedicated database for your reminder system. If you don't have PostgreSQL installed, download it from postgresql.org or use Docker:
Bash
# Using Docker (recommended for development)
docker run --name plivo-postgres -e POSTGRES_PASSWORD=your_password -p 5432:5432 -d postgres:14
# Or connect to an existing PostgreSQL instance
psql -U postgres
# Create the database
CREATE DATABASE plivo_reminders;
# Grant privileges (if using a specific user)
GRANT ALL PRIVILEGES ON DATABASE plivo_reminders TO your_db_user;
Use TypeORM's migration system to create your database schema safely:
Bash
# Generate migration from entity changes (TypeORM v0.3.x syntax)
npx typeorm-ts-node-commonjs migration:generate src/migrations/CreateReminderTable -d src/data-source.ts
# Run the migration
npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts
# To rollback if needed
npx typeorm-ts-node-commonjs migration:revert -d src/data-source.ts
TypeORM v0.3.x migration commands:
Use typeorm-ts-node-commonjs for TypeScript projects
The -d flag specifies your DataSource file path
Always review generated migrations before running them in production
Migration best practices:
Review generated SQL before applying to production
Test migrations on a staging database first
Keep migrations small and focused on specific changes
Never edit migrations after they've run in production
How to Integrate Plivo SMS API with NestJS
Create a dedicated service to handle all Plivo SMS operations. This separation of concerns makes your code more testable and maintainable.
Create the Plivo Service
Generate a new service for Plivo integration:
Bash
nest generate service plivo
Implement src/plivo/plivo.service.ts with comprehensive error handling:
TypeScript
import { Injectable, Logger } from'@nestjs/common';
import * as plivo from'plivo';
exportinterfaceSendSMSResult {
success: boolean;
messageUuid?: string;
error?: string;
}
@Injectable()
exportclassPlivoService {
privatereadonly logger = newLogger(PlivoService.name);
privateplivoClient: plivo.Client;
constructor() {
this.plivoClient = new plivo.Client(
process.env.PLIVO_AUTH_ID,
process.env.PLIVO_AUTH_TOKEN,
);
}
/**
* Send an SMS message using Plivo API
*
* @paramto - Recipient phone number in E.164 format (e.g., '+14155552671')
* @parammessage - Message content (up to 1,600 characters)
* @paramfrom - Sender phone number or alphanumeric sender ID
* @returns Promise with success status and message UUID or error details
*/asyncsendSMS(
to: string,
message: string,
from: string = process.env.PLIVO_PHONE_NUMBER,
): Promise<SendSMSResult> {
try {
if (!this.isValidE164(to)) {
thrownewError(`Invalid phone number format: ${to}. Use E.164 format like +14155552671`);
}
const response = awaitthis.plivoClient.messages.create({
src: from,
dst: to,
text: message,
});
this.logger.log(`SMS sent successfully to ${to}. Message UUID: ${response.messageUuid}`);
return {
success: true,
messageUuid: response.messageUuid[0],
};
} catch (error) {
this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack);
return {
success: false,
error: error.message || 'Unknown error occurred',
};
}
}
/**
* Validate phone number format (E.164)
* E.164 format: + followed by country code and national number
*/privateisValidE164(phoneNumber: string): boolean {
const e164Regex = /^\+[1-9]\d{1,14}$/;
return e164Regex.test(phoneNumber);
}
}
Implementation highlights:
E.164 validation – Ensures phone numbers follow the international standard format
Structured logging – Uses NestJS Logger for consistent log formatting
Error encapsulation – Returns structured results instead of throwing exceptions
Environment-based configuration – Keeps credentials secure in environment variables
SMS character limits: Plivo supports up to 1,600 characters per message. Messages longer than 160 characters (or 70 characters for Unicode) are automatically segmented into multiple SMS parts, each billed separately. See Plivo's SMS character limit documentation for details.
Register Plivo Module
Create src/plivo/plivo.module.ts to make your service available across the application:
Import this module in your RemindersModule to enable dependency injection.
How to Implement NestJS Cron Jobs for SMS Reminder Scheduling
Build the core scheduling logic that checks for due reminders and sends SMS messages using transaction-based locking for safety in distributed environments.
Create the Reminders Service with Cron Jobs
Update src/reminders/reminders.service.ts with scheduling logic:
TypeScript
import { Injectable, Logger } from'@nestjs/common';
import { InjectRepository } from'@nestjs/typeorm';
import { Repository, LessThanOrEqual, In } from'typeorm';
import { Cron, CronExpression } from'@nestjs/schedule';
import { Reminder } from'./entities/reminder.entity';
import { PlivoService } from'../plivo/plivo.service';
@Injectable()
exportclassRemindersService {
privatereadonly logger = newLogger(RemindersService.name);
privatereadonlyMAX_RETRIES = 3;
constructor(@InjectRepository(Reminder)
privatereminderRepository: Repository<Reminder>,
privateplivoService: PlivoService,
) {}
/**
* Cron job that checks for due reminders
*
* IMPORTANT: The @Cron decorator reads process.env.REMINDER_CHECK_CRON_TIME
* at application startup, not dynamically. To change the schedule at runtime,
* use SchedulerRegistry.addCronJob() with dynamic values instead.
*
* @see https://docs.nestjs.com/techniques/task-scheduling#dynamic-schedule-module-api
*/@Cron(process.env.REMINDER_CHECK_CRON_TIME || CronExpression.EVERY_MINUTE, {
name: 'reminder-check',
timeZone: 'UTC',
})
asynchandleCron() {
this.logger.debug('Checking for due reminders...');
awaitthis.reminderRepository.manager.transaction(async transactionalEntityManager => {
const dueReminders = await transactionalEntityManager.find(Reminder, {
where: {
scheduledFor: LessThanOrEqual(newDate()),
status: 'PENDING',
},
take: 100,
order: {
scheduledFor: 'ASC',
},
});
if (dueReminders.length === 0) {
this.logger.debug('No due reminders found');
return;
}
this.logger.log(`Found ${dueReminders.length} due reminder(s)`);
await transactionalEntityManager.update(
Reminder,
{ id: In(dueReminders.map(r => r.id)) },
{ status: 'PROCESSING' }
);
for (const reminder of dueReminders) {
awaitthis.processReminder(reminder);
}
});
}
/**
* Process a single reminder with retry logic
*/privateasyncprocessReminder(reminder: Reminder): Promise<void> {
try {
this.logger.log(`Processing reminder ${reminder.id} for ${reminder.phoneNumber}`);
const result = awaitthis.plivoService.sendSMS(
reminder.phoneNumber,
reminder.message,
);
if (result.success) {
awaitthis.reminderRepository.update(reminder.id, {
status: 'SENT',
plivoMessageUuid: result.messageUuid,
});
this.logger.log(`Reminder ${reminder.id} sent successfully`);
} else {
awaitthis.handleFailedReminder(reminder, result.error);
}
} catch (error) {
this.logger.error(
`Error processing reminder ${reminder.id}: ${error.message}`,
error.stack,
);
awaitthis.handleFailedReminder(reminder, error.message);
}
}
/**
* Handle failed SMS delivery with exponential backoff retry
*/privateasynchandleFailedReminder(
reminder: Reminder,
errorMessage: string,
): Promise<void> {
const newRetryCount = reminder.retryCount + 1;
if (newRetryCount <= this.MAX_RETRIES) {
const delayMinutes = Math.pow(5, newRetryCount - 1);
const newScheduledTime = newDate();
newScheduledTime.setMinutes(newScheduledTime.getMinutes() + delayMinutes);
awaitthis.reminderRepository.update(reminder.id, {
status: 'PENDING',
retryCount: newRetryCount,
scheduledFor: newScheduledTime,
errorMessage,
});
this.logger.warn(
`Reminder ${reminder.id} failed. Retry ${newRetryCount}/${this.MAX_RETRIES} scheduled for ${newScheduledTime}`,
);
} else {
awaitthis.reminderRepository.update(reminder.id, {
status: 'FAILED',
errorMessage: `Max retries (${this.MAX_RETRIES}) exceeded. Last error: ${errorMessage}`,
});
this.logger.error(`Reminder ${reminder.id} permanently failed after ${this.MAX_RETRIES} retries`);
}
}
/**
* Create a new reminder
*/asynccreateReminder(
phoneNumber: string,
message: string,
scheduledFor: Date,
): Promise<Reminder> {
const reminder = this.reminderRepository.create({
phoneNumber,
message,
scheduledFor,
status: 'PENDING',
});
returnawaitthis.reminderRepository.save(reminder);
}
/**
* Find all reminders
*/asyncfindAll(): Promise<Reminder[]> {
returnawaitthis.reminderRepository.find({
order: { scheduledFor: 'DESC' },
});
}
/**
* Find one reminder by ID
*/asyncfindOne(id: string): Promise<Reminder> {
returnawaitthis.reminderRepository.findOne({ where: { id } });
}
/**
* Remove a reminder by ID
*/asyncremove(id: string): Promise<void> {
awaitthis.reminderRepository.delete(id);
}
}
Key implementation details:
Transaction-based locking – The PROCESSING status change within a database transaction ensures that only one application instance processes each reminder, even in distributed deployments with multiple servers
Batch processing – The take: 100 limit prevents memory exhaustion when processing large reminder backlogs
Exponential backoff – Failed messages retry after 1, 5, and 25 minutes before permanent failure
Timezone awareness – Always use timeZone: 'UTC' in cron configuration to match PostgreSQL's timestamp with time zone storage
Crash recovery – If the process crashes during reminder processing, reminders stuck in PROCESSING status will remain there. Implement a separate cleanup job to reset stuck reminders after 30 minutes
Update Reminders Module
Ensure all dependencies are properly registered in src/reminders/reminders.module.ts:
How to Handle SMS Delivery Failures and Retry Logic
Robust error handling is critical for production SMS scheduling systems. This section covers retry strategies, exponential backoff, and failure recovery patterns.
Retry Logic with Exponential Backoff
The retry logic is implemented in the RemindersService.handleFailedReminder() method. Here's how the exponential backoff strategy works:
Exponential backoff calculation:
Retry 1: 5⁰ = 1 minute delay
Retry 2: 5¹ = 5 minutes delay
Retry 3: 5² = 25 minutes delay
After 3 failed attempts, the reminder is marked as permanently FAILED.
Why exponential backoff? This strategy prevents overwhelming the Plivo API during service disruptions while giving transient failures time to resolve. For permanent failures (like invalid phone numbers), the reminder fails quickly without unnecessary retries.
Common Failure Scenarios
Network failures: Temporary connectivity issues with Plivo API
Retry strategy: Exponential backoff usually resolves these automatically
Invalid phone numbers: Incorrectly formatted or disconnected numbers
Error message example: "Invalid phone number format" or "Number not reachable"
Resolution: Validate phone numbers before creating reminders using the E.164 format checker
Rate limiting: Exceeding Plivo's API rate limits
Plivo error code: 429 Too Many Requests
Resolution: Implement rate limiting in your application or increase batch processing interval
Insufficient credits: Plivo account balance too low
Plivo error code: 402 Payment Required
Resolution: Add funds to your Plivo account and set up balance alerts
For each scenario, the retry logic handles gracefully by storing error details and attempting redelivery. To prevent duplicate messages, each reminder is assigned a unique UUID and tracked through its entire lifecycle.
How to Create REST API Endpoints for SMS Reminder Management
Build API endpoints for creating, listing, and managing reminders.
Create the Reminders Controller
Generate a new controller for reminders:
Bash
nest generate controller reminders
Implement src/reminders/reminders.controller.ts with proper validation and error handling:
Production security note: Add authentication and authorization middleware before deploying this API. Never expose reminder management endpoints without proper access controls. Consider using @nestjs/passport with JWT tokens or API keys.
Create the DTO for Input Validation
Install the validation package:
Bash
npm install class-validator class-transformer
Create src/reminders/dto/create-reminder.dto.ts:
TypeScript
import { IsString, IsPhoneNumber, IsDateString, MinLength, MaxLength } from'class-validator';
exportclassCreateReminderDto {
@IsPhoneNumber(null, { message: 'Phone number must be in valid E.164 format' })
phoneNumber: string;
@IsString()
@MinLength(1, { message: 'Message cannot be empty' })
@MaxLength(1600, { message: 'Message exceeds SMS character limit' })
message: string;
@IsDateString({}, { message: 'Scheduled time must be a valid ISO date string' })
scheduledFor: Date;
}
Valid request example:
JSON
{"phoneNumber":"+14155552671","message":"Your appointment is scheduled for tomorrow at 3:00 PM. Reply CANCEL to reschedule.","scheduledFor":"2025-10-08T15:00:00Z"}
Verify transaction locking – Check logs for concurrent processing warnings
Inspect database isolation level – Ensure PostgreSQL transaction isolation is set to READ COMMITTED or higher
Review application instances – Confirm all instances use the same database
Query to find potential duplicates:
SQL
-- Find reminders processed multiple timesSELECT phone_number, message, COUNT(*) as send_count
FROM reminders
WHERE status ='SENT'GROUPBY phone_number, message, DATE_TRUNC('hour', scheduled_for)
HAVINGCOUNT(*) >1;
Performance Degradation
Batch size optimization:
TypeScript
// Adjust batch size in RemindersServicetake: 100, // Increase to 200 for high throughput, decrease to 50 for slower systems
Index maintenance:
SQL
-- Check for index bloatSELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE tablename ='reminders';
-- Rebuild indexes if needed
REINDEX TABLE reminders;
Connection pool monitoring:
SQL
-- View active database connectionsSELECTCOUNT(*), state
FROM pg_stat_activity
WHERE datname ='plivo_reminders'GROUPBY state;
Find stuck reminders:
SQL
-- Reminders stuck in PROCESSING for more than 30 minutesSELECT*FROM reminders
WHERE status ='PROCESSING'AND updated_at < NOW() -INTERVAL'30 minutes';
Next Steps and Advanced Features
Potential Enhancements
Recurring reminders: Add support for daily, weekly, or monthly schedules using cron expressions stored in the database. Create a separate recurring_reminders table with a cron_schedule column.
Template management: Create reusable SMS templates with variable substitution:
TypeScript
const template = "Hi {{name}}, your appointment is at {{time}}";
const message = template.replace('{{name}}', 'John').replace('{{time}}', '3:00 PM');
Timezone detection: Automatically determine recipient timezone from phone number using the Google libphonenumber library.
Delivery webhooks: Implement a webhook endpoint to receive real-time delivery status from Plivo:
TypeScript
@Post('plivo/status')
handleDeliveryStatus(@Body() payload: PlivoDeliveryStatus) {
// Update reminder status based on Plivo callback
}
Analytics dashboard: Track reminder success rates, delivery times, and failure patterns using aggregated database queries or a separate analytics database.
User preferences: Allow recipients to opt out or customize reminder timing by storing preferences in a user_preferences table.
Frequently Asked Questions About NestJS SMS Scheduling
How do I schedule SMS messages in NestJS?
Use the @nestjs/schedule package with the @Cron decorator to create scheduled tasks. Combine this with a database (like PostgreSQL with TypeORM) to store reminder data and an SMS provider (like Plivo) to send messages. The cron job checks the database for due reminders and triggers SMS delivery.
Can I use NestJS cron jobs for appointment reminders?
Yes, NestJS cron jobs are ideal for appointment reminders. Store appointment details in a database with a scheduled send time, then use a cron job to check for due reminders every few minutes. The transaction-based locking pattern prevents duplicate messages in distributed deployments.
What's the best SMS API for NestJS applications?
Plivo, Twilio, and MessageBird are popular choices for NestJS SMS integration. Plivo offers competitive pricing (starting at $0.0040 per SMS), reliable delivery, and a comprehensive Node.js SDK that integrates easily with NestJS dependency injection.
How do I prevent duplicate SMS in distributed NestJS apps?
Use transaction-based locking by wrapping your reminder queries in a database transaction and immediately updating the status to PROCESSING. This ensures only one instance processes each reminder, even when running multiple NestJS servers behind a load balancer.
This foundation can scale to handle thousands of reminders per day while maintaining reliability and preventing duplicate messages. Deploy your system, monitor performance metrics, and iterate based on real-world usage patterns.
Next steps:
Add authentication to your API endpoints
Set up monitoring and alerting
Deploy to production (AWS, Azure, Google Cloud, or your preferred platform)
Test with real phone numbers and monitor delivery rates
Use the @nestjs/schedule module with a cron job to trigger the RemindersService to check for due reminders and send them via the Plivo API. The cron job's frequency is configurable via the REMINDER_CHECK_CRON_TIME environment variable. The service queries the database for pending reminders and updates their status as they're processed.
Plivo is a cloud communications platform that provides the SMS sending functionality. The NestJS application integrates with Plivo's API using the official Plivo Node.js SDK to send SMS messages based on scheduled reminders stored in the database.
TypeORM simplifies database interactions by providing an object-relational mapper (ORM) for TypeScript. It allows you to define entities and interact with the database using familiar JavaScript methods, while PostgreSQL offers a robust and reliable database solution.
The synchronize: true setting in TypeORM should always be set to false in a production environment. While convenient for development, it can lead to data loss. Use TypeORM migrations to manage schema changes safely in production.
Yes, you can potentially adapt the code to use a different database. TypeORM supports various databases. You would need to change the type in the TypeOrmModule configuration and install the appropriate database driver for TypeORM.
Store your Plivo Auth ID, Auth Token, and sender number in a .env file. This file should be added to your .gitignore to prevent it from being committed to version control. The @nestjs/config module loads these variables into your application's environment.
These libraries provide robust validation and transformation for incoming request data in NestJS. class-validator allows you to define validation rules using decorators in your DTOs (Data Transfer Objects), while class-transformer automatically transforms request payloads into DTO instances.
The cron schedule is defined by the REMINDER_CHECK_CRON_TIME environment variable. Use cron expression syntax to specify the frequency. For example, * * * * * represents every minute, and 0 0 * * * represents every day at midnight. See crontab.guru for help with cron syntax.
If Plivo's API returns an error during SMS sending, the application logs the error, updates the reminder's status to FAILED, and stores the error message in the database. An optional retryFailedReminder method can be used to attempt resending failed messages.
The article recommends using an LTS (Long-Term Support) version of Node.js for this project. LTS versions receive security updates and bug fixes for an extended period, ensuring stability and compatibility.
Migrations are managed using the TypeORM CLI. After installing it (npm install -g typeorm), you can generate migrations with npm run migration:generate --name=YourMigrationName and run them with npm run migration:run. Ensure you build the project first with npm run build to compile the migration files.
The PROCESSING status is used to prevent race conditions in a multi-instance deployment, ensuring that a reminder is not picked up and processed multiple times concurrently. The cron job acquires a lock on pending reminders by setting their status to PROCESSING before sending the SMS.
NestJS modules promote modularity and organization. Features like Plivo integration, database interaction, and reminder management are separated into distinct modules. This improves maintainability and testability.
The sendAt field in the Reminder entity uses the timestamp with time zone data type in PostgreSQL to store dates with timezone awareness. It is crucial to consistently store dates in UTC within your database to avoid timezone-related issues.
Your Plivo Auth ID and Auth Token can be found in your Plivo console. After logging in, they are displayed on the main dashboard. You'll also need a Plivo phone number to send SMS messages from, which can be purchased or found in the 'Numbers' section under 'Messaging'.
Build a production-ready SMS scheduling system using Plivo's SMS API with NestJS. This complete tutorial walks you through creating automated scheduled reminders, implementing reliable cron jobs with @nestjs/schedule, and managing SMS delivery with proper error handling for appointment reminders, payment notifications, and event alerts.
What you'll learn: Integrate Plivo SDK v4.x with NestJS v10.x for automated SMS scheduling, set up TypeORM v0.3.x with PostgreSQL for reminder storage, implement timezone-aware cron jobs using @nestjs/schedule v4.x, handle message queuing and retry logic, and deploy a scalable reminder system with transaction-based locking for distributed environments.
Prerequisites: Node.js v18 LTS or v20 LTS, PostgreSQL v14+, and a Plivo account with API credentials (sign up here).
Why Use NestJS and Plivo for Scheduled SMS Reminders?
Scheduled SMS reminders are essential for modern applications – whether you're building appointment scheduling systems, subscription renewal alerts, payment reminders, or event notifications. Automated SMS delivery ensures your users never miss critical information.
NestJS provides a robust framework with built-in dependency injection and task scheduling support, making it ideal for complex SMS scheduling logic. Combined with Plivo's reliable SMS API and competitive pricing (starting at $0.0040 per SMS in the US), you can build enterprise-grade reminder systems with:
This tutorial guides you through building a complete NestJS SMS scheduling system from scratch, with production-ready patterns you can deploy immediately.
How to Set Up Your NestJS Development Environment for SMS Scheduling
Install Node.js and Initialize Your NestJS Project
Start by ensuring you have Node.js v18 LTS or v20 LTS installed. These versions provide the best stability and long-term support as of October 2025. If you need to install or upgrade Node.js, download it from nodejs.org or use a version manager like nvm.
# Verify your Node.js version node --version # Create a new NestJS project npx @nestjs/cli new plivo-reminders # Navigate to your project directory cd plivo-remindersWhen prompted, choose npm as your package manager for consistency with this tutorial.
Install Required Dependencies for SMS Scheduling
Install all necessary packages for scheduling, database management, and SMS delivery:
# Core dependencies npm install @nestjs/schedule @nestjs/typeorm @nestjs/config typeorm pg plivo dotenv # Development dependencies for TypeScript support npm install --save-dev @types/node @types/cronPackage overview:
@nestjs/schedulev4.x – Provides cron job decorators and scheduler registry@nestjs/typeorm– NestJS integration for TypeORM v0.3.x@nestjs/config– Environment variable managementtypeormv0.3.x – Object-relational mapping with PostgreSQL supportpg– PostgreSQL client driverplivov4.x – Official Plivo SDK for SMS operationsdotenv– Environment variable loaderConfigure Environment Variables for Plivo Integration
Create a
.envfile in your project root to store sensitive configuration:# Database configuration DB_HOST=localhost DB_PORT=5432 DB_USERNAME=your_db_user DB_PASSWORD=your_db_password DB_DATABASE=plivo_reminders # Plivo credentials (find these in your Plivo console at https://console.plivo.com/) PLIVO_AUTH_ID=your_auth_id PLIVO_AUTH_TOKEN=your_auth_token PLIVO_PHONE_NUMBER=+14155552671 # Scheduling configuration REMINDER_CHECK_CRON_TIME=*/5 * * * *Cron expression syntax: The value
*/5 * * * *means "run every 5 minutes." The five positions represent: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–6, Sunday = 0). Learn more at crontab.guru.Security note: Never commit your
.envfile to version control. Add it to your.gitignorefile immediately.Set Up PostgreSQL Database
Create a dedicated database for your reminder system. If you don't have PostgreSQL installed, download it from postgresql.org or use Docker:
# Using Docker (recommended for development) docker run --name plivo-postgres -e POSTGRES_PASSWORD=your_password -p 5432:5432 -d postgres:14 # Or connect to an existing PostgreSQL instance psql -U postgres # Create the database CREATE DATABASE plivo_reminders; # Grant privileges (if using a specific user) GRANT ALL PRIVILEGES ON DATABASE plivo_reminders TO your_db_user;Test your connection:
How to Create the SMS Reminder Entity with TypeORM
Define your reminder data structure using TypeORM entities. This entity represents each scheduled SMS reminder in your database.
Create the Entity File
Generate the reminders module structure:
# Generate module, controller, and service nest generate module reminders nest generate service remindersCreate
src/reminders/entities/reminder.entity.tswith this schema:import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, } from 'typeorm'; @Entity('reminders') export class Reminder { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'varchar', length: 20 }) @Index() phoneNumber: string; @Column({ type: 'text' }) message: string; @Column({ type: 'timestamp with time zone' }) @Index() scheduledFor: Date; @Column({ type: 'enum', enum: ['PENDING', 'PROCESSING', 'SENT', 'FAILED'], default: 'PENDING', }) @Index() status: 'PENDING' | 'PROCESSING' | 'SENT' | 'FAILED'; @Column({ type: 'varchar', nullable: true }) plivoMessageUuid: string; @Column({ type: 'text', nullable: true }) errorMessage: string; @Column({ type: 'int', default: 0 }) retryCount: number; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }Key design decisions:
phoneNumber,scheduledFor, andstatuscolumns have indexes for query performanceConfigure TypeORM with DataSource
TypeORM v0.3.x uses the
DataSourceAPI instead of the deprecatedConnectionOptionspattern. Createsrc/data-source.tsfor migration CLI support:import { DataSource, DataSourceOptions } from 'typeorm'; import { config } from 'dotenv'; import { Reminder } from './reminders/entities/reminder.entity'; config(); export const dataSourceOptions: DataSourceOptions = { type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432', 10), username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, database: process.env.DB_DATABASE, entities: [Reminder], migrations: ['src/migrations/*{.ts,.js}'], synchronize: false, logging: true, }; const dataSource = new DataSource(dataSourceOptions); export default dataSource;Critical configuration notes:
synchronize: false– Essential for production safety. Never use auto-schema-sync with migrationstypeorm-ts-node-commonjsexecutionUpdate
src/app.module.tsto initialize TypeORM:import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ScheduleModule } from '@nestjs/schedule'; import { ConfigModule } from '@nestjs/config'; import { dataSourceOptions } from './data-source'; import { RemindersModule } from './reminders/reminders.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), TypeOrmModule.forRoot(dataSourceOptions), ScheduleModule.forRoot(), RemindersModule, ], }) export class AppModule {}Generate and Run Your First Migration
Use TypeORM's migration system to create your database schema safely:
# Generate migration from entity changes (TypeORM v0.3.x syntax) npx typeorm-ts-node-commonjs migration:generate src/migrations/CreateReminderTable -d src/data-source.ts # Run the migration npx typeorm-ts-node-commonjs migration:run -d src/data-source.ts # To rollback if needed npx typeorm-ts-node-commonjs migration:revert -d src/data-source.tsTypeORM v0.3.x migration commands:
typeorm-ts-node-commonjsfor TypeScript projects-dflag specifies your DataSource file pathMigration best practices:
How to Integrate Plivo SMS API with NestJS
Create a dedicated service to handle all Plivo SMS operations. This separation of concerns makes your code more testable and maintainable.
Create the Plivo Service
Generate a new service for Plivo integration:
Implement
src/plivo/plivo.service.tswith comprehensive error handling:import { Injectable, Logger } from '@nestjs/common'; import * as plivo from 'plivo'; export interface SendSMSResult { success: boolean; messageUuid?: string; error?: string; } @Injectable() export class PlivoService { private readonly logger = new Logger(PlivoService.name); private plivoClient: plivo.Client; constructor() { this.plivoClient = new plivo.Client( process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN, ); } /** * Send an SMS message using Plivo API * * @param to - Recipient phone number in E.164 format (e.g., '+14155552671') * @param message - Message content (up to 1,600 characters) * @param from - Sender phone number or alphanumeric sender ID * @returns Promise with success status and message UUID or error details */ async sendSMS( to: string, message: string, from: string = process.env.PLIVO_PHONE_NUMBER, ): Promise<SendSMSResult> { try { if (!this.isValidE164(to)) { throw new Error(`Invalid phone number format: ${to}. Use E.164 format like +14155552671`); } const response = await this.plivoClient.messages.create({ src: from, dst: to, text: message, }); this.logger.log(`SMS sent successfully to ${to}. Message UUID: ${response.messageUuid}`); return { success: true, messageUuid: response.messageUuid[0], }; } catch (error) { this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack); return { success: false, error: error.message || 'Unknown error occurred', }; } } /** * Validate phone number format (E.164) * E.164 format: + followed by country code and national number */ private isValidE164(phoneNumber: string): boolean { const e164Regex = /^\+[1-9]\d{1,14}$/; return e164Regex.test(phoneNumber); } }Implementation highlights:
SMS character limits: Plivo supports up to 1,600 characters per message. Messages longer than 160 characters (or 70 characters for Unicode) are automatically segmented into multiple SMS parts, each billed separately. See Plivo's SMS character limit documentation for details.
Register Plivo Module
Create
src/plivo/plivo.module.tsto make your service available across the application:import { Module } from '@nestjs/common'; import { PlivoService } from './plivo.service'; @Module({ providers: [PlivoService], exports: [PlivoService], }) export class PlivoModule {}Import this module in your RemindersModule to enable dependency injection.
How to Implement NestJS Cron Jobs for SMS Reminder Scheduling
Build the core scheduling logic that checks for due reminders and sends SMS messages using transaction-based locking for safety in distributed environments.
Create the Reminders Service with Cron Jobs
Update
src/reminders/reminders.service.tswith scheduling logic:import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, LessThanOrEqual, In } from 'typeorm'; import { Cron, CronExpression } from '@nestjs/schedule'; import { Reminder } from './entities/reminder.entity'; import { PlivoService } from '../plivo/plivo.service'; @Injectable() export class RemindersService { private readonly logger = new Logger(RemindersService.name); private readonly MAX_RETRIES = 3; constructor( @InjectRepository(Reminder) private reminderRepository: Repository<Reminder>, private plivoService: PlivoService, ) {} /** * Cron job that checks for due reminders * * IMPORTANT: The @Cron decorator reads process.env.REMINDER_CHECK_CRON_TIME * at application startup, not dynamically. To change the schedule at runtime, * use SchedulerRegistry.addCronJob() with dynamic values instead. * * @see https://docs.nestjs.com/techniques/task-scheduling#dynamic-schedule-module-api */ @Cron(process.env.REMINDER_CHECK_CRON_TIME || CronExpression.EVERY_MINUTE, { name: 'reminder-check', timeZone: 'UTC', }) async handleCron() { this.logger.debug('Checking for due reminders...'); await this.reminderRepository.manager.transaction(async transactionalEntityManager => { const dueReminders = await transactionalEntityManager.find(Reminder, { where: { scheduledFor: LessThanOrEqual(new Date()), status: 'PENDING', }, take: 100, order: { scheduledFor: 'ASC', }, }); if (dueReminders.length === 0) { this.logger.debug('No due reminders found'); return; } this.logger.log(`Found ${dueReminders.length} due reminder(s)`); await transactionalEntityManager.update( Reminder, { id: In(dueReminders.map(r => r.id)) }, { status: 'PROCESSING' } ); for (const reminder of dueReminders) { await this.processReminder(reminder); } }); } /** * Process a single reminder with retry logic */ private async processReminder(reminder: Reminder): Promise<void> { try { this.logger.log(`Processing reminder ${reminder.id} for ${reminder.phoneNumber}`); const result = await this.plivoService.sendSMS( reminder.phoneNumber, reminder.message, ); if (result.success) { await this.reminderRepository.update(reminder.id, { status: 'SENT', plivoMessageUuid: result.messageUuid, }); this.logger.log(`Reminder ${reminder.id} sent successfully`); } else { await this.handleFailedReminder(reminder, result.error); } } catch (error) { this.logger.error( `Error processing reminder ${reminder.id}: ${error.message}`, error.stack, ); await this.handleFailedReminder(reminder, error.message); } } /** * Handle failed SMS delivery with exponential backoff retry */ private async handleFailedReminder( reminder: Reminder, errorMessage: string, ): Promise<void> { const newRetryCount = reminder.retryCount + 1; if (newRetryCount <= this.MAX_RETRIES) { const delayMinutes = Math.pow(5, newRetryCount - 1); const newScheduledTime = new Date(); newScheduledTime.setMinutes(newScheduledTime.getMinutes() + delayMinutes); await this.reminderRepository.update(reminder.id, { status: 'PENDING', retryCount: newRetryCount, scheduledFor: newScheduledTime, errorMessage, }); this.logger.warn( `Reminder ${reminder.id} failed. Retry ${newRetryCount}/${this.MAX_RETRIES} scheduled for ${newScheduledTime}`, ); } else { await this.reminderRepository.update(reminder.id, { status: 'FAILED', errorMessage: `Max retries (${this.MAX_RETRIES}) exceeded. Last error: ${errorMessage}`, }); this.logger.error(`Reminder ${reminder.id} permanently failed after ${this.MAX_RETRIES} retries`); } } /** * Create a new reminder */ async createReminder( phoneNumber: string, message: string, scheduledFor: Date, ): Promise<Reminder> { const reminder = this.reminderRepository.create({ phoneNumber, message, scheduledFor, status: 'PENDING', }); return await this.reminderRepository.save(reminder); } /** * Find all reminders */ async findAll(): Promise<Reminder[]> { return await this.reminderRepository.find({ order: { scheduledFor: 'DESC' }, }); } /** * Find one reminder by ID */ async findOne(id: string): Promise<Reminder> { return await this.reminderRepository.findOne({ where: { id } }); } /** * Remove a reminder by ID */ async remove(id: string): Promise<void> { await this.reminderRepository.delete(id); } }Key implementation details:
take: 100limit prevents memory exhaustion when processing large reminder backlogstimeZone: 'UTC'in cron configuration to match PostgreSQL'stimestamp with time zonestorageUpdate Reminders Module
Ensure all dependencies are properly registered in
src/reminders/reminders.module.ts:import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RemindersService } from './reminders.service'; import { Reminder } from './entities/reminder.entity'; import { PlivoModule } from '../plivo/plivo.module'; @Module({ imports: [ TypeOrmModule.forFeature([Reminder]), PlivoModule, ], providers: [RemindersService], exports: [RemindersService], }) export class RemindersModule {}How to Handle SMS Delivery Failures and Retry Logic
Robust error handling is critical for production SMS scheduling systems. This section covers retry strategies, exponential backoff, and failure recovery patterns.
Retry Logic with Exponential Backoff
The retry logic is implemented in the
RemindersService.handleFailedReminder()method. Here's how the exponential backoff strategy works:Exponential backoff calculation:
After 3 failed attempts, the reminder is marked as permanently
FAILED.Why exponential backoff? This strategy prevents overwhelming the Plivo API during service disruptions while giving transient failures time to resolve. For permanent failures (like invalid phone numbers), the reminder fails quickly without unnecessary retries.
Common Failure Scenarios
Network failures: Temporary connectivity issues with Plivo API
Invalid phone numbers: Incorrectly formatted or disconnected numbers
Rate limiting: Exceeding Plivo's API rate limits
Insufficient credits: Plivo account balance too low
For each scenario, the retry logic handles gracefully by storing error details and attempting redelivery. To prevent duplicate messages, each reminder is assigned a unique UUID and tracked through its entire lifecycle.
How to Create REST API Endpoints for SMS Reminder Management
Build API endpoints for creating, listing, and managing reminders.
Create the Reminders Controller
Generate a new controller for reminders:
Implement
src/reminders/reminders.controller.tswith proper validation and error handling:import { Controller, Post, Body, Get, Param, Delete, ValidationPipe } from '@nestjs/common'; import { RemindersService } from './reminders.service'; import { CreateReminderDto } from './dto/create-reminder.dto'; @Controller('reminders') export class RemindersController { constructor(private readonly remindersService: RemindersService) {} @Post() create(@Body(new ValidationPipe()) createReminderDto: CreateReminderDto) { return this.remindersService.createReminder( createReminderDto.phoneNumber, createReminderDto.message, createReminderDto.scheduledFor, ); } @Get() findAll() { return this.remindersService.findAll(); } @Get(':id') findOne(@Param('id') id: string) { return this.remindersService.findOne(id); } @Delete(':id') remove(@Param('id') id: string) { return this.remindersService.remove(id); } }Production security note: Add authentication and authorization middleware before deploying this API. Never expose reminder management endpoints without proper access controls. Consider using @nestjs/passport with JWT tokens or API keys.
Create the DTO for Input Validation
Install the validation package:
Create
src/reminders/dto/create-reminder.dto.ts:import { IsString, IsPhoneNumber, IsDateString, MinLength, MaxLength } from 'class-validator'; export class CreateReminderDto { @IsPhoneNumber(null, { message: 'Phone number must be in valid E.164 format' }) phoneNumber: string; @IsString() @MinLength(1, { message: 'Message cannot be empty' }) @MaxLength(1600, { message: 'Message exceeds SMS character limit' }) message: string; @IsDateString({}, { message: 'Scheduled time must be a valid ISO date string' }) scheduledFor: Date; }Valid request example:
{ "phoneNumber": "+14155552671", "message": "Your appointment is scheduled for tomorrow at 3:00 PM. Reply CANCEL to reschedule.", "scheduledFor": "2025-10-08T15:00:00Z" }Invalid request example:
{ "phoneNumber": "4155552671", "message": "", "scheduledFor": "tomorrow" }API endpoint highlights:
How to Test Your NestJS SMS Scheduling System
Unit Tests
Test your services and entities with NestJS testing utilities.
// src/reminders/reminders.service.spec.ts import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { RemindersService } from './reminders.service'; import { PlivoService } from '../plivo/plivo.service'; import { Reminder } from './entities/reminder.entity'; describe('RemindersService', () => { let service: RemindersService; let plivoService: PlivoService; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ RemindersService, { provide: getRepositoryToken(Reminder), useValue: { create: jest.fn(), save: jest.fn(), find: jest.fn(), update: jest.fn(), manager: { transaction: jest.fn(), }, }, }, { provide: PlivoService, useValue: { sendSMS: jest.fn(), }, }, ], }).compile(); service = module.get<RemindersService>(RemindersService); plivoService = module.get<PlivoService>(PlivoService); }); it('should be defined', () => { expect(service).toBeDefined(); }); it('should create a reminder', async () => { const mockReminder = { id: '123e4567-e89b-12d3-a456-426614174000', phoneNumber: '+14155552671', message: 'Test reminder', scheduledFor: new Date(), status: 'PENDING', }; jest.spyOn(service['reminderRepository'], 'create').mockReturnValue(mockReminder as any); jest.spyOn(service['reminderRepository'], 'save').mockResolvedValue(mockReminder as any); const result = await service.createReminder( '+14155552671', 'Test reminder', new Date(), ); expect(result).toEqual(mockReminder); }); it('should handle exponential backoff correctly', async () => { const mockReminder = { id: '123e4567-e89b-12d3-a456-426614174000', phoneNumber: '+14155552671', message: 'Test reminder', scheduledFor: new Date(), status: 'PENDING', retryCount: 0, }; jest.spyOn(service['reminderRepository'], 'update').mockResolvedValue(null); await service['handleFailedReminder'](mockReminder as Reminder, 'Test error'); expect(service['reminderRepository'].update).toHaveBeenCalledWith( mockReminder.id, expect.objectContaining({ status: 'PENDING', retryCount: 1, }) ); }); });Integration Testing Best Practices
.env.testfile@nestjs/scheduletesting utilities to trigger cron jobs manually without waitingExample test database configuration:
# .env.test DB_DATABASE=plivo_reminders_test PLIVO_AUTH_ID=test_auth_id PLIVO_AUTH_TOKEN=test_auth_tokenProduction Deployment Considerations for NestJS SMS Systems
Environment Configuration
Ensure all environment variables are properly set in production:
Example startup validation:
// src/main.ts async function bootstrap() { const requiredEnvVars = [ 'DB_HOST', 'DB_USERNAME', 'DB_PASSWORD', 'DB_DATABASE', 'PLIVO_AUTH_ID', 'PLIVO_AUTH_TOKEN', 'PLIVO_PHONE_NUMBER' ]; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { throw new Error(`Missing required environment variable: ${envVar}`); } } const app = await NestJSFactory.create(AppModule); await app.listen(3000); } bootstrap();Monitoring and Alerting
Set up monitoring for these critical metrics:
Recommended tools: Prometheus + Grafana for metrics, Sentry for error tracking, or DataDog for all-in-one monitoring.
Scaling Considerations
Example connection pool configuration:
// src/data-source.ts export const dataSourceOptions: DataSourceOptions = { // ... other options extra: { max: 20, // Maximum connections in pool min: 5, // Minimum connections maintained idleTimeoutMillis: 30000, }, };Troubleshooting Common SMS Scheduling Issues
Reminders Not Sending
Check cron job execution:
# View application logs for "Checking for due reminders" messages grep "Checking for due reminders" logs/app.logDatabase connectivity:
-- Test database connection SELECT NOW(); -- Check for due reminders SELECT * FROM reminders WHERE status = 'PENDING' AND scheduled_for <= NOW();Plivo credentials:
# Test Plivo API credentials curl -X GET https://api.plivo.com/v1/Account/{auth_id}/ \ -u {auth_id}:{auth_token}Phone number format:
// Verify E.164 format compliance const validFormat = /^\+[1-9]\d{1,14}$/; console.log(validFormat.test('+14155552671')); // true console.log(validFormat.test('4155552671')); // falseDuplicate SMS Messages
If users receive duplicate messages:
Query to find potential duplicates:
-- Find reminders processed multiple times SELECT phone_number, message, COUNT(*) as send_count FROM reminders WHERE status = 'SENT' GROUP BY phone_number, message, DATE_TRUNC('hour', scheduled_for) HAVING COUNT(*) > 1;Performance Degradation
Batch size optimization:
// Adjust batch size in RemindersService take: 100, // Increase to 200 for high throughput, decrease to 50 for slower systemsIndex maintenance:
-- Check for index bloat SELECT schemaname, tablename, attname, n_distinct, correlation FROM pg_stats WHERE tablename = 'reminders'; -- Rebuild indexes if needed REINDEX TABLE reminders;Connection pool monitoring:
-- View active database connections SELECT COUNT(*), state FROM pg_stat_activity WHERE datname = 'plivo_reminders' GROUP BY state;Find stuck reminders:
-- Reminders stuck in PROCESSING for more than 30 minutes SELECT * FROM reminders WHERE status = 'PROCESSING' AND updated_at < NOW() - INTERVAL '30 minutes';Next Steps and Advanced Features
Potential Enhancements
Recurring reminders: Add support for daily, weekly, or monthly schedules using cron expressions stored in the database. Create a separate
recurring_reminderstable with acron_schedulecolumn.Template management: Create reusable SMS templates with variable substitution:
const template = "Hi {{name}}, your appointment is at {{time}}"; const message = template.replace('{{name}}', 'John').replace('{{time}}', '3:00 PM');Timezone detection: Automatically determine recipient timezone from phone number using the Google libphonenumber library.
Delivery webhooks: Implement a webhook endpoint to receive real-time delivery status from Plivo:
@Post('plivo/status') handleDeliveryStatus(@Body() payload: PlivoDeliveryStatus) { // Update reminder status based on Plivo callback }Analytics dashboard: Track reminder success rates, delivery times, and failure patterns using aggregated database queries or a separate analytics database.
User preferences: Allow recipients to opt out or customize reminder timing by storing preferences in a
user_preferencestable.Frequently Asked Questions About NestJS SMS Scheduling
How do I schedule SMS messages in NestJS?
Use the
@nestjs/schedulepackage with the@Crondecorator to create scheduled tasks. Combine this with a database (like PostgreSQL with TypeORM) to store reminder data and an SMS provider (like Plivo) to send messages. The cron job checks the database for due reminders and triggers SMS delivery.Can I use NestJS cron jobs for appointment reminders?
Yes, NestJS cron jobs are ideal for appointment reminders. Store appointment details in a database with a scheduled send time, then use a cron job to check for due reminders every few minutes. The transaction-based locking pattern prevents duplicate messages in distributed deployments.
What's the best SMS API for NestJS applications?
Plivo, Twilio, and MessageBird are popular choices for NestJS SMS integration. Plivo offers competitive pricing (starting at $0.0040 per SMS), reliable delivery, and a comprehensive Node.js SDK that integrates easily with NestJS dependency injection.
How do I prevent duplicate SMS in distributed NestJS apps?
Use transaction-based locking by wrapping your reminder queries in a database transaction and immediately updating the status to PROCESSING. This ensures only one instance processes each reminder, even when running multiple NestJS servers behind a load balancer.
Related Tutorials
Conclusion
You've built a production-ready SMS scheduling system using Plivo and NestJS with:
✓ Timezone-aware cron job scheduling ✓ Transaction-based locking for distributed deployments ✓ Exponential backoff retry logic ✓ Comprehensive error handling ✓ TypeORM database persistence ✓ RESTful API endpoints
This foundation can scale to handle thousands of reminders per day while maintaining reliability and preventing duplicate messages. Deploy your system, monitor performance metrics, and iterate based on real-world usage patterns.
Next steps:
For questions or issues, refer to Plivo's documentation or NestJS documentation.