code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

How to Build Bulk SMS Broadcasting with Plivo and RedwoodJS (2025 Guide)

Complete guide to implementing bulk SMS broadcasting in RedwoodJS using Plivo's SMS API. Learn GraphQL setup, batch processing, authentication, webhook integration, and production deployment with working code examples.

Build Bulk SMS Broadcasting with Plivo and RedwoodJS: Complete Implementation Guide

This comprehensive tutorial shows you how to implement a production-ready bulk SMS broadcasting system using RedwoodJS and Plivo's SMS API. Whether you're building emergency alert systems, appointment reminders, promotional campaigns, or time-sensitive notifications, this guide covers everything you need to send SMS messages to hundreds or thousands of recipients simultaneously.

You'll learn how to build a secure GraphQL API that processes bulk SMS requests, implement Plivo's bulk messaging API with proper batching (1,000 recipients per request), add authentication and error handling, track delivery status, and deploy to production. This implementation follows RedwoodJS best practices for services, resolvers, and full-stack development.

Prerequisites and RedwoodJS Project Setup for Bulk SMS

Prerequisites:

  • Node.js v22 LTS installed (Active LTS until October 2025)
  • Basic familiarity with RedwoodJS, GraphQL, and TypeScript
  • Plivo account with active credits (sign up at plivo.com)
  • Plivo phone number enabled for SMS (costs vary by country; US numbers typically $1–$3/month)
  • Understanding of E.164 phone number format

System requirements: 4 GB RAM minimum, 2 GB free disk space for Node modules and build artifacts.

Plivo pricing consideration: US SMS messages cost approximately $0.0075–$0.01 per message. Test your implementation with small batches before scaling to thousands of recipients.

Create a new RedwoodJS project and install dependencies.

  1. Create RedwoodJS App:

    bash
    yarn create redwood-app ./plivo-bulk-sender

    Choose TypeScript when prompted for better type safety.

  2. Navigate to Project Directory:

    bash
    cd plivo-bulk-sender
  3. Install Plivo Node.js SDK: Install Plivo in the api workspace only (it's server-side code):

    bash
    yarn workspace api add plivo @types/plivo-node

    Troubleshooting: If installation fails, verify you're in the project root directory and have Node.js v22 LTS installed. Check your network connection – the Plivo SDK downloads native dependencies.

    Version pinning: For production, pin specific versions in api/package.json (e.g., "plivo": "4.74.0") to ensure consistent builds across environments.

  4. Configure Environment Variables: Never hardcode credentials. RedwoodJS uses .env files for secure configuration.

    Create a .env file in the project root:

    bash
    touch .env

    Add these variables with your actual Plivo values:

    dotenv
    # .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

    Find your Plivo credentials:

    • Log in to Plivo Console
    • Auth ID & Token: Found on the main dashboard page
    • Phone Number: Navigate to Messaging → Phone Numbers → Your Numbers. Copy an SMS-enabled number suitable for your target country's regulations.

    Verify .env is in .gitignore: RedwoodJS adds this by default, but confirm your .env file is listed to prevent committing secrets.

    Validate configuration: After setting variables, restart your dev server and check logs for Plivo initialization messages. If credentials are invalid, you'll see error messages when attempting to send.

    Team/CI environments: For team development, use environment-specific .env.development and .env.test files (committed with placeholder values). For CI/CD, configure secrets through your platform's environment variable settings (GitHub Actions secrets, GitLab CI/CD variables, etc.).

How to Implement the Plivo SMS Service in RedwoodJS

RedwoodJS services encapsulate server-side business logic. Create a service to handle Plivo API interactions.

  1. Generate Messaging Service:

    bash
    yarn rw g service messaging

    This creates api/src/services/messaging/messaging.ts and test files.

  2. Implement the Bulk Send Logic: Replace the contents of api/src/services/messaging/messaging.ts with this implementation:

    typescript
    // api/src/services/messaging/messaging.ts
    import { PlivoClient } from 'plivo-node'
    import type { MessageCreateResponse } from 'plivo-node/dist/resources/message'
    import { logger } from 'src/lib/logger'
    
    // Initialize Plivo client once at module load for connection reuse
    // Connection reuse improves performance by avoiding TLS handshake overhead on each request
    // RedwoodJS automatically loads environment variables from .env
    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.'
      )
      // For production, 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
      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: Format destinations for Plivo's bulk API**
      // Plivo requires destinations as a '<'-delimited string (e.g., '+14155551212<+14155551313')
      const plivoDestinationString = destinations.join('<')
    
      // Plivo enforces a 1,000 recipient limit per API request
      // Reference: https://www.plivo.com/docs/messaging/api/message/bulk-messaging
      if (destinations.length > 1000) {
        logger.warn(
          `Destination count (${destinations.length}) exceeds Plivo's 1,000 recipient limit. Messages may fail – split into batches of 1,000.`
        )
      }
    
      logger.info(
        `Attempting to send bulk SMS via Plivo to ${destinations.length} numbers.`
      )
      logger.debug(`Formatted destinations: ${plivoDestinationString.substring(0, 50)}`)
    
      try {
        const response: MessageCreateResponse = await plivoClient.messages.create(
          process.env.PLIVO_SOURCE_NUMBER, // src: Your Plivo sender number
          plivoDestinationString,         // dst: '<'-delimited recipient string
          text                            // text: Message body
          // Optional: Add status callback URL for delivery receipts
          // { url: 'https://your-app.com/api/webhooks/plivo-status' }
        )
    
        logger.info(
          `Plivo bulk message request successful. API Response: ${response.message}`
        )
        // This response confirms the API request was accepted, not that all messages delivered
        // Check individual message statuses via Plivo dashboard or delivery webhook callbacks
        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.')
    
        // Parse Plivo-specific error responses for detailed feedback
        let errorMessage = 'Failed to send bulk message via Plivo.'
        if (error instanceof Error && error.message) {
          errorMessage += ` Error: ${error.message}`
        }
    
        return {
          success: false,
          message: errorMessage,
          error: error instanceof Error ? error.toString() : JSON.stringify(error),
        }
      }
    }

