code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Article

How to Build a Bulk SMS Broadcasting App with RedwoodJS and MessageBird API

Complete guide to building a full-stack bulk SMS application with RedwoodJS, Prisma ORM, and MessageBird API. Learn database setup, GraphQL mutations, React forms, and production deployment.

Build a Bulk SMS Broadcasting App with RedwoodJS and MessageBird

Learn how to build a production-ready bulk SMS broadcasting application using RedwoodJS and MessageBird's SMS API. This comprehensive tutorial walks you through creating a full-stack Node.js application that manages contacts and sends SMS messages to large audiences efficiently using GraphQL, Prisma ORM, and React.

This guide covers everything you need to know about building SMS broadcast functionality with RedwoodJS: initial project setup, database modeling with Prisma, creating Redwood services, GraphQL mutations, React frontend development, secure API key management, error handling, and production deployment. By the end, you'll have a working MessageBird integration ready to send bulk SMS campaigns at scale.

Project Overview and Goals

Goal: Build a web application that sends bulk SMS messages to multiple contacts simultaneously using the MessageBird SMS API and RedwoodJS framework.

Problem Solved: Manually sending SMS to large groups is inefficient and error-prone. This bulk SMS application automates message delivery and provides an intuitive interface for SMS broadcasting campaigns.

Technologies Used:

  • RedwoodJS: Full-stack JavaScript/TypeScript framework combining React frontend, GraphQL API backend, and Prisma database layer with excellent developer experience.
  • Node.js: JavaScript runtime powering the RedwoodJS API server and MessageBird SDK integration.
  • Prisma ORM: Modern database toolkit for Node.js and TypeScript providing type-safe database access, migrations, and schema management.
  • MessageBird SMS API: Cloud communications platform offering reliable SMS delivery worldwide via their Node.js SDK.
  • PostgreSQL/SQLite/MySQL: Database options for storing contact information (Prisma supports all major databases).

System Architecture:

+-------------+ +-----------------+ +-----------------+ +-----------------+ +------------+ | User | ----> | Redwood Web UI | ----> | Redwood API | ----> | MessageBird API | ----> | Recipients | | (Browser) | | (React/Form) | | (GraphQL/Node.js) | | (SMS Gateway) | | (Phones) | +-------------+ +-----------------+ | | | +-----------------+ +------------+ | v | | +-------------+ | | | Prisma/DB | | | | (Contacts) | | | +-------------+ | +-----------------+

Expected Outcome: A RedwoodJS application with:

  • A database table to store contacts (name, phone number)
  • A Redwood service to handle the logic of fetching contacts and sending SMS via MessageBird
  • A GraphQL mutation endpoint to trigger the bulk broadcast
  • A simple web page with a form to input the message and initiate the broadcast

Prerequisites:

  • Node.js: Version 20.x or later
  • Yarn: Version 1.22.21 or later
  • MessageBird Account: Sign up at MessageBird and obtain an API Access Key
  • Database: Access to a PostgreSQL, SQLite, or MySQL database. SQLite is used by default for development in RedwoodJS

1. Setting Up the RedwoodJS Project

