This guide provides a comprehensive walkthrough for building a production-ready bulk SMS broadcasting feature within a RedwoodJS application using the Plivo messaging API. We'll cover everything from initial project setup to deployment and monitoring, enabling you to reliably send messages to multiple recipients with a single API request.
By the end of this tutorial, you will have a RedwoodJS application with a secure API endpoint capable of accepting a list of phone numbers and a message text, then using Plivo to dispatch these messages efficiently.
Project Overview and Goals
What We're Building:
We are building a backend service within a RedwoodJS application that exposes a GraphQL mutation. This mutation will accept an array of destination phone numbers and a message body. It will then interact with the Plivo API to send the message to all specified recipients simultaneously using Plivo's bulk messaging feature.
Problem Solved:
This solves the need to efficiently send the same SMS message to many recipients without iterating and making individual API calls for each number. This reduces latency, simplifies application logic, and leverages Plivo's infrastructure optimized for bulk delivery.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework built on React, GraphQL, and Prisma. Chosen for its integrated structure (API/web sides), opinionated defaults, and developer experience features (generators, cells, etc.).
- Plivo: A cloud communications platform providing SMS, Voice, and WhatsApp APIs. Chosen for its robust messaging capabilities, straightforward API, and specific support for bulk sending.
- Node.js: The runtime environment for RedwoodJS's API side.
- TypeScript: For type safety and improved developer experience.
- Prisma: As the ORM for database interactions (optional logging).
- GraphQL: As the API query language used by RedwoodJS.
System Architecture:
+-------------------+ +-------------------------+ +----------------------+ +------------+
| Web Frontend |----->| RedwoodJS GraphQL API |----->| RedwoodJS Plivo Service|----->| Plivo API |
| (React Component) | | (api/src/functions/...) | | (api/src/services/...) | | (REST) |
+-------------------+ +-------------------------+ +----------------------+ +------------+
| ^ |
| (Optional: Renders UI) | (Handles Request/Auth) | (Formats & Sends)
v | v
+-------------------+ | +--------------------+
| User Interaction |---------------------+ | (Optional) |
| (Input numbers & | | RedwoodJS Database |
| message) | | (Prisma/Postgres) |
+-------------------+ +--------------------+
^
| (Logs messages)
Prerequisites:
- Node.js (v18 or later recommended)
- Yarn (v1 or Classic)
- A Plivo Account (Sign up at https://www.plivo.com/)
- A Plivo Phone Number capable of sending SMS (Purchased or verified within your Plivo console)
- Basic understanding of RedwoodJS, GraphQL, and TypeScript.
Final Outcome:
A functional RedwoodJS backend service with a testable GraphQL endpoint for sending bulk SMS messages via Plivo.
1. Setting up the Project
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
-
Create RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./plivo-bulk-sender
Follow the prompts. We recommend choosing TypeScript.
-
Navigate to Project Directory:
cd plivo-bulk-sender
-
Install Plivo Node.js SDK: The Plivo SDK will interact with their API. Install it specifically in the
api
workspace:yarn workspace api add plivo @types/plivo-node
-
Configure Environment Variables: Sensitive credentials like API keys should never be hardcoded. RedwoodJS uses
.env
files.-
Create a
.env
file in the project root:touch .env
-
Add the following variables, replacing the placeholder values:
# .env PLIVO_AUTH_ID=""YOUR_PLIVO_AUTH_ID"" PLIVO_AUTH_TOKEN=""YOUR_PLIVO_AUTH_TOKEN"" PLIVO_SOURCE_NUMBER=""YOUR_PLIVO_PHONE_NUMBER"" # Must be a Plivo number enabled for SMS
-
How to find these values:
- Log in to your Plivo Console: https://console.plivo.com/
PLIVO_AUTH_ID
andPLIVO_AUTH_TOKEN
: Find these on the main dashboard page. See Plivo documentation for visual examples:- Example Plivo Auth ID/Token Location (Illustrative - UI might change)
PLIVO_SOURCE_NUMBER
: Navigate toMessaging
->Phone Numbers
->Your Numbers
. Copy a number that is SMS-enabled and suitable for sending broadcasts (check country regulations). See Plivo documentation for visual examples:- Example Plivo Phone Numbers Location (Illustrative - UI might change)
-
Add
.env
to.gitignore
: Ensure your.env
file (containing secrets) is listed in your root.gitignore
file. RedwoodJS usually adds this by default, but double-check.
-
Purpose of Configuration: Using environment variables keeps your credentials secure and separate from your codebase, making it easy to manage different credentials for development, staging, and production environments.
2. Implementing Core Functionality (Plivo Service)
RedwoodJS services encapsulate business logic on the API side. We'll create a service to handle interactions with the Plivo API.
-
Generate Messaging Service: Use the RedwoodJS CLI generator:
yarn rw g service messaging
This creates
api/src/services/messaging/messaging.ts
and related test files. -
Implement the Bulk Send Logic: Open
api/src/services/messaging/messaging.ts
and replace its contents with the following:// api/src/services/messaging/messaging.ts import { PlivoClient } from 'plivo-node' // Or import * as Plivo from 'plivo' depending on SDK version import type { MessageCreateResponse } from 'plivo-node/dist/resources/message' // Adjust import path as needed import { logger } from 'src/lib/logger' // Initialize Plivo client outside the function for potential reuse // Ensure environment variables are loaded (Redwood does this automatically) let plivoClient: PlivoClient | null = null if (process.env.PLIVO_AUTH_ID && process.env.PLIVO_AUTH_TOKEN) { plivoClient = new PlivoClient( process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN ) } else { logger.error( 'Plivo Auth ID or Auth Token missing in environment variables.' ) // Optionally throw an error during startup if Plivo is essential // throw new Error('Plivo credentials not configured.') } interface SendBulkSmsParams { destinations: string[] text: string } interface BulkSmsResponse { success: boolean message: string plivoResponse?: MessageCreateResponse // Include Plivo's response for details error?: string } /** * Sends a single SMS message to multiple destinations using Plivo's bulk API format. * * @param destinations - An array of E.164 formatted phone numbers. * @param text - The message body to send. * @returns Promise<BulkSmsResponse> - Indicates success or failure. */ export const sendBulkSms = async ({ destinations, text, }: SendBulkSmsParams): Promise<BulkSmsResponse> => { if (!plivoClient) { logger.error('Plivo client not initialized. Cannot send SMS.') return { success: false, message: 'Plivo client configuration error.', error: 'Plivo client not initialized.', } } if (!destinations || destinations.length === 0) { return { success: false, message: 'No destination numbers provided.' } } if (!text || text.trim() === '') { return { success: false, message: 'Message text cannot be empty.' } } if (!process.env.PLIVO_SOURCE_NUMBER) { logger.error('Plivo source number not configured.') return { success: false, message: 'Source phone number not configured.', error: 'PLIVO_SOURCE_NUMBER missing.', } } // **Crucial Step: Format destinations for Plivo Bulk API** // Plivo expects destination numbers concatenated with '<' const plivoDestinationString = destinations.join('<') // Note: Plivo may have limits on the length of the destination string // or the total number of recipients per API call. Consult Plivo documentation. // For very large lists_ consider splitting into smaller batches. logger.info( `Attempting to send bulk SMS via Plivo to ${destinations.length} numbers.` ) logger.debug(`Formatted destinations: ${plivoDestinationString.substring(0_ 50)}...`) // Log truncated for brevity try { const response: MessageCreateResponse = await plivoClient.messages.create( process.env.PLIVO_SOURCE_NUMBER_ // src plivoDestinationString_ // dst (bulk format) text // text // Optional parameters (e.g._ url for status callbacks) // { url: 'https://your-app.com/api/webhooks/plivo-status' } ) logger.info( `Plivo bulk message request successful. API Response Message: ${response.message}` ) // Note: This response indicates the API *request* was accepted. // Individual message statuses need to be checked via Plivo logs or webhooks. logger.debug(`Plivo response details: ${JSON.stringify(response)}`) return { success: true_ message: `Bulk message submitted to Plivo for ${destinations.length} recipients.`_ plivoResponse: response_ } } catch (error) { logger.error({ error }_ 'Plivo API call failed.') // Attempt to parse Plivo-specific errors if possible let errorMessage = 'Failed to send bulk message via Plivo.' if (error instanceof Error && error.message) { errorMessage += ` Error: ${error.message}` } // Plivo SDK might wrap errors differently_ inspect 'error' object structure // e.g._ if (error.response && error.response.data) { ... } return { success: false_ message: errorMessage_ error: error instanceof Error ? error.toString() : JSON.stringify(error)_ } } } // Optional: Add functions for fetching message status_ handling webhooks_ etc.
Explanation:
- Import Plivo SDK & Types: We import the necessary client and type definitions.
- Initialize Client: We create a single Plivo client instance using credentials from environment variables. Initializing it outside the function avoids recreating it on every call. Robust error handling checks if credentials exist.
sendBulkSms
Function:- Takes an object with
destinations
(array of strings) andtext
(string). - Includes guard clauses to check for client initialization_ empty destinations/text_ and missing source number.
- Formats Destinations: This is key. Plivo's bulk API requires destination numbers to be provided as a single string_ delimited by the
<
character (e.g._`""14155551212<14155551313<14155551414""`
). We usedestinations.join('<')
. - API Call: Uses
plivoClient.messages.create
with the source number_ the formatted destination string_ and the message text. - Logging: Uses Redwood's built-in
logger
for informational and error messages. - Error Handling: A
try...catch
block wraps the API call. If an error occurs_ it's logged_ and asuccess: false
response is returned. - Response: Returns a structured object indicating success/failure and includes the raw Plivo API response message for reference.
- Takes an object with
Design Pattern: This uses a simple Service Layer pattern_ isolating the Plivo interaction logic from the GraphQL resolver layer.
Alternative Approach: Instead of using the bulk dst
format_ you could loop through the destinations and make individual messages.create
calls (perhaps concurrently using Promise.all
). However_ this is less efficient_ potentially hits API rate limits faster_ and doesn't leverage Plivo's dedicated bulk feature. For true bulk sending_ the <
delimited string is the intended method.
3. Building the API Layer (GraphQL)
RedwoodJS uses GraphQL for its API. We need to define a mutation to expose our sendBulkSms
service function.
-
Define GraphQL Schema: Open or create
api/src/graphql/messaging.sdl.ts
and add the following:// api/src/graphql/messaging.sdl.ts export const schema = gql` """""" Response type for the sendBulkMessage mutation. """""" type BulkSmsResponse { success: Boolean! message: String! # Optional: Expose parts of Plivo's response if needed by the client # plivoMessageUUIDs: [String!] error: String } type Mutation { """""" Sends a bulk SMS message to multiple recipients via Plivo. Requires authentication. """""" sendBulkMessage(destinations: [String!]!_ text: String!): BulkSmsResponse! @requireAuth # Secure this endpoint - see Section 7 # Add roles if needed: @requireAuth(roles: [""ADMIN""_ ""EDITOR""]) } `
Explanation:
BulkSmsResponse
Type: Defines the structure of the data returned by the mutation. We keep it simple_ mirroring our service response.Mutation
Type: Defines thesendBulkMessage
mutation.- It accepts two arguments:
destinations
(an array of non-null strings) andtext
(a non-null string). GraphQL enforces these types. - It returns our
BulkSmsResponse
type (non-null). @requireAuth
: This is a RedwoodJS directive that ensures only authenticated users can call this mutation. We'll set up authentication later (Section 7)_ but it's crucial to include this now. You can add role-based access control too.
- It accepts two arguments:
-
Resolver Mapping (Automatic): RedwoodJS automatically maps the
sendBulkMessage
mutation defined in the SDL to thesendBulkSms
function exported fromapi/src/services/messaging/messaging.ts
. No explicit resolver code is needed inapi/src/functions/graphql.ts
for this mapping. -
Testing the Endpoint: Once your RedwoodJS app is running (
yarn rw dev
)_ you can test this mutation using a GraphQL client like Apollo Studio Sandbox (usually available athttp://localhost:8911/graphql
) orcurl
.Curl Example: (Requires authentication setup first - see Section 7. For initial testing without auth_ temporarily remove
@requireAuth
)curl 'http://localhost:8910/graphql' \ -H 'Content-Type: application/json' \ # -H 'Authorization: Bearer YOUR_AUTH_TOKEN' # Add after setting up auth --data-raw '{""query"":""mutation SendBulk($dest: [String!]!_ $msg: String!) {\n sendBulkMessage(destinations: $dest_ text: $msg) {\n success\n message\n error\n }\n}""_""variables"":{""dest"":[""14155551212""_ ""14155551313""]_ ""msg"":""Hello from RedwoodJS Bulk Sender!""}}' \ --compressed
Note: Complex JSON payloads with nested quotes in
--data-raw
can be tricky to escape correctly across different terminals. Using a dedicated GraphQL client or sending the JSON payload from a file (--data @payload.json
) is often more reliable.Expected JSON Request Body (for the
curl
example):{ ""query"": ""mutation SendBulk($dest: [String!]!_ $msg: String!) {\n sendBulkMessage(destinations: $dest_ text: $msg) {\n success\n message\n error\n }\n}""_ ""variables"": { ""dest"": [""14155551212""_ ""14155551313""]_ ""msg"": ""Hello from RedwoodJS Bulk Sender!"" } }
Example Success JSON Response:
{ ""data"": { ""sendBulkMessage"": { ""success"": true_ ""message"": ""Bulk message submitted to Plivo for 2 recipients.""_ ""error"": null } } }
Example Error JSON Response (e.g._ invalid number):
{ ""data"": { ""sendBulkMessage"": { ""success"": false_ ""message"": ""Failed to send bulk message via Plivo. Error: Invalid 'dst' parameter""_ // Example Plivo error ""error"": ""Error: Invalid 'dst' parameter"" } } }
4. Integrating with Plivo (Third-Party Service)
We've already done the core integration within the service file (messaging.ts
). This section reiterates the key integration points and configuration details.
-
Configuration (Recap):
- API Credentials:
PLIVO_AUTH_ID
andPLIVO_AUTH_TOKEN
are stored securely in.env
and accessed viaprocess.env
. - Source Number:
PLIVO_SOURCE_NUMBER
is stored in.env
and used as thesrc
parameter in the API call. - Obtaining Credentials: As detailed in Section 1_ credentials and numbers are managed through the Plivo Console (https://console.plivo.com/).
- API Credentials:
-
Secure Handling of Secrets:
.env
: Never commit this file to version control. Ensure it's in.gitignore
.- Deployment: When deploying (Section 12 - Note: Section 12 is not present in the provided text)_ use the hosting provider's mechanism for setting environment variables securely (e.g._ Vercel Environment Variables_ Netlify Build Environment Variables_ Render Secret Files). Do not include the
.env
file in your deployment bundle.
-
Plivo SDK Initialization: The
PlivoClient
is initialized once inmessaging.ts
using the environment variables. This is the primary point of interaction with the Plivo service. -
Fallback Mechanisms (Considerations):
- API Outages: The current implementation has basic
try...catch
. For production_ if Plivo is temporarily unavailable_ you might want a more robust retry mechanism (Section 5) or queueing system (like BullMQ with Redis) to attempt sending later. - Alternative Providers: For critical systems_ you might consider integrating a secondary SMS provider as a fallback_ but this adds significant complexity.
- API Outages: The current implementation has basic
5. Error Handling_ Logging_ and Retry Mechanisms
Robust applications need solid error handling and logging.
-
Error Handling Strategy:
- Service Level: The
sendBulkSms
function usestry...catch
to capture errors during the Plivo API call. It logs the error and returns a structuredBulkSmsResponse
withsuccess: false
and an error message/detail. - GraphQL Level: Errors thrown within the service (if not caught) or during GraphQL processing will propagate to the client according to GraphQL error specifications. The current setup catches errors within the service_ providing a controlled failure response via the
BulkSmsResponse
type. - Plivo Errors: Plivo API errors (e.g._ invalid number_ authentication failure_ insufficient funds) are caught in the
catch
block. Parsing theerror
object (which might vary slightly based on the SDK version) can provide more specific feedback. Refer to Plivo API documentation for error codes.
- Service Level: The
-
Logging:
- Redwood Logger: We use Redwood's built-in Pino logger (
src/lib/logger.ts
).logger.info
: Used for successful operations or key events.logger.error
: Used for failures_ catching exceptions.logger.debug
: Used for detailed information (like API responses) helpful during development/troubleshooting. Can be disabled in production via log level configuration.logger.warn
: Used for potential issues (e.g._ invalid phone number formats before validation).
- Log Levels: Configure log levels per environment in
api/src/lib/logger.ts
or via environment variables (e.g._LOG_LEVEL=info
for production_LOG_LEVEL=debug
for development). - Log Analysis: In production_ forward logs to a dedicated logging service (e.g._ Datadog_ Logtail_ Papertrail) for aggregation_ searching_ and alerting.
- Redwood Logger: We use Redwood's built-in Pino logger (
-
Retry Mechanisms:
- Simple Retries: For transient network errors_ you could implement a simple retry loop within the
catch
block (e.g._ retry 2-3 times with a short delay). - Exponential Backoff: A better approach uses exponential backoff (wait 1s_ then 2s_ then 4s...) to avoid overwhelming the API during an outage. Libraries like
async-retry
can simplify this. - Background Jobs (Recommended for Scale): For true robustness_ especially with large broadcasts or potential Plivo delays_ decouple the sending process using background jobs.
- The GraphQL mutation would enqueue a job (e.g._ using RedwoodJS Jobs with
dbAuth
or a library like BullMQ + Redis). - A separate worker process would pick up the job_ attempt to send the SMS via the service_ and handle retries/failures independently of the initial API request. This prevents timeouts in the GraphQL request and ensures messages are eventually sent even if there are temporary issues. Implementing background jobs is beyond the scope of this initial guide but is a crucial next step for production scale.
- The GraphQL mutation would enqueue a job (e.g._ using RedwoodJS Jobs with
Example (Conceptual Retry Logic within
catch
- Conceptual example only):// Inside the catch block of sendBulkSms } catch (error) { logger.error({ error }_ 'Plivo API call failed.'); let attempt = 0; const maxAttempts = 3; let lastError = error; // Conceptual example only - requires proper error checking (e.g._ only retry on specific network/server errors)_ // potentially a dedicated retry library_ and careful consideration for production use. while (attempt < maxAttempts) { attempt++; // Determine if error is retryable (e.g._ check error code/type) const isRetryableError = (lastError instanceof Error && lastError.message.includes('Network error')); // Simplified check if (!isRetryableError) { break; // Don't retry non-retryable errors } const delayMs = Math.pow(2_ attempt) * 1000; // Exponential backoff logger.warn(`Retrying Plivo API call (attempt ${attempt}/${maxAttempts}) after ${delayMs}ms...`); await new Promise(resolve => setTimeout(resolve_ delayMs)); try { // Re-attempt the Plivo call here... const retryResponse = await plivoClient.messages.create(/*...*/); logger.info(`Retry attempt ${attempt} successful.`); // Remember to handle DB updates if logging is enabled return { success: true_ /*...*/ plivoResponse: retryResponse }; } catch (retryError) { logger.error({ error: retryError }_ `Retry attempt ${attempt} failed.`); lastError = retryError; } } // If all retries fail or error was not retryable: // Remember to handle DB updates if logging is enabled const errorMessage = lastError instanceof Error ? lastError.toString() : JSON.stringify(lastError) return { success: false_ message: `Failed to send bulk message via Plivo after ${attempt} attempts. Last Error: ${lastError instanceof Error ? lastError.message : 'Unknown error'}`_ error: errorMessage }; }
- Simple Retries: For transient network errors_ you could implement a simple retry loop within the
6. Database Schema and Data Layer (Optional Logging)
While not strictly required for sending_ logging message attempts or storing recipient lists can be useful. We'll add a simple log.
-
Define Prisma Schema: Open
api/db/schema.prisma
and add a model to log bulk message jobs:// api/db/schema.prisma datasource db { provider = ""postgresql"" // Or sqlite_ mysql url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" // Add other targets if needed e.g._ ""rhel-openssl-1.0.x"" } // Add your existing models (e.g._ User if using auth) model BulkMessageJob { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt status String // e.g._ ""PENDING""_ ""SUBMITTED""_ ""FAILED""_ ""PARTIAL_SUCCESS"" (more complex) messageText String recipientCount Int plivoMessageId String? // The MessageUUID from Plivo's response (identifies the batch request) errorMessage String? // Optional: Store destinations (consider length/privacy implications) // destinations String[] // Might exceed DB limits_ consider separate table or storing hash @@index([status_ createdAt]) // Index for querying jobs by status/time }
-
Apply Migrations: Generate and apply the database migration:
yarn rw prisma migrate dev --name add_bulk_message_job
Follow the prompts to create the migration file and apply it to your development database.
-
Update Service to Log: Modify
api/src/services/messaging/messaging.ts
to interact with the database.// api/src/services/messaging/messaging.ts import { db } from 'src/lib/db' // Import Redwood's Prisma client // ... other imports import { PlivoClient } from 'plivo-node' import type { MessageCreateResponse } from 'plivo-node/dist/resources/message' import { logger } from 'src/lib/logger' // ... (Plivo client initialization) interface SendBulkSmsParams { destinations: string[] text: string } interface BulkSmsResponse { success: boolean message: string plivoResponse?: MessageCreateResponse error?: string } export const sendBulkSms = async ({ destinations_ text_ }: SendBulkSmsParams): Promise<BulkSmsResponse> => { // ... (initial checks for client, destinations, text, source number) const plivoDestinationString = destinations.join('<') // ... (note about potential length limits) let jobRecordId: string | null = null try { // Log initial attempt to DB const jobRecord = await db.bulkMessageJob.create({ data: { status: 'PENDING', // Start as pending messageText: text, recipientCount: destinations.length, }, select: { id: true }, // Only select the ID we need }) jobRecordId = jobRecord.id logger.info( `Attempting to send bulk SMS (Job ID: ${jobRecordId}) via Plivo to ${destinations.length} numbers.` ) // Ensure plivoClient is initialized before use (already checked above, but good practice) if (!plivoClient || !process.env.PLIVO_SOURCE_NUMBER) { throw new Error('Plivo client or source number not configured.'); } const response: MessageCreateResponse = await plivoClient.messages.create( process.env.PLIVO_SOURCE_NUMBER, plivoDestinationString, text ) logger.info( `Plivo bulk message request successful (Job ID: ${jobRecordId}). API Response Message: ${response.message}` ) // Update DB record on success await db.bulkMessageJob.update({ where: { id: jobRecordId }, data: { status: 'SUBMITTED', plivoMessageId: response.messageUuid?.[0], // Plivo returns array, often just one UUID for the batch updatedAt: new Date(), }, }) return { success: true, message: `Bulk message submitted to Plivo for ${destinations.length} recipients.`, plivoResponse: response, } } catch (error) { logger.error({ error, jobRecordId }, 'Plivo API call failed.') const errorMessage = error instanceof Error ? error.toString() : JSON.stringify(error) // Update DB record on failure if (jobRecordId) { try { await db.bulkMessageJob.update({ where: { id: jobRecordId }, data: { status: 'FAILED', errorMessage: errorMessage.substring(0, 1000), // Truncate if needed updatedAt: new Date(), }, }) } catch (dbError) { logger.error({ error: dbError, originalJobId: jobRecordId }, 'Failed to update BulkMessageJob status to FAILED.') } } return { success: false, message: `Failed to send bulk message via Plivo. Error: ${ error instanceof Error ? error.message : 'Unknown error' }`, error: errorMessage, } } }
Explanation:
- We import Redwood's pre-configured Prisma client (
db
). - Before calling Plivo, we create a
BulkMessageJob
record withstatus: 'PENDING'
. - If the Plivo call succeeds, we update the record to
status: 'SUBMITTED'
and store themessageUuid
returned by Plivo (useful for correlating logs). - If the call fails, we update the record to
status: 'FAILED'
and store the error message. Added a nested try-catch for the DB update on failure to prevent masking the original Plivo error if the DB update itself fails. - This provides a basic audit trail within your application's database.
Performance/Scale: For very high volume, writing to the DB on every request can become a bottleneck. Consider batching log writes or logging asynchronously (e.g., via background jobs). Ensure relevant columns (status
, createdAt
) are indexed (added @@index
).
7. Adding Security Features
Securing your API endpoint is critical, especially when it triggers external actions and potentially incurs costs.
-
Authentication:
- We already added
@requireAuth
to the GraphQL mutation definition (messaging.sdl.ts
). This is the first line of defense. - Setup Auth: You need to configure an authentication provider in RedwoodJS. Common choices include
dbAuth
(built-in, username/password stored in your DB), or third-party providers like Auth0, Netlify Identity, Firebase Auth, etc. - Follow the RedwoodJS Authentication documentation: https://redwoodjs.com/docs/auth/introduction
- Run the auth setup command, e.g., for
dbAuth
:yarn rw setup auth dbAuth
- This will scaffold login pages, session handling, and make
@requireAuth
functional. Users will need to log in to obtain a session/token to call the protected mutation.
- We already added
-
Authorization (Role-Based Access Control):
- If only certain user roles (e.g., 'admin', 'manager') should be allowed to send bulk messages, modify the directive:
// api/src/graphql/messaging.sdl.ts type Mutation { sendBulkMessage(destinations: [String!]!, text: String!): BulkSmsResponse! @requireAuth(roles: [""admin"", ""manager""]) // Only users with these roles }
- You'll need to implement role management within your user model and authentication setup. Redwood's
dbAuth
supports roles.
- If only certain user roles (e.g., 'admin', 'manager') should be allowed to send bulk messages, modify the directive:
-
Input Validation and Sanitization:
- GraphQL Types: The schema enforces that
destinations
is an array of strings andtext
is a string.[String!]!
means the array itself cannot be null, and its elements cannot be null. - Phone Number Format (E.164): Plivo generally expects numbers in E.164 format (e.g.,
+14155551212
). While Plivo might handle some variations, it's best practice to validate/normalize numbers before sending them to the service. You can add validation logic in the service or preferably before calling the mutation (e.g., on the frontend or in a validation layer). Libraries likelibphonenumber-js
can help. - Text Length: Validate message length against SMS standards (160 GSM-7 characters, 70 UCS-2) or Plivo limits if necessary. Add checks in the service.
- Sanitization: While SMS content is less susceptible to XSS than HTML, sanitize input if the message text originates from potentially untrusted user input elsewhere in your system.
- GraphQL Types: The schema enforces that