Key implementation details:

  1. Import Plivo SDK & Types: Import the necessary client and type definitions for type safety.
  2. Initialize Client: Create a single Plivo client instance using environment variables. Initializing outside the function enables connection reuse, improving performance by avoiding TLS handshake overhead on every call.
  3. sendBulkSms Function:
    • Validates client initialization, destination numbers, message text, and source number
    • Formats Destinations: Plivo's bulk API requires destinations as a '<'-delimited string (e.g., '+14155551212<+14155551313<+14155551414'). Use destinations.join('<').
    • API Call: Calls plivoClient.messages.create with source number, formatted destination string, and message text
    • Logging: Uses RedwoodJS's built-in Pino logger for operational visibility
    • Error Handling: try...catch block captures API errors and returns structured responses
    • Response: Returns structured object indicating success/failure with raw Plivo response

Design Pattern: This Service Layer pattern isolates Plivo interaction logic from GraphQL resolvers, improving testability and maintainability.

Rate limiting consideration: Plivo enforces account-level rate limits (typically 200–1,000 messages/second depending on your plan). Messages exceeding your rate limit are queued automatically by Plivo, but expect delays for large batches.

Alternative approach: You could loop through destinations and make individual messages.create calls (perhaps using Promise.all). However, this approach is inefficient, hits API rate limits faster, and doesn't leverage Plivo's bulk optimization. The '<'-delimited string is Plivo's recommended method for bulk sending.

How to Create the GraphQL API for Bulk SMS Broadcasting