Create a new RedwoodJS project and install the MessageBird Node.js SDK to enable SMS functionality.

  1. Create RedwoodJS App: Open your terminal and run the RedwoodJS create command. This guide uses JavaScript, but TypeScript (--typescript) is also fully supported.

    bash
    yarn create redwood-app redwood-messagebird-broadcast
  2. Navigate to Project Directory:

    bash
    cd redwood-messagebird-broadcast
  3. Install Dependencies: RedwoodJS installs most dependencies during creation. Ensure everything is up to date.

    bash
    yarn install
  4. Install MessageBird SDK: Install the MessageBird Node.js SDK for the API side of your application. Use yarn workspace to add it correctly.

    bash
    yarn workspace api add messagebird
  5. Configure Environment Variables:

    Never hardcode sensitive information like API keys. RedwoodJS uses .env files for environment variables. Create a .env file in the project root:

    plaintext
    # .env
    # Replace YOUR_LIVE_API_KEY *entirely* with your actual key from the MessageBird dashboard.
    # Do not include quotes around the key itself. Correct format: MESSAGEBIRD_ACCESS_KEY=YourActualKeyValue
    MESSAGEBIRD_ACCESS_KEY=YOUR_LIVE_API_KEY
    
    # Optional: Set a default originator (sender ID).
    # Replace YourSenderID with your registered MessageBird number (E.164 format, e.g., +12025550181)
    # or an alphanumeric sender ID (max 11 chars, e.g., MyCompany). Check country regulations.
    # Do not include quotes around the sender ID itself. Correct format: MESSAGEBIRD_ORIGINATOR=YourActualSenderID
    MESSAGEBIRD_ORIGINATOR=YourSenderID
    • MESSAGEBIRD_ACCESS_KEY: Your live API key obtained from the MessageBird Dashboard (Developers → API access). Replace YOUR_LIVE_API_KEY with your actual key. The line should look like MESSAGEBIRD_ACCESS_KEY=live_xxxxxxxxxxxxxxxxxxxxxxx.
    • MESSAGEBIRD_ORIGINATOR: The sender ID recipients will see. This can be a purchased MessageBird number (in E.164 format, e.g., +12025550181) or an alphanumeric string (max 11 characters, e.g., MyCompany). Note: Alphanumeric sender IDs have restrictions in some countries (like the US). Check MessageBird documentation and country regulations. Replace YourSenderID with your actual value.

    Security: Add .env to your .gitignore file (it should be there by default) to prevent accidentally committing secrets.

  6. Start Development Server: Verify the basic setup by starting the development server.

    bash
    yarn redwood dev

    Your browser should open to http://localhost:8910, displaying the RedwoodJS welcome page.

Project Structure: RedwoodJS organizes code into specific directories:

  • api/: Contains the backend code (GraphQL API, services, database schema)
  • web/: Contains the frontend code (React components, pages, CSS)

2. Creating the Database Schema with Prisma

Define a Prisma schema to store contacts for your SMS broadcasts with proper E.164 phone number formatting.

  1. Define Prisma Schema: Open api/db/schema.prisma and define a Contact model. Replace any existing example models.

    prisma
    // api/db/schema.prisma
    
    datasource db {
      provider = "sqlite" // Or "postgresql" or "mysql"
      url      = env("DATABASE_URL")
    }
    
    generator client {
      provider = "prisma-client-js"
    }
    
    model Contact {
      id          Int      @id @default(autoincrement())
      name        String?
      phoneNumber String   @unique // E.164 format recommended (e.g., +14155552671)
      createdAt   DateTime @default(now())
    }
    • phoneNumber: Marked as unique to avoid duplicates. Store numbers in E.164 format (including the + and country code) for international compatibility with MessageBird.
    • name: An optional field for personalization.
  2. Apply Database Migrations:

    Prisma Migrate uses the schema to create and apply SQL migrations to your database.

    bash
    yarn rw prisma migrate dev
    • You'll be prompted to name the migration (e.g., create contact model).
    • This command creates the migration file and applies it to your development database (an SQLite file by default).
  3. Seed the Database (Optional but Recommended): Add some sample contacts to test your application. Redwood provides a seeding mechanism. Create or edit api/db/seed.js:

    javascript
    // api/db/seed.js
    import { db } from './db'
    
    // IMPORTANT: Replace these placeholder numbers with REAL, VALID phone numbers
    // in E.164 format (e.g. +14155552671). Include your own phone number
    // for testing purposes so you can verify message delivery.
    const CONTACTS_TO_CREATE = [
      { name: 'Test User One', phoneNumber: '+15551234567' }, // REPLACE with a real number
      { name: 'Test User Two', phoneNumber: '+15559876543' }, // REPLACE with another real number
      // Add more contacts as needed for testing
    ]
    
    export default async () => {
      try {
        console.log(`Seeding database with ${CONTACTS_TO_CREATE.length} contacts...`)
        console.log('Please ensure the phone numbers in seed.js are valid E.164 format for testing.')
    
        // Using Promise.all to execute all promises concurrently
        await Promise.all(
          CONTACTS_TO_CREATE.map((contactData) => {
            // Basic validation check (does not guarantee deliverability)
            if (!/^\+[1-9]\d{1,14}$/.test(contactData.phoneNumber)) {
               console.warn(`Skipping invalid phone number format: ${contactData.phoneNumber}. Use E.164 format (e.g., +14155552671).`)
               return Promise.resolve(); // Skip this contact
            }
            return db.contact.upsert({
              where: { phoneNumber: contactData.phoneNumber },
              update: contactData, // Update if phone number exists
              create: contactData, // Create if it doesn't
            })
          })
        )
        console.log(`Finished seeding contacts.`)
      } catch (error) {
        console.error('Error during database seeding:', error)
      }
    }
    • Replace the placeholder phone numbers (+1555...) with valid E.164 format numbers you can test with. Use your own phone number as one of the contacts.
    • The upsert operation prevents errors if you run the seed command multiple times with the same phone numbers.

    Run the seed command:

    bash
    yarn rw prisma db seed

