code examples

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

Build a Scalable Bulk SMS System with RedwoodJS and AWS SNS

A comprehensive guide on creating a bulk SMS broadcasting application using RedwoodJS, AWS SNS, Prisma, and PostgreSQL, including setup, implementation, and security.

This guide provides a complete walkthrough for building a production-ready bulk SMS broadcasting system using the RedwoodJS framework and Amazon Simple Notification Service (AWS SNS).

We'll build an application that enables authenticated users to send a single message to a list of contacts stored in a database. This solves the common need for businesses to efficiently distribute notifications, alerts, or marketing messages via SMS.

Technologies Used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. Chosen for its integrated GraphQL API, Prisma ORM, and developer experience.
  • AWS Simple Notification Service (SNS): A fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication. Chosen for its scalability, reliability, and direct SMS sending capabilities.
  • PostgreSQL: A powerful, open-source object-relational database system. (Other databases supported by Prisma can also be used).
  • Prisma: A next-generation Node.js and TypeScript ORM for database access.
  • AWS SDK for JavaScript v3: Used to interact with AWS SNS programmatically.

System Architecture:

text
+-----------------+      +---------------------+      +-----------------+      +-------------------+
|   RedwoodJS     | ---> | RedwoodJS API       | ---> | AWS SDK Client  | ---> | AWS SNS Service   |
|   Web Interface |      | (GraphQL Mutation)  |      | (@aws-sdk/...)  |      | (PublishBatch API)|
|   (React)       |      +---------------------+      +-----------------+      +-------------------+
+-----------------+               |
                                 | Uses
                                 v
                         +-----------------+      +-------------------+
                         | Prisma Client   | ---> | PostgreSQL DB     |
                         | (Data Access)   |      | (Contacts Store)  |
                         +-----------------+      +-------------------+

Final Outcome:

By the end of this guide, you will have a RedwoodJS application with:

  • A database schema for storing contacts (name, phone number).
  • A secure GraphQL API endpoint for triggering bulk SMS messages.
  • Integration with AWS SNS to send messages efficiently using batching.
  • Basic error handling and logging.

Prerequisites:

  • Node.js (v18 or later recommended)
  • Yarn (v1 or later)
  • An AWS account with permissions to manage SNS and IAM.
  • Basic understanding of RedwoodJS, GraphQL, and AWS concepts.
  • AWS CLI configured locally (optional but helpful for verification).

1. Setting up the RedwoodJS Project