RedwoodJS uses GraphQL for its API. Define a mutation to expose the sendBulkSms service function.

  1. Define GraphQL Schema: Open or create api/src/graphql/messaging.sdl.ts and add:

    graphql
    // api/src/graphql/messaging.sdl.ts
    
    export const schema = gql`
      """
      Response type for the sendBulkMessage mutation.
      """
      type BulkSmsResponse {
        success: Boolean!
        message: String!
        # Optional: Expose Plivo response details 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
          # Add roles if needed: @requireAuth(roles: ["ADMIN", "EDITOR"])
      }
    `

    Schema details:

    • BulkSmsResponse Type: Defines the mutation response structure. GraphQL's type system provides automatic validation and clear client contracts.
    • Mutation Type: Defines sendBulkMessage mutation with:
      • destinations argument: Array of non-null strings (enforced by GraphQL)
      • text argument: Non-null string
      • Returns non-null BulkSmsResponse
      • @requireAuth: RedwoodJS directive ensuring only authenticated users call this mutation. Add role-based access control if needed.
  2. Resolver Mapping (Automatic): RedwoodJS automatically maps the sendBulkMessage mutation to the sendBulkSms function in api/src/services/messaging/messaging.ts. No explicit resolver code needed.

  3. Testing the Endpoint: Start your RedwoodJS app:

    bash
    yarn rw dev

    Test using GraphQL Playground at http://localhost:8911/graphql or curl.

    GraphQL Playground test (recommended): Navigate to http://localhost:8911/graphql and use this mutation:

    graphql
    mutation SendBulk($dest: [String!]!, $msg: String!) {
      sendBulkMessage(destinations: $dest, text: $msg) {
        success
        message
        error
      }
    }

    Variables:

    json
    {
      "dest": ["+14155551212", "+14155551313"],
      "msg": "Hello from RedwoodJS Bulk Sender!"
    }

    Curl Example: (Requires authentication setup first. For initial testing without auth, temporarily remove @requireAuth from schema)

    bash
    curl 'http://localhost:8910/graphql' \
      -H 'Content-Type: application/json' \
      --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!"}}' \
      --compressed

    Expected Success Response:

    json
    {
      "data": {
        "sendBulkMessage": {
          "success": true,
          "message": "Bulk message submitted to Plivo for 2 recipients.",
          "error": null
        }
      }
    }

    Expected Error Response (e.g., invalid number):

    json
    {
      "data": {
        "sendBulkMessage": {
          "success": false,
          "message": "Failed to send bulk message via Plivo. Error: Invalid 'dst' parameter",
          "error": "Error: Invalid 'dst' parameter"
        }
      }
    }

Plivo SMS API Integration Best Practices for RedwoodJS

The core integration is complete in messaging.ts. This section covers additional integration considerations.

Configuration Summary

  • API Credentials: PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN stored in .env, accessed via process.env
  • Source Number: PLIVO_SOURCE_NUMBER stored in .env, used as sender number
  • Credential Management: Manage through Plivo Console

Secure Secrets Management

Development:

  • Never commit .env to version control – verify it's in .gitignore
  • Use .env.example with placeholder values for team collaboration

Production/Deployment:

  • Use your hosting provider's environment variable configuration:
    • Vercel: Environment Variables section
    • Netlify: Build Environment Variables
    • Render: Secret Files or Environment Variables
    • AWS: Systems Manager Parameter Store or Secrets Manager
  • Never include .env in deployment bundles

Webhook Integration for Delivery Receipts

To track individual message delivery status, configure Plivo webhooks:

  1. Add a webhook URL to your message creation call:

    typescript
    const response = await plivoClient.messages.create(
      process.env.PLIVO_SOURCE_NUMBER,
      plivoDestinationString,
      text,
      { url: 'https://your-app.com/api/webhooks/plivo-status' }
    )
  2. Create a webhook handler in RedwoodJS:

    typescript
    // api/src/functions/plivoWebhook/plivoWebhook.ts
    export const handler = async (event) => {
      const body = JSON.parse(event.body)
      // Process delivery status: body.Status, body.MessageUUID, etc.
      logger.info(`Message ${body.MessageUUID} status: ${body.Status}`)
      return { statusCode: 200, body: 'OK' }
    }
  3. Configure the webhook URL in Plivo Console under Messaging → Your Application → Message URL

Fallback Mechanisms

ScenarioSolutionComplexity
Temporary Plivo outageImplement retry with exponential backoff (covered in next section)Medium
Extended Plivo outageMessage queue system (BullMQ + Redis) for async processingHigh
Critical system requiring 99.99% uptimeSecondary SMS provider with automatic failoverVery High

For most applications, retry logic with exponential backoff provides sufficient resilience without added complexity.

Error Handling and Retry Strategies for Bulk SMS Delivery

Robust error handling and logging are essential for production systems.

Error Handling Strategy

Service Level:

  • try...catch blocks capture Plivo API errors
  • Returns structured BulkSmsResponse with success: false and error details
  • Logs all errors with context for debugging

GraphQL Level:

  • Service-level error handling provides controlled failure responses
  • GraphQL errors (if service throws) propagate per GraphQL specification
  • Client receives consistent response format