3. Implementing the SMS Broadcasting Service

Create a RedwoodJS service to handle SMS sending logic using the MessageBird API with proper error handling and batch processing.

  1. Generate the Service: Generate the service file using the Redwood CLI.

    bash
    yarn rw g service messageBroadcaster

    This creates api/src/services/messageBroadcaster/messageBroadcaster.js and its corresponding test file.

  2. Implement the Broadcast Logic: Open api/src/services/messageBroadcaster/messageBroadcaster.js and add the logic.

    javascript
    // api/src/services/messageBroadcaster/messageBroadcaster.js
    import { db } from 'src/lib/db'
    import { logger } from 'src/lib/logger'
    import { initClient } from 'messagebird' // Import MessageBird SDK
    
    // Initialize MessageBird Client
    // Ensure MESSAGEBIRD_ACCESS_KEY is set in your .env file
    const messagebird = initClient(process.env.MESSAGEBIRD_ACCESS_KEY)
    
    // Note: Check current MessageBird API documentation for recipient limits per request.
    // This value might change over time.
    const MAX_RECIPIENTS_PER_REQUEST = 50
    
    export const sendBulkSms = async ({ messageBody }) => {
      logger.info('Initiating bulk SMS broadcast...')
    
      if (!process.env.MESSAGEBIRD_ACCESS_KEY) {
        logger.error('MessageBird API Key (MESSAGEBIRD_ACCESS_KEY) is not configured.')
        throw new Error('SMS service is not configured.')
      }
    
      if (!messageBody || messageBody.trim() === '') {
        logger.warn('Attempted to send empty message.')
        throw new Error('Message body cannot be empty.')
      }
    
      const originator = process.env.MESSAGEBIRD_ORIGINATOR || 'MessageBird' // Fallback originator
    
      try {
        // 1. Fetch all contacts from the database
        const contacts = await db.contact.findMany({
          select: { phoneNumber: true }, // Only select necessary field
        })
    
        if (!contacts || contacts.length === 0) {
          logger.warn('No contacts found in the database to send SMS to.')
          return { success: true, message: 'No contacts to send to.', sentCount: 0 }
        }
    
        const recipients = contacts.map((contact) => contact.phoneNumber)
        logger.info(`Found ${recipients.length} contacts. Preparing to send...`)
    
        // 2. Prepare the MessageBird payload & handle chunking
        let totalSent = 0
        let failedBatches = 0
    
        for (let i = 0; i < recipients.length; i += MAX_RECIPIENTS_PER_REQUEST) {
          const recipientChunk = recipients.slice(i, i + MAX_RECIPIENTS_PER_REQUEST)
          const params = {
            originator: originator,
            recipients: recipientChunk,
            body: messageBody,
          }
    
          const batchNum = i / MAX_RECIPIENTS_PER_REQUEST + 1
          logger.debug(`Sending batch ${batchNum} to ${recipientChunk.length} recipients.`)
    
          try {
            // 3. Send the message using MessageBird SDK
            const response = await new Promise((resolve, reject) => {
              messagebird.messages.create(params, (err, response) => {
                if (err) {
                  // Log detailed error from MessageBird
                  logger.error({ err }, `MessageBird API error during batch ${batchNum} send`)
                  return reject(err)
                }
                resolve(response)
              })
            })
    
            // Optional: Log success details (be mindful of sensitive data)
            // logger.debug({ response }, `MessageBird API success response for batch ${batchNum}`)
            totalSent += recipientChunk.length // Assume success for the batch if no error thrown
    
          } catch (batchError) {
            failedBatches++
            logger.error(`Failed to send batch ${batchNum} starting at index ${i}. Error: ${batchError.message}`)
            // Consider adding specific error handling or retry logic here
          }
        } // End of batch loop
    
        const totalBatches = Math.ceil(recipients.length / MAX_RECIPIENTS_PER_REQUEST)
        logger.info(`Bulk SMS broadcast attempt finished. Sent to approximately ${totalSent} recipients across ${totalBatches} batches. ${failedBatches} batches failed.`)
    
        if (failedBatches > 0) {
          return {
            success: false,
            message: `Broadcast attempted, but ${failedBatches} out of ${totalBatches} batches failed. Approximately ${totalSent} messages may have been sent. Check logs for details.`,
            sentCount: totalSent,
          }
        }
    
        return {
          success: true,
          message: `Successfully initiated broadcast to ${totalSent} recipients.`,
          sentCount: totalSent,
        }
    
      } catch (error) {
        logger.error({ error }, 'Error during bulk SMS broadcast process')
        // Return a generic error to the client
        throw new Error('Failed to send bulk SMS broadcast. Check API logs for details.')
      }
    }
    • Initialization: Initialize the MessageBird client using the key from .env
    • Input Validation: Check for the API key and non-empty message body
    • Fetch Contacts: Retrieve all phone numbers from the Contact table using Prisma
    • Chunking: Implement basic chunking based on MAX_RECIPIENTS_PER_REQUEST. Check MessageBird docs for current limits
    • API Call: Use messagebird.messages.create within a Promise wrapper for async/await syntax
    • Error Handling: Include try...catch blocks for database errors and MessageBird API errors. Log errors using Redwood's logger
    • Response: Return a success status, a message, and the count of messages attempted