Let's initialize a new RedwoodJS project and install necessary dependencies.

  1. Create RedwoodJS App: Open your terminal and run the following command. Choose TypeScript when prompted.

    bash
    yarn create redwood-app redwood-sns-bulk-sms
  2. Navigate to Project Directory:

    bash
    cd redwood-sns-bulk-sms
  3. Install AWS SDK v3 for SNS: We need the specific client package for SNS.

    bash
    yarn workspace api add @aws-sdk/client-sns

    Why @aws-sdk/client-sns? The AWS SDK v3 is modular. Installing only the SNS client keeps our dependency footprint smaller compared to installing the entire SDK.

  4. Setup Database: RedwoodJS uses Prisma. By default, it's configured for SQLite. Let's switch to PostgreSQL (adjust connection string for your setup).

    • Edit api/db/schema.prisma: Change the provider in the datasource block.

      prisma
      // api/db/schema.prisma
      datasource db {
        provider = ""postgresql"" // Changed from ""sqlite""
        url      = env(""DATABASE_URL"")
      }
      
      generator client {
        provider      = ""prisma-client-js""
        binaryTargets = ""native""
      }
    • Create a .env file in the project root (if it doesn't exist) and add your PostgreSQL connection string:

      env
      # ./.env
      DATABASE_URL=""postgresql://user:password@host:port/database?schema=public""
      # Replace with your actual connection details
    • Explanation: The schema.prisma file defines our database connection and data models. The .env file stores sensitive information like database credentials, keeping them out of version control.

  5. Initial Database Migration: Apply the initial schema setup to your database.

    bash
    yarn rw prisma migrate dev
    # Follow prompts to name the migration (e.g., ""init"")

Your basic RedwoodJS project structure is now ready. The api directory contains backend code (GraphQL API, services, database schema), and the web directory contains frontend code (React components, pages).

2. Implementing Core Functionality (Backend Service & SNS Client)

We'll create a reusable SNS client and a service function to handle the logic of sending bulk messages.

  1. Configure AWS Credentials Securely: The AWS SDK needs credentials to interact with your account. We'll use environment variables, which RedwoodJS automatically loads from the .env file in development. Never hardcode credentials in your source code.

    • Add the following to your .env file (obtain these from your AWS IAM user setup - see Section 4):

      env
      # ./.env
      # Add these lines
      AWS_ACCESS_KEY_ID=""YOUR_AWS_ACCESS_KEY_ID""
      AWS_SECRET_ACCESS_KEY=""YOUR_AWS_SECRET_ACCESS_KEY""
      AWS_REGION=""us-east-1"" # Or your preferred AWS region
    • Explanation: The SDK automatically detects AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION (or AWS_DEFAULT_REGION) environment variables. Storing them here keeps them secure and environment-specific.

  2. Create an SNS Client Library: This centralizes the SNS client initialization.

    • Create a new file: api/src/lib/snsClient.js

      javascript
      // api/src/lib/snsClient.js
      import { SNSClient } from ""@aws-sdk/client-sns"";
      import { logger } from ""src/lib/logger""; // Import Redwood logger
      
      // The AWS SDK v3 automatically reads credentials from environment variables
      // (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)
      // or IAM roles if deployed on AWS infrastructure.
      const region = process.env.AWS_REGION;
      
      if (!region) {
        logger.warn(
          ""AWS_REGION environment variable not set. SNS Client might not initialize correctly.""
        );
        // Consider throwing an error in production if region is mandatory
      }
      
      export const snsClient = new SNSClient({ region });
      
      logger.info(""AWS SNS Client initialized."");
    • Explanation: We import the SNSClient, instantiate it (optionally passing the region), and export it. The Redwood logger is used for basic initialization feedback.

  3. Define the Contact Data Model: We need a way to store the phone numbers we want to message.

    • Edit api/db/schema.prisma and add the Contact model:

      prisma
      // api/db/schema.prisma
      
      // Keep the datasource and generator blocks from before
      
      model Contact {
        id        Int      @id @default(autoincrement())
        name      String? // Optional name
        phone     String   @unique // E.164 format recommended, ensure uniqueness
        createdAt DateTime @default(now())
        updatedAt DateTime @updatedAt
      }
    • Explanation: Defines a simple Contact table with an ID, an optional name, a unique phone number, and timestamps. Using @unique on phone prevents duplicate entries.

  4. Apply Database Migration: Create and apply a migration for the new Contact model.

    bash
    yarn rw prisma migrate dev
    # Name the migration (e.g., ""add contact model"")
  5. Generate Contact Scaffolding (Optional but helpful): Redwood can generate basic CRUD operations (Create, Read, Update, Delete) for the Contact model. This gives us a service and SDL definitions automatically.

    bash
    yarn rw generate scaffold contact
    • This creates:
      • api/src/graphql/contacts.sdl.js (GraphQL schema definitions)
      • api/src/services/contacts/contacts.js (Service functions for DB interaction)
      • Frontend pages/components in web/src (we won't focus on the UI here, but they are generated)
  6. Create the Messaging Service: This service will contain the core logic for fetching contacts and sending the bulk SMS via SNS.

    • Generate a new service:

      bash
      yarn rw generate service messaging
    • Edit api/src/services/messaging/messaging.js:

      javascript
      // api/src/services/messaging/messaging.js
      import { PublishBatchCommand } from ""@aws-sdk/client-sns"";
      import { UserInputError } from ""@redwoodjs/graphql-server"";
      
      import { db } from ""src/lib/db"";
      import { logger } from ""src/lib/logger"";
      import { snsClient } from ""src/lib/snsClient""; // Import our initialized client
      import { requireAuth } from ""src/lib/auth""; // We'll add auth later
      
      // E.164 regex (basic example; for robust validation, consider using libphonenumber-js as discussed in Section 8)
      const E164_REGEX = /^\+[1-9]\d{1,14}$/;
      
      // Max batch size for SNS PublishBatch
      const MAX_BATCH_SIZE = 10;
      
      export const sendBulkSms = async ({ message }) => {
        requireAuth(); // Ensure user is logged in
      
        if (!message || message.trim().length === 0) {
          throw new UserInputError(""Message content cannot be empty."");
        }
      
        // Consider adding length checks for SMS here (e.g., 160 chars for standard SMS)
      
        logger.info(""Initiating bulk SMS send job..."");
      
        let contacts = []; // Initialize contacts array
        try {
          contacts = await db.contact.findMany({
            select: { id: true, phone: true }, // Only select necessary fields
          });
      
          if (!contacts || contacts.length === 0) {
            logger.warn(""No contacts found in the database. Aborting SMS job."");
            return {
              successCount: 0,
              failureCount: 0,
              failedNumbers: [],
              message: ""No contacts found to send messages to."",
            };
          }
      
          logger.info(`Found ${contacts.length} contacts. Preparing batches...`);
      
          let successfulSends = 0;
          let failedSends = 0;
          const failedNumbersDetails = []; // Store { number, reason }
      
          // Process contacts in batches of MAX_BATCH_SIZE
          for (let i = 0; i < contacts.length; i += MAX_BATCH_SIZE) {
            const batch = contacts.slice(i, i + MAX_BATCH_SIZE);
            const batchEntries = batch
              .map((contact, index) => {
                // Validate phone number format before adding to batch
                if (!E164_REGEX.test(contact.phone)) {
                  logger.warn(
                    `Invalid phone number format skipped: ${contact.phone}`
                  );
                  failedSends++;
                  failedNumbersDetails.push({
                    number: contact.phone,
                    reason: ""Invalid E.164 Format"",
                  });
                  return null; // Skip this entry
                }
                return {
                  Id: `msg_${contact.id}_${index}`, // Unique ID within the batch
                  PhoneNumber: contact.phone,
                  // Optional: Add MessageAttributes if needed
                  // MessageAttributes: {
                  //   'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' }, // Or 'Promotional'
                  //   // Add other attributes if necessary
                  // }
                };
              })
              .filter((entry) => entry !== null); // Remove null entries (invalid numbers)
      
            if (batchEntries.length === 0) {
              logger.warn(`Skipping empty batch starting at index ${i}.`);
              continue; // Nothing to send in this batch
            }
      
            const command = new PublishBatchCommand({
              PublishBatchRequestEntries: batchEntries,
              Message: message, // The actual SMS content
            });
      
            try {
              logger.info(`Sending batch of ${batchEntries.length} messages...`);
              const result = await snsClient.send(command);
      
              // Process results
              if (result.Successful) {
                successfulSends += result.Successful.length;
                // Optional: Log successful IDs: result.Successful.map(s => s.Id)
              }
              if (result.Failed) {
                failedSends += result.Failed.length;
                result.Failed.forEach((failure) => {
                  // Find the original phone number from the batch using failure.Id
                  const originalEntry = batchEntries.find(
                    (entry) => entry.Id === failure.Id
                  );
                  const failedNumber = originalEntry
                    ? originalEntry.PhoneNumber
                    : ""Unknown ID: "" + failure.Id;
      
                  logger.error(
                    `Failed to send to ${failedNumber}: ${failure.Code} - ${failure.Message} (SenderFault: ${failure.SenderFault})`
                  );
                  failedNumbersDetails.push({
                    number: failedNumber,
                    reason: `${failure.Code}: ${failure.Message}`,
                  });
                });
              }
              logger.info(
                `Batch finished. Success: ${
                  result.Successful?.length || 0
                }, Failed: ${result.Failed?.length || 0}`
              );
            } catch (batchError) {
              // Handle errors sending the entire batch
              logger.error(
                `Error sending SMS batch starting at index ${i}:`,
                batchError
              );
              // Increment failure count for all intended recipients in this batch
              const attemptedCount = batchEntries.length;
              failedSends += attemptedCount;
              batchEntries.forEach((entry) => {
                failedNumbersDetails.push({
                  number: entry.PhoneNumber,
                  reason: `Batch Send Error: ${batchError.message || batchError.name}`,
                });
              });
            }
          } // End of batch loop
      
          logger.info(
            `Bulk SMS job completed. Total Success: ${successfulSends}, Total Failed: ${failedSends}`
          );
      
          return {
            successCount: successfulSends,
            failureCount: failedSends,
            failedNumbers: failedNumbersDetails, // Return detailed failures
            message: `SMS Job Finished. Sent: ${successfulSends}, Failed: ${failedSends}.`,
          };
        } catch (error) {
          logger.error(""Error during bulk SMS processing:"", error);
          // Generic error for the entire process
          return {
            successCount: 0,
            failureCount: contacts?.length || 0, // Assume all failed if error before/during loop
            failedNumbers: [],
            message: `An unexpected error occurred: ${error.message}`,
            error: true, // Indicate a top-level error occurred
          };
        }
      };
    • Explanation:

      • Imports necessary modules: PublishBatchCommand from AWS SDK, db for database access, logger, our snsClient, and UserInputError for GraphQL validation errors.
      • Defines MAX_BATCH_SIZE (SNS limit is 10 for PublishBatch).
      • The sendBulkSms function takes the message content as input.
      • It fetches all contacts from the DB (selecting only id and phone).
      • It iterates through contacts in batches.
      • Phone Number Validation: Includes a basic check using E164_REGEX before adding a number to a batch. Invalid numbers are skipped and logged. (Reminds user about Section 8 for better validation).
      • It constructs PublishBatchRequestEntries for each valid contact in the batch, ensuring a unique Id for each entry.
      • It creates and sends a PublishBatchCommand using snsClient.send().
      • Batch Result Handling: It processes the Successful and Failed arrays from the SNS response, logging details about failures and updating counters. It attempts to map failure IDs back to phone numbers for better reporting.
      • Error Handling: Includes try...catch blocks for both individual batch sends and the overall process.
      • Returns a summary object with success/failure counts and details on failed numbers.
      • requireAuth() is added (we'll set up auth next).

3. Building the API Layer (GraphQL Mutation)

We need a way for the frontend (or external clients) to trigger the sendBulkSms service function. We'll use a GraphQL mutation.

  1. Define the GraphQL Mutation:

    • Edit api/src/graphql/messaging.sdl.js:

      graphql
      # api/src/graphql/messaging.sdl.js
      
      type SendBulkSmsFailure {
        number: String!
        reason: String!
      }
      
      type SendBulkSmsResponse {
        successCount: Int!
        failureCount: Int!
        message: String!
        failedNumbers: [SendBulkSmsFailure!] # Return details
        error: Boolean # Optional flag for top-level errors
      }
      
      type Mutation {
        # Mutation to trigger sending SMS to all contacts
        sendBulkSms(message: String!): SendBulkSmsResponse! @requireAuth
      }
    • Explanation:

      • Defines a custom SendBulkSmsFailure type to structure failure details.
      • Defines a SendBulkSmsResponse type to structure the return value of our mutation, including success/failure counts and the detailed failures list.
      • Defines the sendBulkSms mutation within the Mutation type. It accepts a required message string.
      • Applies the @requireAuth directive to ensure only authenticated users can call this mutation.
  2. Implement Basic Authentication (dbAuth): Redwood makes setting up simple database authentication straightforward.

    • Run the setup command:

      bash
      yarn rw setup auth dbAuth
    • This command:

      • Adds necessary auth models (User, UserCredential, UserRole) to schema.prisma.
      • Creates auth-related service (api/src/services/users) and GraphQL definitions (api/src/graphql/users.sdl.js).
      • Sets up auth functions (api/src/lib/auth.js).
      • Adds frontend pages for login/signup (web/src/pages/LoginPage, web/src/pages/SignupPage, etc.) and updates routing.
    • Apply Auth Migrations: Run the migration to add the new auth tables to your database.

      bash
      yarn rw prisma migrate dev
      # Name the migration (e.g., ""add dbAuth models"")
    • Create a Test User: You'll need a user to test the authenticated mutation. You can either:

      • Start the dev server (yarn rw dev) and navigate to the signup page (/signup) in your browser.
      • Or, use yarn rw prisma studio to add a user directly to the database (remember to hash the password correctly if adding manually, or use the signup page).
  3. Link SDL to Service: Redwood automatically maps the sendBulkSms mutation in messaging.sdl.js to the sendBulkSms function exported from api/src/services/messaging/messaging.js because the names match. No extra resolver code is needed here.

  4. Testing the Mutation (GraphQL Playground):

    • Start the Redwood development server:

      bash
      yarn rw dev
    • Open your browser to http://localhost:8911/graphql.

    • Authentication Header: You need to simulate being logged in.

      1. Log in via the web interface (/login).
      2. Open your browser's developer tools (Network tab).
      3. Find a GraphQL request (e.g., one made after logging in).
      4. Copy the value of the auth-provider header (should be dbAuth).
      5. Copy the value of the authorization header (should be Bearer <session-token>).
      6. In the GraphQL Playground, click the ""HTTP HEADERS"" tab at the bottom left.
      7. Add the headers:
        json
        {
          ""auth-provider"": ""dbAuth"",
          ""authorization"": ""Bearer YOUR_COPIED_SESSION_TOKEN""
        }
    • Execute the Mutation: Run the following mutation in the Playground:

      graphql
      mutation SendTestBulkSms {
        sendBulkSms(message: ""Hello from Redwood SNS Bulk Sender!"") {
          successCount
          failureCount
          message
          failedNumbers {
            number
            reason
          }
        }
      }
    • Check the response in the Playground and the logs in your terminal (yarn rw dev output) to see the outcome. You should see logs indicating contacts being fetched, batches being sent, and the final result.

4. Integrating with AWS SNS (IAM & Setup)

Properly configuring AWS permissions is crucial for security and functionality.

  1. Create an IAM User for Your Application: It's best practice to create a dedicated IAM user with the minimum necessary permissions rather than using your root AWS account credentials.

    • Navigate to the IAM console in AWS.
    • Go to Users -> Create user.
    • Enter a User name (e.g., redwood-sns-app-user).
    • Select Provide user access to the AWS Management Console - Optional. If you select this, choose a password option.
    • Click Next.
    • Choose Attach policies directly.
    • Search for and select the AmazonSNSFullAccess policy. Note: For production, create a custom policy granting only sns:Publish and sns:PublishBatch permissions, potentially restricted to specific regions or topics if applicable. AmazonSNSFullAccess is simpler for this guide but too permissive for production.
      json
      // Example Custom Policy (More Secure)
      {
          ""Version"": ""2012-10-17"",
          ""Statement"": [
              {
                  ""Effect"": ""Allow"",
                  ""Action"": [
                      ""sns:Publish"",
                      ""sns:PublishBatch""
                  ],
                  ""Resource"": ""*"" // Restrict further by ARN if possible: ""arn:aws:sns:REGION:ACCOUNT_ID:*""
              }
          ]
      }
    • Click Next.
    • Review the user details and policy, then click Create user.
  2. Generate Access Keys:

    • After the user is created, click on the username to go to the user's summary page.
    • Go to the Security credentials tab.
    • Scroll down to Access keys and click Create access key.
    • Select Application running outside AWS as the use case.
    • Click Next.
    • (Optional) Add a description tag (e.g., redwood-sns-bulk-sms-key).
    • Click Create access key.
    • CRITICAL: Copy the Access key ID and Secret access key immediately and store them securely. You cannot retrieve the secret key again after closing this screen.
  3. Add Credentials to .env:

    • Open your project's .env file.

    • Paste the copied keys into the respective variables:

      env
      # ./.env
      DATABASE_URL=""..."" # Keep existing vars
      AWS_ACCESS_KEY_ID=""PASTE_YOUR_ACCESS_KEY_ID_HERE""
      AWS_SECRET_ACCESS_KEY=""PASTE_YOUR_SECRET_ACCESS_KEY_HERE""
      AWS_REGION=""us-east-1"" # Ensure this matches your desired region
  4. Restart Development Server: If your dev server (yarn rw dev) is running, stop it (Ctrl+C) and restart it to ensure it picks up the new environment variables from .env.

    bash
    yarn rw dev

Your application is now configured to authenticate with AWS SNS using the dedicated IAM user's credentials.

5. Implementing Error Handling, Logging, and Retries

Robust error handling and logging are essential for production systems.

  1. Error Handling (Already Implemented):

    • Input Validation: The service function (sendBulkSms) already includes checks for an empty message (UserInputError) and basic E.164 phone number format validation.
    • SNS Batch Errors: The code iterates through the Failed array in the PublishBatch response, logs each specific failure, and includes details in the mutation response.
    • General Exceptions: The main try...catch block in sendBulkSms catches unexpected errors during the process (e.g., database connection issues, SDK client errors).
    • Authentication: The @requireAuth directive handles unauthorized access attempts.
  2. Logging (Already Implemented):

    • RedwoodJS's built-in logger (api/src/lib/logger.js) is used throughout the sendBulkSms service.
    • We log:
      • Job initiation and completion.
      • Number of contacts found.
      • Batch processing progress.
      • Specific send failures with reasons from SNS.
      • Invalid phone numbers skipped.
      • Overall success/failure counts.
      • Major exceptions.
    • Configuration: You can configure Redwood's logger levels and destinations (e.g., console, file, third-party services) in api/src/lib/logger.js. For production, you'd typically set the level to info or warn and potentially log to a file or a log aggregation service.
  3. Retry Mechanisms:

    • SNS Internal Retries: AWS SNS itself has built-in retry mechanisms for transient delivery issues when sending SMS messages. You generally don't need to implement complex retry logic for the final delivery attempt from SNS to the carrier.

    • Application-Level Retries (for PublishBatch): The PublishBatch API call itself might fail due to transient network issues or AWS throttling. Our current code logs batch errors but doesn't automatically retry the entire batch.

    • Implementing Retries (Optional Enhancement): To add retries for failed PublishBatch calls, you could wrap the snsClient.send(command) call within a loop with exponential backoff:

      javascript
      // Inside the batch loop in api/src/services/messaging/messaging.js
      
      const MAX_RETRIES = 3;
      let attempt = 0;
      let batchResult = null;
      let lastBatchError = null;
      
      while (attempt < MAX_RETRIES && !batchResult) {
          try {
              logger.info(`Sending batch (Attempt ${attempt + 1}/${MAX_RETRIES})...`);
              batchResult = await snsClient.send(command);
              // Success, break loop
          } catch (error) {
              lastBatchError = error;
              logger.warn(`Batch send attempt ${attempt + 1} failed: ${error.name} - ${error.message}`);
              if (attempt + 1 < MAX_RETRIES) {
                  // Exponential backoff: wait 1s, 2s, 4s...
                  const delay = Math.pow(2, attempt) * 1000;
                  logger.info(`Retrying batch in ${delay / 1000}s...`);
                  await new Promise(resolve => setTimeout(resolve, delay));
              }
              attempt++;
          }
      }
      
      if (!batchResult && lastBatchError) {
         // Handle final failure after retries (log, increment counters as before)
         logger.error(`Error sending SMS batch starting at index ${i} after ${MAX_RETRIES} attempts:`, lastBatchError);
         // ... update failedSends and failedNumbersDetails for the whole batch ...
         continue; // Move to next batch
      }
      
      // Process the successful batchResult as before...
      if (batchResult.Successful) { /* ... */ }
      if (batchResult.Failed) { /* ... */ }
    • Consideration: Adding retries increases complexity. Evaluate if the potential benefits outweigh the added code, especially given SNS's own reliability. For critical messages, retrying might be necessary. Also consider idempotency if retrying.

6. Creating Database Schema and Data Layer

This was largely covered during the setup and core functionality implementation.

  1. Schema Definition (api/db/schema.prisma): We defined the Contact model and the dbAuth models.

    prisma
    // api/db/schema.prisma recap
    datasource db {
      provider = ""postgresql""
      url      = env(""DATABASE_URL"")
    }
    
    generator client {
      provider      = ""prisma-client-js""
      binaryTargets = ""native""
    }
    
    model Contact {
      id        Int      @id @default(autoincrement())
      name      String?
      phone     String   @unique // E.164 format recommended
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    }
    
    // --- dbAuth Models ---
    // RedwoodJS automatically adds these via `yarn rw setup auth dbAuth`
    // model User { ... }
    // model UserCredential { ... }
    // model UserRole { ... }
  2. Data Access Layer:

    • RedwoodJS uses Prisma Client as the data access layer. The db object imported from src/lib/db provides typed access to your database models.
    • Example usage in messaging.js: await db.contact.findMany(...)
    • The contacts service (api/src/services/contacts/contacts.js) generated by the scaffold command provides standard CRUD operations.
  3. Migrations:

    • We used yarn rw prisma migrate dev to apply schema changes. This command:
      • Compares the schema.prisma file to the database state.
      • Generates SQL migration files in api/db/migrations.
      • Applies the migration to the development database.
      • Updates the Prisma Client types.
    • For production deployments, you'll typically use yarn rw prisma migrate deploy.
  4. Sample Data Population (Seeding): To easily add test contacts, you can use Prisma's seeding feature.

    • Create a seed file: api/db/seed.js

      javascript
      // api/db/seed.js
      import { db } from 'src/lib/db'
      // Import any other libraries you need
      
      // Use ""+COUNTRYCODE AREACODE NUMBER"" format (E.164)
      const CONTACTS_TO_SEED = [
        { name: 'Test User One', phone: '+15551234567' },
        { name: 'Test User Two', phone: '+15559876543' },
        { name: 'Test User Three', phone: '+447700900123' }, // Example UK number
        { name: 'Invalid Number User', phone: '555-INVALID'}, // To test validation
      ]
      
      export default async () => {
        try {
          console.log('Seeding contacts...')
      
          // Use Prisma Promise API for potential performance improvement with many records
          await db.$transaction(
            CONTACTS_TO_SEED.map((contact) =>
              db.contact.upsert({
                where: { phone: contact.phone }, // Avoid duplicates based on phone
                update: { name: contact.name }, // Update name if phone exists
                create: contact,
              })
            )
          )
      
          console.log(`Seeded ${CONTACTS_TO_SEED.length} contacts (or updated existing).`);
      
          // Add other seed data if needed (e.g., default UserRoles)
      
        } catch (error) {
          console.error('Error seeding database:', error)
          // Avoid exiting with error code 1 in dev to prevent Prisma migrate prompts
          // process.exit(1)
        }
      }
    • Run the seed command (this automatically runs after migrate dev or can be run manually):

      bash
      yarn rw prisma db seed

7. Adding Security Features

Security is paramount, especially when dealing with potentially sensitive contact information and incurring costs via AWS services.

Frequently Asked Questions

How to send bulk SMS messages using RedwoodJS?

You can send bulk SMS messages by creating a RedwoodJS application that integrates with AWS SNS. This involves setting up a GraphQL API endpoint connected to a service function that interacts with the AWS SDK. This function handles sending messages in batches to contacts stored in your database.

What is RedwoodJS used for in this project?

RedwoodJS is the core framework for building the bulk SMS application. It provides structure, routing, GraphQL API capabilities, and integration with Prisma ORM for database interactions. This simplifies development and provides a cohesive full-stack experience.

Why use AWS SNS for bulk SMS messaging?

AWS SNS is chosen for its scalability and reliability in handling message delivery. It's a fully managed service, meaning AWS handles the infrastructure, allowing developers to focus on application logic. SNS also provides direct SMS sending capabilities.

How to set up a RedwoodJS project for bulk SMS?

First, create a new RedwoodJS project using `yarn create redwood-app`. Then, install the necessary AWS SDK modules and configure your database connection in `schema.prisma`. Finally, set up authentication using Redwood's built-in dbAuth and define a GraphQL mutation to trigger the SMS sending process.

What is the role of Prisma in the bulk SMS system?

Prisma acts as the Object-Relational Mapper (ORM) allowing RedwoodJS to interact with the database. It simplifies database operations by providing a type-safe and easy-to-use API for querying and managing data, such as fetching contact information.

When to use the PublishBatch API in AWS SNS?

The `PublishBatch` API is ideal for sending messages to multiple recipients simultaneously. This optimizes the sending process and is more efficient than individual API calls for each message. However, keep in mind that SNS limits batch sizes to 10 recipients.

Can I use a database other than PostgreSQL?

Yes, Prisma supports other databases besides PostgreSQL. You can configure the database connection by editing the `provider` field in your `schema.prisma` file and updating the `DATABASE_URL` environment variable accordingly.

How to handle AWS credentials securely in RedwoodJS?

Store your AWS credentials (Access Key ID and Secret Access Key) as environment variables in a `.env` file. RedwoodJS loads these variables automatically in development. Never hardcode credentials directly in your code.

What is the maximum batch size for sending SMS with AWS SNS?

The maximum batch size for the `PublishBatch` API in AWS SNS is 10. The provided code enforces this with a `MAX_BATCH_SIZE` constant. Exceeding this limit will cause errors.

How to handle errors when sending bulk SMS messages?

The example code provides error handling at multiple levels: input validation for empty messages, individual message failures within a batch using the SNS response's `Failed` array, batch-level error handling with retries, and overall process errors using try-catch blocks. Ensure logging is configured for monitoring.

How to structure phone numbers for sending SMS via AWS SNS?

The recommended format is E.164, which includes a plus sign (+) followed by the country code, area code, and local number. The code includes a basic regular expression, and Section 8 in the original article was meant to have better validation. For robust validation, consider a dedicated library like libphonenumber-js.

Why should I use IAM users instead of root credentials for AWS?

Using dedicated IAM users with limited permissions is crucial for security. It reduces the potential damage from compromised credentials and ensures least privilege access. In production, create custom IAM policies to restrict access only to essential SNS actions (like Publish and PublishBatch).

What is the purpose of setting up authentication with dbAuth?

The dbAuth setup protects the bulk SMS sending functionality. It ensures that only authorized users can access and trigger the GraphQL mutation responsible for sending messages, preventing unauthorized access.

How can I validate phone numbers more robustly?

While the code provides a basic regular expression for E.164 validation, consider using a specialized library like libphonenumber-js to enhance this process. A library handles various number formats and provides more accurate validation than regex alone.

What are the benefits of using a separate SNS client library?

Centralizing SNS client initialization in a separate library (`snsClient.js`) improves code organization and modularity. It makes it easier to manage and update the SNS client configuration across your application.