Common Plivo Error Scenarios:

Error TypeCauseSolution
Authentication failedInvalid AUTH_ID or AUTH_TOKENVerify credentials in Plivo Console
Invalid 'dst' parameterMalformed phone number or non-E.164 formatValidate numbers with libphonenumber-js
Insufficient balanceAccount out of creditsAdd credits in Plivo Console
Rate limit exceededToo many requests per secondImplement request throttling or increase rate limit
Invalid 'src' parameterSource number not owned or not SMS-enabledVerify number in Plivo Console under Phone Numbers

Logging Configuration

Log Levels:

  • logger.info: Successful operations, key events (message submitted)
  • logger.error: Failures, exceptions (API errors)
  • logger.debug: Detailed information (API responses, formatted numbers)
  • logger.warn: Potential issues (recipient count exceeding limits)

Configure per environment:

typescript
// api/src/lib/logger.ts
export const logger = createLogger({
  options: {
    level: process.env.LOG_LEVEL || 'info', // 'debug' for dev, 'info' for prod
  },
})

Production log forwarding: Forward logs to aggregation services for analysis and alerting:

  • Datadog: Use pino-datadog transport
  • Logtail: Use Logtail's Pino integration
  • Papertrail: Use pino-papertrail

Example configuration:

bash
# .env (production)
LOG_LEVEL=info
LOGTAIL_SOURCE_TOKEN=your_logtail_token

Retry Mechanisms

Simple Retry with Exponential Backoff:

For transient network errors, implement retry logic:

typescript
// Example implementation (add to sendBulkSms function)
const maxAttempts = 3
let attempt = 0
let lastError: Error | null = null

while (attempt < maxAttempts) {
  attempt++
  try {
    const response = await plivoClient.messages.create(...)
    logger.info(`Message sent successfully on attempt ${attempt}`)
    return { success: true, message: '...', plivoResponse: response }
  } catch (error) {
    lastError = error as Error

    // Only retry on network/server errors (5xx), not client errors (4xx)
    const isRetryable = error.message?.includes('ECONNRESET') ||
                       error.message?.includes('ETIMEDOUT')

    if (!isRetryable || attempt >= maxAttempts) {
      break
    }

    const delayMs = Math.pow(2, attempt) * 1000 // 2s, 4s, 8s...
    logger.warn(`Retrying Plivo API call (attempt ${attempt}/${maxAttempts}) after ${delayMs}ms`)
    await new Promise(resolve => setTimeout(resolve, delayMs))
  }
}

return {
  success: false,
  message: `Failed after ${attempt} attempts: ${lastError?.message}`,
  error: lastError?.toString()
}

Background Jobs (Recommended for Scale):

For production systems handling large volumes, decouple SMS sending using background jobs:

Benefits:

  • Prevents GraphQL request timeouts
  • Enables automatic retries independent of user requests
  • Processes large batches without blocking
  • Provides job status tracking

Implementation options:

SolutionUse CaseSetup Complexity
RedwoodJS Jobs (experimental)Simple use cases, integrated with RedwoodJSLow
BullMQ + RedisProduction-scale, robust retry/failure handlingMedium
AWS SQS + LambdaAWS deployments, serverless architectureHigh

BullMQ example structure:

typescript
// api/src/jobs/sendBulkSms.ts
import { Queue, Worker } from 'bullmq'

const smsQueue = new Queue('bulk-sms', { connection: redisConnection })

// Enqueue job from GraphQL mutation
export const enqueueBulkSms = async (destinations: string[], text: string) => {
  await smsQueue.add('send', { destinations, text }, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 }
  })
}

// Worker processes jobs
const worker = new Worker('bulk-sms', async (job) => {
  const { destinations, text } = job.data
  return await sendBulkSms({ destinations, text })
}, { connection: redisConnection })

How to Track SMS Delivery with Database Logging (Optional)

Database logging provides an audit trail for compliance, cost tracking, and debugging.

Privacy and Compliance Considerations

GDPR/Privacy compliance:

  • Store only necessary data (recipient count, message status – not full phone numbers unless required)
  • Implement data retention policies (auto-delete records after 90 days)
  • Include privacy notice in your terms of service
  • Provide user data export/deletion capabilities

Retention policy example:

typescript
// api/src/services/cleanup/cleanup.ts
export const cleanupOldMessageJobs = async () => {
  const ninetyDaysAgo = new Date()
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)

  const deleted = await db.bulkMessageJob.deleteMany({
    where: { createdAt: { lt: ninetyDaysAgo } }
  })

  logger.info(`Deleted ${deleted.count} message job records older than 90 days`)
}

Define Prisma Schema

Open api/db/schema.prisma and add a model for logging:

prisma
// api/db/schema.prisma

datasource db {
  provider = "postgresql" // Or sqlite, mysql
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = "native"
}

model BulkMessageJob {
  id             String   @id @default(cuid())
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
  status         String   // "PENDING", "SUBMITTED", "FAILED"
  messageText    String
  recipientCount Int
  plivoMessageId String?  // MessageUUID from Plivo (correlates with logs)
  errorMessage   String?

  @@index([status, createdAt]) // Query optimization for status filters
  @@index([createdAt]) // Supports retention cleanup queries
}

Index strategy:

  • [status, createdAt]: Optimizes queries filtering by status with date ranges
  • [createdAt]: Supports efficient retention policy cleanup

Apply Migrations

Generate and apply the migration:

bash
yarn rw prisma migrate dev --name add_bulk_message_job

Update Service to Log

Modify api/src/services/messaging/messaging.ts to write database logs:

typescript
// api/src/services/messaging/messaging.ts
import { db } from 'src/lib/db' // RedwoodJS Prisma client
import { PlivoClient } from 'plivo-node'
import type { MessageCreateResponse } from 'plivo-node/dist/resources/message'
import { logger } from 'src/lib/logger'

// ... (Plivo client initialization)