4. Building the GraphQL API Layer

Define a GraphQL mutation endpoint that allows the frontend to trigger bulk SMS sends through your MessageBird service.

  1. Generate GraphQL Definition (SDL): Generate the SDL file for your broadcaster using the Redwood CLI. Define a custom mutation rather than full CRUD.

    bash
    yarn rw g sdl messageBroadcaster --no-crud

    This creates api/src/graphql/messageBroadcaster.sdl.js.

  2. Define the Mutation: Open api/src/graphql/messageBroadcaster.sdl.js and define the sendBulkSms mutation.

    graphql
    // api/src/graphql/messageBroadcaster.sdl.js
    export const schema = gql`
      type BroadcastResult {
        success: Boolean!
        message: String!
        sentCount: Int!
      }
    
      type Mutation {
        """
        Initiates a bulk SMS broadcast to all contacts.
        Requires authentication in production environments.
        """
        sendBulkSms(messageBody: String!): BroadcastResult! @skipAuth # WARNING: Development only!
        # !! IMPORTANT !!
        # For any real application, especially production, you MUST replace @skipAuth
        # with @requireAuth after setting up authentication (see Security section).
        # Leaving @skipAuth allows anyone to trigger SMS sends.
        # Example for production:
        # sendBulkSms(messageBody: String!): BroadcastResult! @requireAuth
      }
    `
    • BroadcastResult: Defines the shape of the data returned by the mutation
    • sendBulkSms Mutation: Takes a non-nullable messageBody string as input and returns a BroadcastResult
    • @skipAuth / @requireAuth: The @skipAuth directive makes the mutation publicly accessible for easy testing during development. Replace this with @requireAuth before deploying to any non-local environment. Failure to do so creates a major security vulnerability. See the Security section for implementing authentication
  3. Link Service to SDL (Resolver): The resolver connects the GraphQL mutation to the service function. Redwood automatically maps mutations in the SDL to functions with the same name in the corresponding service file (api/src/services/messageBroadcaster/messageBroadcaster.js). Since the service function and mutation are both named sendBulkSms, Redwood handles the connection automatically. No extra resolver code is needed.

5. Creating the React Frontend Interface

