code examples
code examples
Build a RedwoodJS App with Plivo Scheduled Reminders
A guide to building a full-stack RedwoodJS application for scheduling and sending SMS reminders using the Plivo API, covering setup, backend logic, database modeling, and deployment considerations.
This guide provides a comprehensive walkthrough for building a full-stack application using RedwoodJS that enables users to schedule SMS reminders sent via Plivo. We'll cover everything from project setup and core logic implementation to deployment and best practices for a production-ready system.
By the end of this tutorial, you will have a functional web application where authenticated users can create, view, and delete reminders. A background process will periodically check for due reminders and use the Plivo API to dispatch them as SMS messages. This guide focuses heavily on the backend implementation details necessary for the scheduling and notification functionality.
Project Overview and Goals
What We're Building:
A multi-user web application where individuals can schedule SMS messages (reminders) to be sent to a specified phone number at a future date and time.
Problem Solved:
Automates the process of sending timely reminders via SMS, useful for appointments, tasks, events, or any time-sensitive notification. It provides a persistent, reliable mechanism managed through a web interface.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the modern web. It provides structure for the API (GraphQL), web frontend (React), database access (Prisma), and simplifies setup, auth, and deployment. We use it for its integrated tooling and conventions.
- Plivo: A Communications Platform as a Service (CPaaS). We use Plivo's SMS API to send the scheduled reminders. Its reliability and developer-friendly API make it a solid choice.
- Node.js: The runtime environment for RedwoodJS's backend API layer where our scheduling and Plivo integration logic will reside.
- Prisma: A next-generation Node.js and TypeScript ORM. RedwoodJS uses Prisma for database modeling, migrations, and type-safe database access.
- PostgreSQL: A powerful, open-source object-relational database system. We'll use it to store user data and reminder schedules. (Other Prisma-compatible databases like MySQL or SQLite can also be used).
node-cron: A simple cron-like job scheduler for Node.js. We'll use this for demonstrating the scheduling mechanism within the Node.js process. Note: For production serverless deployments, an external scheduler (like Vercel Cron Jobs, AWS EventBridge Scheduler) is strongly recommended.
System Architecture:
+-------------+ +-------------------+ +-----------------+ +-------+
| User via | ----> | RedwoodJS Frontend| ----> | RedwoodJS API | ----> | Plivo |
| Web Browser | | (React) | | (GraphQL, Node.js)| | (SMS) |
+-------------+ +-------------------+ +--------+--------+ +-------+
|
| +------------------+
+-> | PostgreSQL DB |
| | (Prisma) |
| +------------------+
|
| +------------------+
+-> | Scheduling Logic |
| (`node-cron` or |
| external trigger)|
+------------------+(Note: Consider using a graphical diagram (SVG/PNG) for clearer presentation if your platform supports it.)
- A user interacts with the React frontend served by RedwoodJS.
- The frontend communicates with the RedwoodJS GraphQL API for actions like creating, fetching, or deleting reminders.
- The API uses Prisma services to interact with the PostgreSQL database, storing user and reminder information.
- Authentication ensures users can only manage their own reminders.
- A separate scheduling process (initially
node-cronwithin the API, ideally an external trigger in production) runs periodically. - The scheduler queries the database via Prisma for reminders due to be sent.
- For each due reminder, the API calls the Plivo SMS API to send the message.
- The reminder status is updated in the database.
Prerequisites:
- Node.js: Version 20.x or higher recommended (check RedwoodJS docs for specific version compatibility). Use
nvmto manage Node versions. - Yarn: Version 1.22.x or higher.
- Plivo Account: Sign up for a Plivo account to get API credentials and a Plivo phone number capable of sending SMS.
- Database: Access to a PostgreSQL database. We'll include steps for running one locally using Docker.
- Git: (Recommended) For version control.
- Basic understanding: JavaScript/TypeScript, React, GraphQL, and command-line usage.
Final Outcome:
A secure, authenticated RedwoodJS application enabling users to schedule and receive SMS reminders via Plivo, with a clear path to production deployment.
1. Setting up the Project
Let's initialize our RedwoodJS application and configure the essential tools.
-
Create the RedwoodJS App: Open your terminal and run the
create redwood-appcommand. We'll use TypeScript for enhanced type safety.bashyarn create redwood-app plivo-scheduler --typescript- Follow the prompts:
- Initialize git repo? (Recommended:
yes) - Enter commit message? (
Initial commit) - Run yarn install? (
yes)
- Initialize git repo? (Recommended:
- Follow the prompts:
-
Navigate to Project Directory:
bashcd plivo-scheduler -
Verify Node and Yarn Versions: Ensure your Node.js and Yarn versions meet the prerequisites mentioned earlier.
bashnode -v yarn -v -
Set up Local PostgreSQL Database (using Docker): If you don't have PostgreSQL running, Docker is a convenient way to start a local instance.
bash# Pull the official PostgreSQL image docker pull postgres # Run a container named 'plivo-scheduler-db' # Replace 'your_postgres_password' with a secure password docker run --name plivo-scheduler-db -e POSTGRES_PASSWORD=your_postgres_password -d -p 5432:5432 postgres # Wait a few seconds for the container to initialize, then create the database # Connect to the running container and open psql docker exec -it plivo-scheduler-db psql -U postgres # Inside psql: # CREATE DATABASE plivo_scheduler; # \l # (Optional: List databases to verify creation) # \q # (Exit psql)- Explanation: This downloads the Postgres image and runs a container in detached mode (
-d), mapping port 5432 on your host to the container's port 5432. It sets the defaultpostgresuser's password. We then usedocker execto run thepsqlcommand-line tool inside the container and execute theCREATE DATABASEcommand to set up theplivo_schedulerdatabase.
- Explanation: This downloads the Postgres image and runs a container in detached mode (
-
Configure Environment Variables: RedwoodJS uses a
.envfile for environment variables. Create this file in the project root. Never commit.envfiles to Git. Add your database connection string.sh# .env DATABASE_URL=postgresql://postgres:your_postgres_password@localhost:5432/plivo_scheduler?schema=public # Add Plivo credentials later in Step 4 # PLIVO_AUTH_ID= # PLIVO_AUTH_TOKEN= # PLIVO_SENDER_NUMBER=- Explanation:
DATABASE_URLtells Prisma how to connect to your database. Adjust the username, password, host, port, and database name if they differ from the Docker example. The?schema=publicpart is often required. Quotes around the value are typically not needed unless the value contains special characters problematic for the shell.
- Explanation:
-
Initialize Prisma: Apply the initial database schema (which is currently empty besides Prisma's internal tracking).
bashyarn rw prisma migrate dev --name initial-setup- Explanation: This command compares your
schema.prismafile with the database, generates SQL migration files inapi/db/migrations, and applies them to your database. The--nameflag provides a descriptive name for the migration.
- Explanation: This command compares your
-
Start the Development Server: Verify the basic setup works.
bashyarn rw dev- Your browser should open to
http://localhost:8910. - The API server runs on
http://localhost:8911. - The GraphQL playground is accessible at
http://localhost:8911/graphql.
Keep this running in a separate terminal tab during development.
- Your browser should open to
2. Implementing Core Functionality (Scheduling Logic)
We'll store reminders in the database and use node-cron to check periodically for reminders that need sending.
-
Install
node-cron: Add thenode-cronlibrary to the API workspace.bashyarn workspace api add node-cron @types/node-cron --dev -
Define the Reminder Database Model: Edit the Prisma schema file (
api/db/schema.prisma) to define the structure for storing reminders.prisma// api/db/schema.prisma datasource db { provider = ""postgresql"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" } // Define the Reminder model model Reminder { id Int @id @default(autoincrement()) message String recipientPhoneNumber String // Phone number to send the SMS to scheduledAt DateTime // When the reminder should be sent (MUST be stored in UTC) status String @default(""PENDING"") // PENDING, SENT, FAILED createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId Int // Foreign key to link to the User model user User @relation(fields: [userId], references: [id]) // Index optimized for the scheduler's primary query (finding pending/due reminders across all users) @@index([status, scheduledAt]) // Index for efficient lookup of reminders by user (e.g., in the API) @@index([userId]) } // Define the User model (basic example, add more fields as needed) model User { id Int @id @default(autoincrement()) email String @unique hashedPassword String? salt String? resetToken String? resetTokenExpiresAt DateTime? reminders Reminder[] // Relation to Reminders createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }- Explanation:
Remindermodel stores the message content, recipient number, scheduled time (scheduledAtshould always be stored in UTC), current status, and a link (userId) to the user who created it.status: Tracks the lifecycle ('PENDING', 'SENT', 'FAILED').userId/user: Creates a one-to-many relationship betweenUserandReminder.@@index([status, scheduledAt]): Crucial index for scheduler performance. Allows the database to efficiently find rows matchingstatus = 'PENDING'andscheduledAt <= now().@@index([userId]): Supports efficient querying for reminders belonging to a specific user (e.g., in the API layer).Usermodel: Includes basic fields needed for authentication later (using Redwood'sdbAuth).
- Explanation:
-
Apply Database Migrations: Generate and apply the migration for the new models.
bashyarn rw prisma migrate dev --name add-reminder-and-user-models -
Create the Scheduler Service: This service (
api/src/lib/scheduler.ts) will contain the logic to find and process due reminders.typescript// api/src/lib/scheduler.ts import cron from 'node-cron' import { db } from './db' // Redwood's Prisma client instance import { logger } from './logger' // Redwood's logger import { sendSms } from './smsSender' // We will create this later in Step 4 const SCHEDULE = process.env.CRON_SCHEDULE || '*/1 * * * *' // Run every minute by default let isJobRunning = false // Simple lock to prevent overlap logger.info(`Scheduler configured to run with schedule: ${SCHEDULE}`) // Schedule the task cron.schedule(SCHEDULE, async () => { if (isJobRunning) { logger.warn('Previous scheduler job still running, skipping this run.') return } isJobRunning = true logger.info('Scheduler job started: Checking for due reminders...') try { // **IMPORTANT**: `new Date()` uses the server's system time. // For reliable comparison with the UTC `scheduledAt` from the database, // the server environment where this code runs *should* be configured to UTC. // This is the simplest way to ensure correct time comparisons. const now = new Date() // Current time (ideally UTC) // Find reminders that are PENDING and scheduled for now or earlier const dueReminders = await db.reminder.findMany({ where: { status: 'PENDING', scheduledAt: { lte: now, // Less than or equal to the current time }, }, include: { user: true, // Include user info if needed later (e.g., for context) }, // Limit the batch size to prevent overwhelming resources take: parseInt(process.env.SCHEDULER_BATCH_SIZE || '50', 10), }) logger.info(`Found ${dueReminders.length} due reminders.`) if (dueReminders.length === 0) { logger.info('No due reminders to process.') isJobRunning = false return } // Process each reminder for (const reminder of dueReminders) { try { logger.info(`Processing reminder ID: ${reminder.id} for user ${reminder.userId}`) // 1. Attempt to send the SMS via Plivo const plivoResponse = await sendSms( reminder.recipientPhoneNumber, reminder.message ) // 2. Update reminder status to SENT if successful // Plivo acceptance confirmed by presence of messageUuid if (plivoResponse && plivoResponse.messageUuid) { await db.reminder.update({ where: { id: reminder.id }, data: { status: 'SENT' }, }) logger.info(`Successfully sent reminder ID: ${reminder.id}, Plivo Message UUID: ${plivoResponse.messageUuid}`) } else { // Handle cases where sendSms might return null/undefined or lack UUID on failure throw new Error('Plivo SMS sending failed or did not return expected response.') } } catch (error: any) { // 3. Update reminder status to FAILED if sending fails logger.error( `Failed to process reminder ID: ${reminder.id}. Error: ${error.message}` ) await db.reminder.update({ where: { id: reminder.id }, data: { status: 'FAILED' }, // Consider adding an error message field to the model }) } } // end for loop logger.info('Finished processing batch of reminders.') } catch (error: any) { logger.error(`Error during scheduler run: ${error.message}`, error.stack) } finally { isJobRunning = false // Release the lock logger.info('Scheduler job finished.') } }, { scheduled: true, // Timezone: For consistency, strongly recommend running the server/container in UTC. // `new Date()` uses system time, so UTC avoids timezone conversion complexities. // If UTC environment isn't possible, you *can* set a specific timezone here, // but ensure all time comparisons are handled correctly. // timezone: ""Etc/UTC"" // Example: Explicitly set UTC if needed }) logger.info('Cron job scheduled.') // Export a function to potentially stop the scheduler if needed (e.g., during tests) export const stopScheduler = () => { // `cron.getTasks()` returns a Map; iterate and stop each task cron.getTasks().forEach(task => task.stop()); logger.info('Scheduler stopped.') }- Explanation:
- Imports
node-cron, Prisma (db), logger, and thesendSmsfunction (to be created). - Defines a
SCHEDULE(defaulting to every minute). Configurable via.env. - Uses a simple
isJobRunningflag as a lock to prevent the job from running concurrently if a previous run takes too long. cron.schedulesets up the job.- Inside the job:
- Timezone Critical: Queries the DB for
PENDINGreminders wherescheduledAt(UTC) is less than or equal to the current time (now). It's crucial the server runs in UTC sonew Date()reflects UTC, ensuring accurate comparison. - Uses
taketo process reminders in batches (configurable viaSCHEDULER_BATCH_SIZE). - Loops through due reminders.
- Calls
sendSms. - Updates the status to
SENTon success orFAILEDon error usingdb.reminder.update. - Includes robust logging and error handling using
try...catch.
- Timezone Critical: Queries the DB for
- The
finallyblock ensures theisJobRunninglock is always released. - Adds a note reinforcing the importance of UTC for the server environment.
- Imports
- Explanation:
-
Start the Scheduler: The scheduler needs to be initialized when the API server starts. A common place in Redwood is within the server configuration or alongside the db client initialization, ensuring it runs once. Let's modify the db client setup file (
api/src/lib/db.ts).typescript// api/src/lib/db.ts (or db.js) // ... other imports (like PrismaClient) import { logger } from './logger' // Redwood's default Prisma client setup import { PrismaClient } from '@prisma/client' export const db = new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'], }) // Import and initialize the scheduler *once* // ======================================================================== // **WARNING:** THIS APPROACH IS **NOT SUITABLE** FOR SERVERLESS ENVIRONMENTS // ======================================================================== // `node-cron` relies on a long-running process. Standard serverless functions // (like Vercel or Netlify) are ephemeral and won't keep the cron job running. // For serverless, you MUST use an external scheduler (e.g., Vercel Cron Jobs, // Netlify Scheduled Functions, AWS EventBridge) to trigger a dedicated API function. // See the Deployment section (Step 12) for the correct serverless approach. // This setup is only appropriate for local development or traditional, // long-running serverful deployments (e.g., Docker container on EC2/Render/Fly.io). // ------------------------------------------------------------------------ if (process.env.NODE_ENV !== 'test' && process.env.DISABLE_SCHEDULER !== 'true') { logger.info('Initializing node-cron scheduler for serverful/dev environment...') import('./scheduler').catch(err => { logger.error('Failed to load node-cron scheduler:', err) }) } else if (process.env.DISABLE_SCHEDULER === 'true'){ logger.warn('Scheduler explicitly disabled via DISABLE_SCHEDULER env var.') } else { logger.info('Scheduler disabled in test environment.') } // ... rest of the file if any- Explanation:
- We import the
./schedulerfile after thedbclient is potentially initialized. The dynamicimport()ensures it runs. - We add checks (
process.env.NODE_ENV !== 'test',process.env.DISABLE_SCHEDULER !== 'true') to prevent the scheduler from running during tests or if explicitly disabled via an environment variable. - Crucially, a prominent warning explains this
node-croninitialization method is unsuitable for serverless and directs the user to the Deployment section for the correct approach using external triggers.
- We import the
- Explanation:
3. Building the API Layer (GraphQL)
We'll use Redwood's generators to create the GraphQL schema definition (SDL) and service implementations for managing reminders.
-
Set up Authentication: Reminders should be user-specific. We'll use Redwood's built-in
dbAuth.bashyarn rw setup auth dbAuth- This command modifies files to integrate database-backed authentication:
- Adds required fields (
hashedPassword,salt, etc.) to theUsermodel inschema.prisma(we already added them, but it ensures they're standard). - Generates API functions (
api/src/functions/auth.ts). - Generates auth service (
api/src/services/auth.ts). - Adds frontend pages for login, signup, etc. (
web/src/pages/LoginPage,web/src/pages/SignupPage, etc.). - Sets up the
AuthContext(web/src/auth.ts).
- Adds required fields (
- Important: After running
yarn rw setup auth dbAuth, if you hadn't already manually added the necessary fields (hashedPassword,salt, etc.) to yourUsermodel inschema.prisma(as we did in our example), you must run a database migration to add them:bashyarn rw prisma migrate dev --name setup-dbAuth
- This command modifies files to integrate database-backed authentication:
-
Generate SDL and Services for Reminder:
bashyarn rw g sdl Reminder --crud- Explanation: This command inspects your
Remindermodel inschema.prismaand generates:api/src/graphql/reminders.sdl.ts: Defines the GraphQL schema types (Reminder,CreateReminderInput,UpdateReminderInput) and CRUD operations (queries likereminders,reminder, and mutations likecreateReminder,updateReminder,deleteReminder).api/src/services/reminders/reminders.ts: Implements the resolver functions for the generated SDL operations, interacting with the database via Prisma.api/src/services/reminders/reminders.scenarios.ts: Seed data for testing.api/src/services/reminders/reminders.test.ts: Basic test structure.
- Explanation: This command inspects your
-
Secure the Reminder Service: Modify the generated service file (
api/src/services/reminders/reminders.ts) to ensure users can only access and manage their own reminders. Use Redwood'srequireAuthutility and filter queries/mutations by the current user's ID.typescript// api/src/services/reminders/reminders.ts import type { QueryResolvers, MutationResolvers, ReminderRelationResolvers } from 'types/graphql' import { RedwoodGraphQLError, ForbiddenError } from '@redwoodjs/graphql-server' // Use Redwood's error classes import { db } from 'src/lib/db' import { requireAuth } from 'src/lib/auth' // Import requireAuth // Query to get all reminders for the logged-in user export const reminders: QueryResolvers['reminders'] = () => { requireAuth() // Ensure user is logged in return db.reminder.findMany({ where: { userId: context.currentUser?.id }, // Filter by logged-in user ID orderBy: { scheduledAt: 'asc' } // Order by scheduled time }) } // Query to get a single reminder, ensuring it belongs to the logged-in user export const reminder: QueryResolvers['reminder'] = async ({ id }) => { requireAuth() const reminder = await db.reminder.findFirst({ where: { id, userId: context.currentUser?.id }, // Check both ID and user ID }) if (!reminder) { throw new RedwoodGraphQLError('Reminder not found.') } return reminder } // Mutation to create a new reminder for the logged-in user export const createReminder: MutationResolvers['createReminder'] = ({ input }) => { requireAuth() // Add validation if needed (e.g., using Redwood Service Validations - see Step 7) // validate(input.recipientPhoneNumber, 'recipientPhoneNumber', { presence: true, /* other rules */ }) return db.reminder.create({ data: { ...input, userId: context.currentUser?.id, // Assign reminder to the logged-in user status: 'PENDING', // Ensure initial status is PENDING }, }) } // Mutation to update a reminder, ensuring it belongs to the logged-in user export const updateReminder: MutationResolvers['updateReminder'] = async ({ id, input }) => { requireAuth() // First, verify the reminder exists and belongs to the user const existingReminder = await db.reminder.findFirst({ where: { id, userId: context.currentUser?.id }, }) if (!existingReminder) { throw new RedwoodGraphQLError('Reminder not found or you do not have permission to update it.') } // Prevent changing userId or potentially sensitive fields if needed // Prevent updating if status is already SENT or FAILED? (Business logic decision) if (existingReminder.status !== 'PENDING') { throw new RedwoodGraphQLError(`Cannot update a reminder with status: ${existingReminder.status}`) } return db.reminder.update({ data: input, // Only update fields provided in input where: { id }, }) } // Mutation to delete a reminder, ensuring it belongs to the logged-in user export const deleteReminder: MutationResolvers['deleteReminder'] = async ({ id }) => { requireAuth() // First, verify the reminder exists and belongs to the user const existingReminder = await db.reminder.findFirst({ where: { id, userId: context.currentUser?.id }, }) if (!existingReminder) { throw new RedwoodGraphQLError('Reminder not found or you do not have permission to delete it.') } return db.reminder.delete({ where: { id }, }) } // Resolver for the relation between Reminder and User export const Reminder: ReminderRelationResolvers = { user: (_obj, { root }) => { return db.reminder.findUnique({ where: { id: root?.id } }).user() }, }- Explanation:
requireAuth(): This function (provided byyarn rw setup auth dbAuth) checks if a user is logged in (context.currentUseris set). If not, it throws anAuthenticationError.where: { userId: context.currentUser?.id }: This clause is added tofindMany,findFirst,update, anddeleteoperations (or checks preceding them) to ensure actions only affect reminders belonging to the currently authenticated user.RedwoodGraphQLError: Used for user-facing errors like ""Reminder not found.""- Added a check in
updateReminderto prevent updating non-pending reminders (example of business logic).
- Explanation:
-
Testing API Endpoints (Example using GraphQL Playground):
-
Start the dev server:
yarn rw dev -
Sign up and log in through the frontend UI (e.g.,
http://localhost:8910/signup,http://localhost:8910/login). -
Open the GraphQL Playground:
http://localhost:8911/graphql -
The Playground usually handles authentication cookies automatically after you log in via the frontend on the same
localhostdomain. If not, you might need to manually copy authentication headers/tokens. -
Create Reminder Mutation:
graphqlmutation CreateReminderMutation($input: CreateReminderInput!) { createReminder(input: $input) { id message recipientPhoneNumber scheduledAt status userId } }Variables:
json{ ""input"": { ""message"": ""Test reminder from GraphQL Playground"", ""recipientPhoneNumber"": ""+15551234567"", ""scheduledAt"": ""2025-12-01T10:00:00.000Z"" } }(Replace phone number and use a future UTC date/time)
Expected Response (JSON):
json{ ""data"": { ""createReminder"": { ""id"": 1, // Or next available ID ""message"": ""Test reminder from GraphQL Playground"", ""recipientPhoneNumber"": ""+15551234567"", ""scheduledAt"": ""2025-12-01T10:00:00.000Z"", ""status"": ""PENDING"", ""userId"": 1 // Or the logged-in user's ID } } } -
Get Reminders Query:
graphqlquery GetRemindersQuery { reminders { id message scheduledAt status } }Expected Response (JSON):
json{ ""data"": { ""reminders"": [ { ""id"": 1, ""message"": ""Test reminder from GraphQL Playground"", ""scheduledAt"": ""2025-12-01T10:00:00.000Z"", ""status"": ""PENDING"" } // ... other reminders for this user ] } } -
Test
updateReminderanddeleteRemindersimilarly, ensuring you use the ID of a reminder created by the logged-in user.
-
4. Integrating with Plivo
Now, let's connect our application to Plivo to actually send the SMS messages.
-
Obtain Plivo Credentials and Phone Number:
- Log in to your Plivo Console (dashboard).
- Navigate to Settings > API Credentials. Note your Auth ID and Auth Token.
- Navigate to Phone Numbers > Your Numbers. Ensure you have an SMS-enabled Plivo phone number. Note the Number. If you don't have one, purchase one that supports SMS in your desired region.
-
Store Credentials Securely: Add your Plivo credentials and sender number to the
.envfile. Do not commit this file.sh# .env DATABASE_URL=postgresql://postgres:your_postgres_password@localhost:5432/plivo_scheduler?schema=public # Plivo Credentials PLIVO_AUTH_ID=""YOUR_PLIVO_AUTH_ID"" # Replace with your actual Auth ID PLIVO_AUTH_TOKEN=""YOUR_PLIVO_AUTH_TOKEN"" # Replace with your actual Auth Token PLIVO_SENDER_NUMBER=""+1YOURPLIVONUMBER"" # Replace with your SMS-enabled Plivo number (E.164 format) # Optional Scheduler Config # CRON_SCHEDULE='*/1 * * * *' # Example: Run every minute # SCHEDULER_BATCH_SIZE=50 # Example: Process 50 reminders per run # DISABLE_SCHEDULER=false # Set to true to disable node-cron scheduler (e.g., for serverless) # CRON_TRIGGER_SECRET=""your-super-secret-value"" # Secret for securing serverless trigger function (Step 12)- Explanation: These environment variables will be used by our Plivo client setup.
PLIVO_SENDER_NUMBERmust be in E.164 format (e.g.,+15551234567). We also addedCRON_TRIGGER_SECRETfor later use in serverless deployments.
- Explanation: These environment variables will be used by our Plivo client setup.
-
Install Plivo Node.js SDK: Add the official Plivo SDK to the API workspace.
bashyarn workspace api add plivo -
Create Plivo Client Utility: Create a file (
api/src/lib/plivoClient.ts) to initialize and export the Plivo client instance.typescript// api/src/lib/plivoClient.ts import * as plivo from 'plivo' import { logger } from './logger' const authId = process.env.PLIVO_AUTH_ID const authToken = process.env.PLIVO_AUTH_TOKEN if (!authId || !authToken) { logger.error('Plivo Auth ID or Auth Token missing in environment variables.') // Optionally throw an error to prevent startup if Plivo is essential // throw new Error('Plivo credentials are not configured.'); } // Initialize Plivo client only if credentials exist export const plivoClient = authId && authToken ? new plivo.Client(authId, authToken) : null if (plivoClient) { logger.info('Plivo client initialized successfully.') } else { logger.warn('Plivo client not initialized due to missing credentials.') }
Frequently Asked Questions
What's RedwoodJS's dbAuth used for?
dbAuth provides a built-in authentication system integrated with a database. It simplifies user signup, login, logout, and session management within your RedwoodJS application.
How to schedule SMS reminders using RedwoodJS?
Use RedwoodJS's GraphQL API to create reminders with a scheduled time, which are then processed by a background job using node-cron or an external scheduler in production. The job queries for due reminders and sends them via the Plivo SMS API.
What is Plivo used for in this RedwoodJS app?
Plivo is a communications platform that handles sending the actual SMS messages. The RedwoodJS app interacts with Plivo's API to dispatch scheduled reminders as SMS.
Why does the RedwoodJS scheduler need UTC time?
The scheduler uses UTC to accurately compare the scheduledAt timestamp (stored in UTC in the database) with the server's current time, ensuring reminders are sent at the correct moment regardless of server location.
When should I use an external scheduler with RedwoodJS?
An external scheduler (like Vercel Cron Jobs or AWS EventBridge Scheduler) is strongly recommended for production serverless deployments because node-cron isn't suitable for ephemeral serverless functions.
Can I use a database other than PostgreSQL with RedwoodJS and Prisma?
Yes, Prisma supports various databases (MySQL, SQLite, etc.). Ensure the DATABASE_URL in your .env file is correctly configured for the chosen database.
How to set up a RedwoodJS project with Plivo?
Create a RedwoodJS app, set up a PostgreSQL database (e.g., using Docker), install necessary libraries (node-cron, plivo), configure environment variables (database, Plivo), and implement the reminder model and services.
What is the role of Prisma in the RedwoodJS reminder app?
Prisma is an ORM that simplifies database interactions. It's used to model the reminder data, manage database migrations, and provide type-safe access to the database within the RedwoodJS services.
How to create a reminder in the RedwoodJS Plivo app?
Use the createReminder GraphQL mutation, providing the message, recipient phone number, and scheduled time in UTC. The app will handle storing and processing the reminder.
What are the reminder statuses in the RedwoodJS app?
Reminders have three statuses: PENDING (waiting to be sent), SENT (successfully delivered), and FAILED (an error occurred during sending).
Why is node-cron not recommended for serverless RedwoodJS deployments?
node-cron depends on a continuously running process, which doesn't align with the ephemeral nature of serverless functions. Serverless environments require an external scheduler to trigger the reminder processing logic.
How to secure the RedwoodJS reminder API?
Use Redwood's requireAuth function and filter database queries by the current user's ID to ensure users can only access and manage their own reminders.
What is the purpose of the @@index in the Prisma schema?
The @@index attribute creates database indexes to optimize query performance. In the reminder schema, indexes on status and scheduledAt, and userId improve the efficiency of fetching due reminders and reminders by user.
How to run the RedwoodJS reminder app locally?
After setting up the project, run yarn rw dev to start the development server. The frontend will be accessible on port 8910, the API on 8911, and the GraphQL Playground on 8911/graphql.