export const sendBulkSms = async ({
  destinations,
  text,
}: SendBulkSmsParams): Promise<BulkSmsResponse> => {
  // ... (validation checks)

  const plivoDestinationString = destinations.join('<')

  let jobRecordId: string | null = null
  try {
    // Create pending job record
    const jobRecord = await db.bulkMessageJob.create({
      data: {
        status: 'PENDING',
        messageText: text,
        recipientCount: destinations.length,
      },
      select: { id: true },
    })
    jobRecordId = jobRecord.id

    logger.info(
      `Attempting bulk SMS (Job ID: ${jobRecordId}) to ${destinations.length} recipients`
    )

    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(
      `Bulk SMS successful (Job ID: ${jobRecordId}). API Response: ${response.message}`
    )

    // Update job record on success
    await db.bulkMessageJob.update({
      where: { id: jobRecordId },
      data: {
        status: 'SUBMITTED',
        plivoMessageId: response.messageUuid?.[0],
        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 job record on failure
    if (jobRecordId) {
      try {
        await db.bulkMessageJob.update({
          where: { id: jobRecordId },
          data: {
            status: 'FAILED',
            errorMessage: errorMessage.substring(0, 1000), // Truncate long errors
            updatedAt: new Date(),
          },
        })
      } catch (dbError) {
        logger.error(
          { error: dbError, originalJobId: jobRecordId },
          'Failed to update job status to FAILED.'
        )
      }
    }

    return {
      success: false,
      message: `Failed to send bulk message via Plivo. Error: ${
        error instanceof Error ? error.message : 'Unknown error'
      }`,
      error: errorMessage,
    }
  }
}

Performance considerations:

  • For high volume (>100 messages/second), database writes become bottlenecks
  • Consider async logging via background jobs
  • Batch multiple log writes using Prisma transactions
  • Monitor database connection pool utilization

How to Secure Your Bulk SMS API with RedwoodJS Authentication

Security is critical – bulk SMS endpoints can incur costs and enable abuse if exposed.

Authentication Setup

The @requireAuth directive in your GraphQL schema provides the first security layer. Configure authentication:

Setup dbAuth (self-hosted):

bash
yarn rw setup auth dbAuth

This scaffolds:

  • Login/signup pages
  • Session handling with secure HTTP-only cookies
  • Password hashing with bcrypt
  • User model in Prisma schema

Alternative providers:

  • Auth0: Enterprise-grade authentication
  • Netlify Identity: Simple integration for Netlify deployments
  • Firebase Auth: Google-backed authentication with social providers
  • Clerk: Modern authentication with React components

Follow RedwoodJS Authentication docs for setup instructions.

Verify authentication works:

  1. Start dev server: yarn rw dev
  2. Navigate to /login (scaffolded by dbAuth)
  3. Create a test account
  4. Attempt to call sendBulkMessage mutation – should succeed when authenticated, fail when logged out

Authorization (Role-Based Access Control)

Restrict bulk messaging to specific user roles:

graphql
// api/src/graphql/messaging.sdl.ts
type Mutation {
  sendBulkMessage(destinations: [String!]!, text: String!): BulkSmsResponse!
    @requireAuth(roles: ["admin", "manager"])
}

Implement roles in User model:

prisma
// api/db/schema.prisma
model User {
  id       String   @id @default(cuid())
  email    String   @unique
  hashedPassword String
  salt     String
  roles    String   @default("user") // "user", "admin", "manager"
  // ... other fields
}

Rate Limiting

Prevent abuse by implementing rate limiting:

Install rate limiting library:

bash
yarn workspace api add express-rate-limit

Configure rate limiter:

typescript
// api/src/functions/graphql.ts
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later.',
})

// Apply to GraphQL endpoint
export const handler = createGraphQLHandler({
  // ... other config
  extraPlugins: [limiter],
})

Input Validation and Sanitization

GraphQL type enforcement: The schema enforces destinations as non-null string array and text as non-null string.

Phone number validation with libphonenumber-js:

Install library:

bash
yarn workspace api add libphonenumber-js

Add validation to service:

typescript
import { parsePhoneNumber } from 'libphonenumber-js'

export const sendBulkSms = async ({
  destinations,
  text,
}: SendBulkSmsParams): Promise<BulkSmsResponse> => {
  // Validate and normalize phone numbers
  const validatedNumbers: string[] = []
  const invalidNumbers: string[] = []

  for (const number of destinations) {
    try {
      const parsed = parsePhoneNumber(number)
      if (parsed && parsed.isValid()) {
        validatedNumbers.push(parsed.format('E.164'))
      } else {
        invalidNumbers.push(number)
      }
    } catch {
      invalidNumbers.push(number)
    }
  }

  if (invalidNumbers.length > 0) {
    return {
      success: false,
      message: `Invalid phone numbers: ${invalidNumbers.join(', ')}`,
    }
  }

  // Continue with validatedNumbers...
}

Message length validation:

typescript
const MAX_SMS_LENGTH = 1600 // GSM-7 max with concatenation

if (text.length > MAX_SMS_LENGTH) {
  return {
    success: false,
    message: `Message exceeds maximum length of ${MAX_SMS_LENGTH} characters.`,
  }
}

Frequently Asked Questions About Plivo Bulk SMS with RedwoodJS

How many recipients can I send to in a single Plivo API request?

Plivo supports up to 1,000 unique destination numbers per API request for bulk messaging. For larger lists, split them into batches of 1,000 or fewer. Messages may not deliver instantly – they're queued based on your account's rate limits.

Reference: https://www.plivo.com/docs/messaging/api/message/bulk-messaging

What Node.js version should I use for RedwoodJS and Plivo integration?

Use Node.js v22 LTS for production. Node.js v22 remains in Active LTS until October 2025, then transitions to Maintenance LTS until April 2027. Plivo's Node.js SDK (v4.74.0) supports Node.js 5.5 and higher, making v22 LTS ideal for long-term stability.

How do I format phone numbers for Plivo's bulk SMS API?

Plivo requires E.164 format (e.g., +14155551212). For bulk messaging, separate multiple numbers with the < character: +14155551212<+14155551313<+14155551414. Use libphonenumber-js to validate and normalize numbers before sending.

What are the SMS character limits for Plivo messages?

Character limits depend on encoding:

EncodingSingle MessagePer Segment (Concatenated)Max Total
GSM-7160 characters153 characters1,600 characters
UCS-270 characters67 characters737 characters

Plivo automatically detects encoding. GSM-7 covers standard English/Western European characters. UCS-2 handles Unicode (emojis, special characters). Plivo's intelligent encoding replaces common Unicode characters (smart quotes, em dashes) with GSM-7 equivalents to optimize costs.

Reference: https://www.plivo.com/docs/messaging/concepts/encoding-and-concatenation

How do I secure my bulk SMS endpoint in RedwoodJS?

Use RedwoodJS's @requireAuth directive in your GraphQL schema. For role-based access, use @requireAuth(roles: ["admin", "manager"]). Configure authentication with dbAuth (self-hosted) or third-party providers (Auth0, Firebase Auth, Netlify Identity). Implement rate limiting to prevent abuse. Never expose bulk SMS endpoints without authentication.

What is the best way to handle Plivo API errors in RedwoodJS?

Implement comprehensive error handling with try...catch blocks in your service layer. Log errors using RedwoodJS's Pino logger (logger.error). For transient network errors, implement exponential backoff retry logic (wait 1s, 2s, 4s…). For production, use background job queues (RedwoodJS Jobs or BullMQ + Redis) to decouple sending from GraphQL requests, preventing timeouts and ensuring delivery during temporary outages.

How do I test my bulk SMS implementation without sending real messages?

Development testing approaches:

  1. Plivo test credentials: Plivo provides test credentials that simulate API calls without sending real messages or charging your account. Find these in your Plivo Console under Account Settings → Test Credentials.

  2. Mock testing: For automated tests, mock the Plivo client using Jest:

    typescript
    // messaging.test.ts
    jest.mock('plivo-node')
    
    const mockCreate = jest.fn().mockResolvedValue({
      message: 'message(s) queued',
      messageUuid: ['test-uuid-123']
    })
    
    PlivoClient.mockImplementation(() => ({
      messages: { create: mockCreate }
    }))
  3. GraphQL Playground: Test at http://localhost:8911/graphql with sample data (use your own number to verify delivery)

  4. Temporary auth bypass: Remove @requireAuth during initial testing (remember to add back before deployment)

Should I log SMS messages to a database in production?

Database logging is optional but recommended for production:

Benefits:

  • Audit trail for compliance
  • Diagnose delivery issues
  • Track costs
  • Monitor usage patterns

Implementation: Create a BulkMessageJob model storing status (PENDING, SUBMITTED, FAILED), recipient count, Plivo message UUIDs, and error messages.

Considerations:

  • Implement data retention policies (GDPR compliance)
  • For high volume (>100 messages/second), use async logging or batch writes
  • Monitor database performance impact
  • Consider privacy implications of storing phone numbers

How do I deploy a RedwoodJS bulk SMS application to production?

Deployment platforms:

PlatformBest ForSetup Complexity
VercelNext.js-style deployments, simple setupLow
NetlifyIntegrated CI/CD, instant rollbacksLow
RenderFull-stack apps, managed PostgreSQLMedium
AWS (Amplify/Elastic Beanstalk)Enterprise, full controlHigh

Deployment checklist:

  1. Environment variables: Configure PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SOURCE_NUMBER through your platform's settings – never commit .env

  2. Database migrations:

    bash
    yarn rw prisma migrate deploy
  3. Log forwarding: Configure production logging (Datadog, Logtail)

  4. Rate limiting: Implement request throttling for API protection

  5. Message queues: For high volume, deploy BullMQ with Redis or use AWS SQS

  6. Monitoring: Set up alerts for error rates, API costs, delivery failures

  7. CI/CD: Configure GitHub Actions or GitLab CI for automated testing and deployment

Example GitHub Actions workflow:

yaml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '22'
      - run: yarn install
      - run: yarn rw test
      - run: yarn rw prisma migrate deploy
      - run: yarn rw deploy netlify
        env:
          PLIVO_AUTH_ID: ${{ secrets.PLIVO_AUTH_ID }}
          PLIVO_AUTH_TOKEN: ${{ secrets.PLIVO_AUTH_TOKEN }}

What are the cost considerations for bulk SMS with Plivo?

Pricing structure:

  • Plivo uses pay-as-you-go pricing varying by destination country
  • US SMS: $0.0075–$0.01 per message
  • International rates vary significantly (check Plivo pricing page)
  • No per-message discount for bulk sending (same rate as individual messages)

Cost optimization strategies:

  1. Character optimization: Use Plivo's automatic encoding to minimize segments
  2. Phone number validation: Avoid wasted messages to invalid numbers
  3. Message batching: Reduce API overhead (not message costs)
  4. Monitor usage: Track daily/monthly spend in Plivo Console
  5. Volume pricing: Contact Plivo for discounts on high volumes (100,000+ messages/month)

Example cost calculation:

  • 10,000 recipients
  • US destinations
  • Average rate: $0.0085/message
  • Total: 10,000 × $0.0085 = $85

Budget alerts: Set up spending notifications in Plivo Console to prevent unexpected bills.

Frequently Asked Questions

How to send bulk SMS with RedwoodJS?

You can send bulk SMS messages by creating a RedwoodJS service that interacts with the Plivo API. This service will handle formatting destination numbers and sending the message text via a GraphQL mutation secured with `@requireAuth`.

What is Plivo used for in RedwoodJS?

Plivo is a cloud communications platform that provides SMS, voice, and WhatsApp APIs, used in this RedwoodJS application for its robust messaging capabilities and support for bulk sending. It allows you to send messages to multiple recipients with a single API request, reducing latency and simplifying logic.

Why use RedwoodJS for bulk SMS?

RedwoodJS offers an integrated full-stack structure, uses GraphQL, and provides developer-friendly features like generators and cells. Its backend services neatly encapsulate business logic like Plivo integration, offering a streamlined development experience for building a bulk SMS feature.

What is the system architecture for bulk SMS sending?

The architecture involves a web frontend sending requests to a RedwoodJS GraphQL API. The API interacts with a RedwoodJS Plivo service, which formats and sends messages via the Plivo API. Optionally, a RedwoodJS database logs messages and handles user interactions.

How to set up a RedwoodJS project for bulk SMS?

Create a new RedwoodJS project, install the Plivo Node.js SDK and types in the API workspace, and configure environment variables (`PLIVO_AUTH_ID`, `PLIVO_AUTH_TOKEN`, `PLIVO_SOURCE_NUMBER`) in a `.env` file. These credentials are essential for interacting with the Plivo API.

Where do I find my Plivo Auth ID and Auth Token?

Your Plivo Auth ID and Auth Token can be found on the main dashboard page after logging in to your Plivo Console at https://console.plivo.com/. Plivo documentation provides visual guides to their location on the dashboard if needed.

Where can I find my Plivo phone number for sending SMS?

Your Plivo phone number can be found in the Plivo Console by navigating to Messaging -> Phone Numbers -> Your Numbers. Be sure to select a number enabled for SMS and compliant with country regulations.

What format does Plivo expect for bulk destinations?

Plivo expects destination phone numbers to be concatenated into a single string delimited by the '<' character. For instance, "14155551212<14155551313<14155551414". This allows sending to multiple recipients in one API request.

How to secure the RedwoodJS bulk SMS API endpoint?

Secure the endpoint using `@requireAuth` directive in the GraphQL schema and set up an authentication provider (like dbAuth, Auth0, Netlify Identity, etc.). Restrict access further using roles (e.g., "admin", "manager") if needed.

How to validate phone numbers for Plivo?

While Plivo handles some number variations, validate and normalize numbers into E.164 format (e.g., +14155551212) before sending them to the service to prevent errors. Libraries like `libphonenumber-js` are helpful for this purpose.

What are the error handling mechanisms for Plivo API calls?

The `sendBulkSms` service function uses try-catch blocks to capture and log errors during Plivo API calls. The function returns a structured response indicating success or failure with error messages. For production, consider more robust retry mechanisms or queueing systems.

How to implement error logging for the bulk SMS feature?

Utilize Redwood's built-in Pino logger (`src/lib/logger.ts`) to log successful operations (info), failures (error), and detailed information (debug). Configure log levels per environment and consider forwarding logs to dedicated logging services in production.

Why does this example initialize the Plivo client outside the service function?

Initializing the Plivo client outside the `sendBulkSms` function allows for potential reuse and avoids recreating the client instance on every call, improving efficiency. It also ensures that environment variables needed for the Plivo client are properly loaded.

When should I use background jobs for sending bulk SMS?

Background jobs are recommended for production-level bulk SMS sending, especially for large broadcasts or potential Plivo delays. This decouples sending from the initial request and improves reliability by handling retries and failures independently.

Can I log bulk message attempts to a database?

Yes, the example provides an optional logging mechanism using Prisma. A `BulkMessageJob` model is added to the Prisma schema, allowing you to store job status, recipient count, messages, errors, and potentially destinations for tracking and analysis.