Build a React form component that sends bulk SMS messages through your GraphQL API with real-time feedback.

  1. Generate the Page: Use the Redwood CLI to generate a page component.

    bash
    yarn rw g page Broadcast /broadcast

    This creates web/src/pages/BroadcastPage/BroadcastPage.js and sets up the route /broadcast.

  2. Implement the Broadcast Form: Open web/src/pages/BroadcastPage/BroadcastPage.js and add the form and mutation logic.

    javascript
    // web/src/pages/BroadcastPage/BroadcastPage.js
    import { useState } from 'react'
    import { MetaTags, useMutation } from '@redwoodjs/web'
    import { toast, Toaster } from '@redwoodjs/web/toast'
    import { Form, TextAreaField, Submit, FieldError } from '@redwoodjs/forms'
    
    // GraphQL Mutation Definition (must match SDL)
    const SEND_BULK_SMS_MUTATION = gql`
      mutation SendBulkSmsMutation($messageBody: String!) {
        sendBulkSms(messageBody: $messageBody) {
          success
          message
          sentCount
        }
      }
    `
    
    const BroadcastPage = () => {
      const [messageBody, setMessageBody] = useState('')
      const [characterCount, setCharacterCount] = useState(0)
    
      const [sendSms, { loading, error }] = useMutation(SEND_BULK_SMS_MUTATION, {
        onCompleted: (data) => {
          const result = data.sendBulkSms
          if (result.success) {
            toast.success(
              `${result.message} (Approx. ${result.sentCount} messages)`
            )
            setMessageBody('') // Clear form on success
            setCharacterCount(0)
          } else {
            // Display potentially partial failures as warnings/errors
            toast.error(`Broadcast issue: ${result.message}`, { duration: 8000 })
          }
        },
        onError: (error) => {
          // Catches GraphQL/network level errors
          toast.error(`Error sending broadcast: ${error.message}`)
          console.error('GraphQL Mutation Error:', error)
        },
      })
    
      const onSubmit = (data) => {
        console.log('Form Submitted:', data)
        sendSms({ variables: { messageBody: data.messageBody } })
      }
    
      const handleInputChange = (event) => {
        const body = event.target.value
        setMessageBody(body)
        setCharacterCount(body.length)
      }
    
      // Basic SMS segment calculation (approximate).
      // NOTE: This is a simplified calculation. Real-world segment calculation
      // depends heavily on character encoding (GSM-7 vs. UCS-2 for special chars/emojis)
      // and concatenation headers. Using non-standard characters significantly reduces
      // characters per segment (often to 70). This estimate may not be accurate for billing.
      const calculateSegments = (count) => {
        if (count === 0) return 0
        // Very basic GSM-7 estimate
        if (count <= 160) return 1
        // Very rough multi-part estimate (assumes 153 chars per subsequent segment)
        return Math.ceil(count / 153)
      }
      const segments = calculateSegments(characterCount)
    
      return (
        <>
          <MetaTags title="Broadcast SMS" description="Send Bulk SMS Page" />
          <Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
    
          <h1>Send Broadcast SMS</h1>
          <p>
            Enter the message you want to send to all contacts stored in the
            database.
          </p>
    
          <Form onSubmit={onSubmit} className="rw-form-wrapper">
            <TextAreaField
              name="messageBody"
              value={messageBody}
              onChange={handleInputChange}
              placeholder="Type your SMS message here..."
              validation={{ required: true, minLength: 1 }}
              className="rw-input" // Basic Redwood input class
              errorClassName="rw-input rw-input-error"
              // Customize height/width using CSS classes
              // e.g., in web/src/index.css or a dedicated CSS module.
            />
            <FieldError name="messageBody" className="rw-field-error" />
    
            <div style={{ marginTop: '10px', marginBottom: '10px', color: '#555' }}>
              Character Count: {characterCount} (Approx. {segments} segment{segments !== 1 ? 's' : ''})
              <p style={{color: 'orange', fontSize: '0.9em', marginTop: '5px'}}>
                <strong>Note:</strong> Segment calculation is approximate. Messages over 160 characters (or fewer with special characters/emojis) will be split and billed as multiple SMS.
              </p>
            </div>
    
            <Submit disabled={loading || messageBody.trim() === ''} className="rw-button rw-button-blue">
              {loading ? 'Sending...' : 'Send Broadcast'}
            </Submit>
    
            {error && (
              <div className="rw-form-error" style={{ marginTop: '15px' }}>
                <p><strong>Broadcast Failed (Network/GraphQL Error):</strong></p>
                <pre>{error.message}</pre>
              </div>
            )}
          </Form>
        </>
      )
    }
    
    export default BroadcastPage
    • SEND_BULK_SMS_MUTATION: Defines the GraphQL mutation query. Uses gql tag.
    • useMutation Hook: Manages mutation state (loading, error).
    • onCompleted / onError: Handles success/error feedback using toast.
    • Form Components: Uses Redwood form helpers.
    • Character Count/Segments: Provides a basic estimate with a strong caveat emphasizing the approximation and billing implications.
    • State: Manages message body and count.
    • Styling: Uses default Redwood styles.

6. MessageBird API Integration Guide

Follow these essential steps to properly configure MessageBird's SMS API in your RedwoodJS application:

  • API Key: Store securely in .env as MESSAGEBIRD_ACCESS_KEY. Access in the service via process.env.MESSAGEBIRD_ACCESS_KEY. Ensure the correct .env syntax (KEY=VALUE) is used.
  • SDK Initialization: initClient(process.env.MESSAGEBIRD_ACCESS_KEY) in the service file (messageBroadcaster.js).
  • Sending Messages: Use messagebird.messages.create(params, callback) within the service.
  • Originator: Configure via MESSAGEBIRD_ORIGINATOR in .env or default in the service. Ensure the correct .env syntax (KEY=VALUE) is used. Ensure the chosen originator is valid and complies with regulations in target countries.
  • Recipient Format: Store phone numbers in the database in E.164 format (e.g., +14155552671) for reliable delivery.

Getting the API Key:

  1. Log in to your MessageBird Dashboard.
  2. Navigate to the "Developers" section in the left-hand menu.
  3. Click on "API access".
  4. You might see existing keys or need to "Add access key".
  5. Use a Live access key for sending actual messages. Copy this key.
  6. Paste the key into your .env file as the value for MESSAGEBIRD_ACCESS_KEY, ensuring the format is MESSAGEBIRD_ACCESS_KEY=YourActualKeyValue.

7. Implementing Error Handling, Logging, and Retries

Build robust applications with solid error handling and logging.

  • Error Handling:
    • Service Level: The messageBroadcaster service uses try...catch blocks. Catch specific errors (missing key, empty message) early. Log MessageBird API errors. Throw generic errors upwards. Log batch failures individually.
    • API Level (GraphQL): Redwood catches service errors and formats them as GraphQL errors.
    • Frontend Level: useMutation hook's onError catches GraphQL/network errors. onCompleted checks the success flag for application-level feedback.
  • Logging:
    • Use Redwood's logger (src/lib/logger.js) in the service (logger.info, logger.warn, logger.error, logger.debug).
    • Logs go to the console in development. Configure production log targets.
  • Retry Mechanisms (Conceptual):
    • The current implementation logs batch failures but doesn't retry.
    • For Production: Implement retries, especially for transient errors.
      • Simple Retry: Add a retry loop with backoff within the batch catch block in the service.
      • Background Jobs: For high reliability and large lists, use a background job queue (e.g., Redwood's experimental queue, BullMQ+Redis). This avoids HTTP timeouts and allows robust retry and tracking. (Beyond this guide's scope).

8. Adding Security Features

Secure your application.

  1. Authentication & Authorization:
    • Problem: The sendBulkSms mutation currently uses @skipAuth, making it public.
    • Solution: Implement RedwoodJS authentication.
      • Run yarn rw setup auth <provider> (e.g., dbAuth, Auth0). Follow setup instructions.
      • CRITICAL: Protect the API endpoint: Change @skipAuth to @requireAuth in api/src/graphql/messageBroadcaster.sdl.js. This is mandatory before any deployment.
      • Protect the Frontend Page: Wrap BroadcastPage in <Private> or use role-based controls. See RedwoodJS Auth docs.
  2. Secure API Key Management:
    • Use .env (added to .gitignore).
    • Set environment variables securely in deployment (do not commit production secrets).
  3. Input Validation:
    • Frontend: Basic validation via Redwood Forms.
    • Backend: Service validates messageBody. Prisma enforces phoneNumber uniqueness. Add server-side E.164 format validation if needed.
  4. Rate Limiting:
    • Problem: Risk of abuse leading to high costs or API throttling.
    • Solution: Implement rate limiting on the sendBulkSms mutation (e.g., using graphql-rate-limit-directive or infrastructure-level limits).
  5. Cross-Site Request Forgery (CSRF) Protection:
    • Enabled by default in RedwoodJS. Keep it enabled.
  6. Dependencies: Keep dependencies up-to-date (yarn outdated, yarn upgrade-interactive).

9. Handling Special Cases

  • Phone Number Formatting: Enforce E.164 format validation before saving contacts.
  • Character Encoding & Message Length: Standard SMS (GSM-7) is 160 characters. Non-standard characters (emojis) switch to UCS-2 (70 characters per segment). Long messages are split and billed per segment. The frontend estimate is approximate; inform users.
  • MessageBird Rate Limits: Be aware of MessageBird API limits. The chunking helps. For high volumes, add delays between batches or use background jobs.
  • Originator Restrictions: Check country rules for alphanumeric vs. numeric sender IDs.
  • Delivery Reports (Advanced): Set up MessageBird webhooks to receive delivery status updates for better tracking (requires a dedicated webhook handler endpoint in Redwood).

10. Implementing Performance Optimizations

Optimize performance for large lists.

  • Database Queries:
    • Current findMany() loads all contacts. For very large lists, this uses significant memory.
    • Optimization: Process contacts in DB batches using Prisma skip and take within a loop, instead of loading all at once.
      javascript
      // Inside sendBulkSms service – conceptual alternative loop for DB fetching
      const DB_BATCH_SIZE = 1000; // How many contacts to fetch from DB at once
      let skip = 0;
      let contactsBatch;
      let allRecipients = []; // Collect recipients if needed, or process directly
      
      do {
        contactsBatch = await db.contact.findMany({
          select: { phoneNumber: true },
          skip: skip,
          take: DB_BATCH_SIZE,
          orderBy: { id: 'asc' } // Consistent ordering is good practice for skip/take
        });
      
        if (contactsBatch.length > 0) {
          const batchRecipients = contactsBatch.map(c => c.phoneNumber);
          // Now, process 'batchRecipients' by sending them to MessageBird
          // (applying the MessageBird API chunking within this loop)
          // e.g., call a helper function: await sendToMessageBird(batchRecipients, originator, messageBody);
          logger.debug(`Fetched ${batchRecipients.length} contacts from DB (offset ${skip})`);
          skip += contactsBatch.length;
        }
      } while (contactsBatch.length === DB_BATCH_SIZE);
      
      // Make sure the MessageBird sending logic handles these smaller DB batches
  • API Call Chunking: Already implemented. Ensure MAX_RECIPIENTS_PER_REQUEST aligns with MessageBird's current limits (check their docs).
  • Asynchronous Processing: Use background jobs for sending to improve frontend responsiveness and reliability for large broadcasts.
  • Caching: Less relevant for sending, but could apply to displaying contacts or history.

11. Adding Monitoring, Observability, and Analytics

Monitor your application in production.

  • Logging: Use a centralized logging service (Datadog, Logtail, Sentry) in production.
  • Error Tracking: Integrate Sentry, Bugsnag, etc., to capture and alert on exceptions.
  • Health Checks: Implement a health check endpoint for your deployment platform.
  • Performance Monitoring (APM): Use APM tools (Datadog, New Relic) to trace requests and find bottlenecks.
  • Key Metrics: Track broadcasts, messages sent, error rates (API, MessageBird), API response times, and (Advanced) delivery rates.
  • Dashboards: Visualize key metrics in your monitoring tool.

12. Troubleshooting and Caveats

Common issues:

  • MESSAGEBIRD_ACCESS_KEY Incorrect/Missing: Check .env and production variables. Use a Live key. Error: "authentication failed".
  • Invalid Phone Number Format: Use E.164 (+1...). Error: "recipient not valid". Validate before saving.
  • MessageBird API Errors: Check MessageBird's API documentation for specific error codes returned in the logs. Common ones relate to invalid recipients, insufficient balance, or originator issues.
  • @skipAuth Left in Production: Major security risk. Ensure @requireAuth is used after setting up authentication.
  • Database Connection Issues: Verify DATABASE_URL in .env and ensure the database server is running and accessible.
  • Deployment Environment Variables: Ensure MESSAGEBIRD_ACCESS_KEY, MESSAGEBIRD_ORIGINATOR, and DATABASE_URL are correctly configured in your hosting environment (Vercel, Netlify, Render, etc.).

For more information about building SMS applications with Node.js and RedwoodJS, check out these resources: