code examples

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

Developer Guide: Implementing Bulk SMS Broadcasts with Next.js and AWS SNS

A step-by-step guide for building a Next.js feature to send bulk SMS messages efficiently using AWS SNS PublishBatch API.

This guide provides a step-by-step walkthrough for building a feature within a Next.js application to send bulk SMS messages (broadcasts) efficiently using Amazon Simple Notification Service (SNS) and its PublishBatch API.

Sending individual messages via API calls can become slow and costly when dealing with hundreds or thousands of recipients. AWS SNS PublishBatch enables you to send up to 10 messages in a single API request, drastically reducing overhead and potentially lowering costs. We will build a secure Next.js API endpoint that accepts a list of phone numbers and a message, then uses the AWS SDK for JavaScript (v3) to interact with SNS for batch delivery.

Project Goals:

  • Create a Next.js API route to handle bulk SMS requests.
  • Integrate the AWS SDK v3 for SNS communication.
  • Implement efficient message batching using PublishBatchCommand.
  • Handle potential errors and provide feedback on delivery status.
  • Secure the API endpoint and AWS credentials.
  • Provide a foundation for reliable broadcast messaging.

Technology Stack:

  • Next.js: React framework for frontend and backend API routes.
  • AWS SNS: Managed messaging service for sending SMS.
  • AWS SDK for JavaScript v3: For interacting with AWS services.
  • Node.js: Runtime environment.

System Architecture:

plaintext
+-----------------+      +---------------------+      +-------------------+      +----------------+      +-----------+
| User Interface  | ---> | Next.js API Route   | ---> | AWS SDK (SNS v3)  | ---> | AWS SNS Service| ---> | Recipients|
| (Optional Form) |      | (/api/broadcast)    |      | (PublishBatch)    |      | (SMS Delivery) |      | (Mobile)  |
+-----------------+      +---------------------+      +-------------------+      +----------------+      +-----------+
       |                        |                             |
       |                        +-----------------------------+-------------> CloudWatch Logs/Metrics (Monitoring)
       |
       +---------------------------------------------------------------------> Secure API Call (Auth)

Prerequisites:

  • An AWS account with permissions to manage SNS and IAM.
  • Node.js (v18 or later recommended) and npm/yarn installed.
  • Basic understanding of Next.js, React, JavaScript/TypeScript, and REST APIs.
  • AWS CLI configured locally (optional but helpful for setup).

1. Project Setup

Let's initialize a new Next.js project and install the necessary dependencies.

  1. Create Next.js App: Open your terminal and run:

    bash
    npx create-next-app@latest sns-bulk-messaging-app
    cd sns-bulk-messaging-app

    (Choose your preferred settings regarding TypeScript, ESLint, Tailwind, src/ directory, App Router/Pages Router – this guide assumes Pages Router for API route simplicity, but App Router works similarly).

  2. Install AWS SDK: We need the SNS client package from the AWS SDK v3.

    bash
    npm install @aws-sdk/client-sns
    • Why AWS SDK v3? It offers modular packages, improved TypeScript support, and follows modern JavaScript practices compared to v2.
  3. Environment Variables: Sensitive information like AWS credentials and the SNS Topic ARN (if used) should never be hardcoded. Create a file named .env.local in the root of your project.

    plaintext
    # .env.local
    
    # AWS Credentials - BEST PRACTICE: Use IAM Roles for production deployments (e.g., on EC2/ECS/Lambda)
    # For local development, you can use credentials from ~/.aws/credentials or these vars.
    AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
    AWS_REGION=us-east-1 # Replace with your desired AWS region
    
    # Optional: If broadcasting via a Topic instead of direct phone numbers
    # SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:YourSnsTopicName
    • AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY: Credentials for an IAM user with programmatic access. Strongly recommended: For production, use IAM roles attached to your compute environment (like Vercel, EC2, Lambda) instead of long-lived keys. See Section 6 for more on security.
    • AWS_REGION: The AWS region where your SNS resources will be or are located (e.g., us-west-2, eu-central-1).
    • SNS_TOPIC_ARN: (Optional) If you intend to publish to a topic where users subscribe, include its ARN. This guide focuses on direct-to-phone-number batching, which doesn't strictly require a topic ARN for the PublishBatch command itself when specifying phone numbers directly in the entries.
  4. Gitignore: Ensure .env.local is included in your .gitignore file to prevent accidentally committing secrets. The default Next.js .gitignore usually includes it.

    text
    # .gitignore (ensure this line exists)
    .env*.local

2. AWS SNS and IAM Configuration

Before writing code, we need to configure AWS resources.

  1. IAM User/Role and Permissions: You need an IAM identity (user or role) that your Next.js application can use to interact with SNS.

    • Navigate to IAM: In the AWS Management Console, go to IAM.
    • Create Policy:
      • Go to Policies -> Create policy.
      • Switch to the JSON editor.
      • Paste the following policy, which grants permission to publish messages (including batches) via SNS. Replace YOUR_REGION, YOUR_ACCOUNT_ID, and optionally specify a Topic ARN if you only want to allow publishing to a specific topic. For direct SMS batching, the Resource: ""*"" might be necessary if not tied to a specific topic. Consult AWS documentation for the most precise permissions needed for direct SMS batch publish without a topic. For simplicity here, we grant broad publish access.
        json
        {
          ""Version"": ""2012-10-17"",
          ""Statement"": [
            {
              ""Effect"": ""Allow"",
              ""Action"": [
                ""sns:Publish"",
                ""sns:GetSMSAttributes"",
                ""sns:SetSMSAttributes""
              ],
              ""Resource"": ""*""
            }
          ]
        }
      • Click Next, give the policy a name (e.g., NextJsSnsBroadcastPolicy), and create it.
    • Create IAM User (for local dev / non-ideal production):
      • Go to Users -> Add users.
      • Enter a username (e.g., nextjs-sns-app-user).
      • Select ""Provide user access to the AWS Management Console"" - Uncheck this.
      • Select ""Attach policies directly"".
      • Search for and select the NextJsSnsBroadcastPolicy you just created.
      • Create the user.
      • Go to the user's details page -> Security credentials tab -> Create access key.
      • Select ""Application running outside AWS"".
      • Copy the Access key ID and Secret access key immediately and store them securely (e.g., in your .env.local for local dev, or a secrets manager). This is the only time the secret key is shown.
    • Use IAM Role (Recommended for Production): If deploying to AWS services (EC2, ECS, Lambda) or platforms like Vercel that support IAM roles, create an IAM Role instead. Attach the NextJsSnsBroadcastPolicy to the role, and configure your deployment environment to use this role. This avoids storing long-lived access keys in your application environment.
  2. SNS Configuration (Optional but Recommended):

    • SMS Sending Settings: In the AWS Console, navigate to SNS -> Text messaging (SMS). Review settings like:
      • Account spending limit: Set a reasonable limit to prevent unexpected costs.
      • Default message type: Choose 'Transactional' (for critical messages like OTPs, alerts) or 'Promotional' (for marketing). Transactional often has higher deliverability but stricter content rules. You can override this per message if needed using message attributes. This guide assumes 'Transactional' for reliability.
      • Sender ID: Depending on the destination country, you might need to register a Sender ID or use specific number types (Long Code, Short Code, Toll-Free). Research requirements for your target audience.

3. Implementing Core Batch Messaging Logic

Let's create the core function responsible for batching and sending messages.

  1. Create SNS Client Utility: Create a file lib/snsClient.js to initialize and export the SNS client instance.

    javascript
    // lib/snsClient.js
    import { SNSClient } from "@aws-sdk/client-sns";
    
    // Ensure environment variables are loaded (Next.js does this automatically for .env.local)
    const region = process.env.AWS_REGION;
    const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
    const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
    
    if (!region) {
      // Region is always required
      console.error("AWS_REGION environment variable is not set.");
      // Potentially throw an error or handle appropriately depending on deployment strategy
    }
    
    if (!accessKeyId || !secretAccessKey) {
      // In production, prefer IAM Roles, so keys might be undefined.
      // The SDK will automatically attempt to find credentials in the environment (e.g., instance profile).
      console.warn("AWS Access Key ID or Secret Access Key might be missing. Ensure IAM Role is configured for production or keys are set for local dev.");
    }
    
    const snsClientConfig = {
      region: region,
      // Only provide credentials if they are explicitly set in the environment.
      // If running with an IAM Role (e.g., on EC2/Lambda/Vercel), omit credentials
      // and the SDK will automatically use the role's permissions.
      ...(accessKeyId && secretAccessKey && {
        credentials: {
          accessKeyId: accessKeyId,
          secretAccessKey: secretAccessKey,
        },
      }),
    };
    
    const snsClient = new SNSClient(snsClientConfig);
    
    export { snsClient };
    • Why a separate file? It promotes reusability and keeps client initialization logic clean and centralized. It also handles credential configuration conditionally, supporting both local key-based development and production IAM roles.
  2. Create Batch Publishing Helper: Create lib/publishBatchHelper.js. This function will take phone numbers and a message, chunk them, and send batches to SNS.

    javascript
    // lib/publishBatchHelper.js
    import { PublishBatchCommand } from "@aws-sdk/client-sns";
    import { snsClient } from "./snsClient"; // Import the initialized client
    
    const MAX_BATCH_SIZE = 10; // SNS PublishBatch limit
    const MAX_RETRIES = 3; // Max retries for failed messages within a batch
    const INITIAL_DELAY_MS = 200; // Initial delay for exponential backoff
    
    /**
     * Sends messages in batches using SNS PublishBatch.
     * Handles basic retries for failed messages within a batch using exponential backoff.
     * NOTE: This basic retry attempts all failures; production logic should differentiate
     * between retryable (e.g., throttling) and non-retryable (e.g., invalid number) errors.
     *
     * @param {string[]} phoneNumbers - Array of E.164 formatted phone numbers.
     * @param {string} message - The message content to send.
     * @returns {Promise<{success: {id: string, phoneNumber: string, messageId: string}[], failed: {id: string, phoneNumber: string, code: string, message: string}[]}>} - Summary of successful and failed sends.
     */
    export const sendBulkSms = async (phoneNumbers, message) => {
      const allSuccessful = [];
      const allFailed = [];
      const uniqueIdPrefix = `batch-${Date.now()}`; // Unique prefix for batch entries
    
      // Chunk phone numbers into batches of MAX_BATCH_SIZE
      for (let i = 0; i < phoneNumbers.length; i += MAX_BATCH_SIZE) {
        const batchNumbers = phoneNumbers.slice(i, i + MAX_BATCH_SIZE);
        let currentBatchEntries = batchNumbers.map((num, index) => ({
          Id: `${uniqueIdPrefix}-${i + index}`, // Unique ID within the entire operation
          PhoneNumber: num, // Use PhoneNumber for direct SMS
          Message: message,
          // Optional: Add MessageAttributes for SenderID, SMSType etc.
          // MessageAttributes: {
          //   'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' },
          //   'AWS.SNS.SMS.SenderID': { DataType: 'String', StringValue: 'MySenderID' }
          // }
        }));
    
        let attempts = 0;
        let batchFailed = true; // Assume failure until proven otherwise
        let failedEntriesDetails = new Map(); // Store details of failures for final reporting
    
        while (attempts < MAX_RETRIES && currentBatchEntries.length > 0) {
          failedEntriesDetails.clear(); // Clear details for this attempt
          try {
            const command = new PublishBatchCommand({
              // TopicArn: process.env.SNS_TOPIC_ARN, // Use this if publishing to a Topic instead
              PublishBatchRequestEntries: currentBatchEntries,
            });
    
            console.log(`Attempt ${attempts + 1}: Sending batch of ${currentBatchEntries.length} messages starting with ID ${currentBatchEntries[0].Id}...`);
            const response = await snsClient.send(command);
            console.log(`Batch Response Received. Success: ${response.Successful?.length || 0}, Failed: ${response.Failed?.length || 0}`);
    
            // Process successful messages
            if (response.Successful) {
              response.Successful.forEach(success => {
                // Find the original entry to get the phone number
                const originalEntryIndex = currentBatchEntries.findIndex(entry => entry.Id === success.Id);
                if (originalEntryIndex !== -1) {
                    allSuccessful.push({
                        id: success.Id,
                        phoneNumber: currentBatchEntries[originalEntryIndex].PhoneNumber,
                        messageId: success.MessageId,
                    });
                    // Remove successful entry from current batch for retry logic
                    currentBatchEntries.splice(originalEntryIndex, 1);
                }
              });
            }
    
            // Identify entries that failed in this attempt and store details
            if (response.Failed) {
              response.Failed.forEach(failure => {
                 // Only retry sender faults potentially? Or specific error codes? Be selective.
                 // The current simplistic logic retries all failures.
                 failedEntriesDetails.set(failure.Id, failure);
                 console.error(`Failed Entry: ID=${failure.Id}, Code=${failure.Code}, Message=${failure.Message}, SenderFault=${failure.SenderFault}`);
                 // Keep the failed entry in currentBatchEntries for the next retry attempt
              });
            }
    
            // Check if any entries are left in the current batch to retry
            if (currentBatchEntries.length === 0) {
              batchFailed = false; // All entries in the original batch succeeded eventually
              break; // Exit retry loop for this batch
            } else {
               // Apply exponential backoff before retrying remaining entries
               const delay = INITIAL_DELAY_MS * Math.pow(2, attempts);
               console.log(`Retrying ${currentBatchEntries.length} failed entries after ${delay}ms...`);
               await new Promise(resolve => setTimeout(resolve, delay));
               // currentBatchEntries already contains only the failures to retry
            }
    
          } catch (error) {
            console.error(`Error sending batch (Attempt ${attempts + 1}):`, error);
            // If the entire batch request fails (e.g., network error, throttling), retry all entries in the current batch.
             const delay = INITIAL_DELAY_MS * Math.pow(2, attempts);
             console.log(`Retrying entire batch after ${delay}ms due to error...`);
             await new Promise(resolve => setTimeout(resolve, delay));
             // Keep currentBatchEntries as is for the next attempt
          }
          attempts++;
        } // End while retry loop
    
        // After retries, any remaining entries in currentBatchEntries are considered finally failed
        if (batchFailed && currentBatchEntries.length > 0) {
           console.error(`Batch finally failed after ${MAX_RETRIES} attempts for IDs: ${currentBatchEntries.map(e => e.Id).join(', ')}`);
           currentBatchEntries.forEach(entry => {
             // Use the last known failure details or create a generic one
             const failureDetails = failedEntriesDetails.get(entry.Id) || { Code: 'RetryFailed', Message: `Failed after ${MAX_RETRIES} attempts`, SenderFault: true };
             allFailed.push({
               id: entry.Id,
               phoneNumber: entry.PhoneNumber, // We still have the full entry object here
               code: failureDetails.Code,
               message: failureDetails.Message,
             });
           });
        }
    
      } // End for loop iterating through chunks
    
      console.log(`Bulk SMS process completed. Success: ${allSuccessful.length}, Failed: ${allFailed.length}`);
      return { success: allSuccessful, failed: allFailed };
    };
    • Why this approach?
      • Chunking: It splits potentially large lists into SNS-compatible batches of 10.
      • Direct PhoneNumber: It uses the PhoneNumber property within PublishBatchRequestEntries for direct SMS, which is common for broadcast use cases where recipients aren't necessarily subscribed to a topic.
      • Unique IDs: Generates unique IDs (Id) for each message within the batch, essential for correlating responses in the PublishBatchResult.
      • Response Handling: It parses the Successful and Failed arrays from the SNS response to track individual message status.
      • Retry Logic: Implements a basic exponential backoff retry mechanism for messages that failed within a batch. This handles transient errors. Note: This basic logic attempts to retry all failed messages; a production system should implement more sophisticated logic to differentiate between transient/retryable errors (like throttling) and permanent/non-retryable errors (like InvalidParameterValue for a bad phone number or OptedOut) to avoid unnecessary retries.
      • Error Logging: Includes console.log and console.error for visibility (replace with a proper logger in production).

4. Building the API Layer

Now, let's create the Next.js API route that will expose this functionality.

  1. Create API Route File: Create pages/api/broadcast.js.

    javascript
    // pages/api/broadcast.js
    import { sendBulkSms } from '../../lib/publishBatchHelper'; // Adjust path if using src/ directory
    // Optional: Use a validation library like zod for robust input validation
    // import { z } from 'zod';
    
    // WARNING: Basic API Key Auth - Placeholder Only!
    // Replace this with a robust authentication mechanism (JWT, OAuth, etc.) in production.
    const DUMMY_API_KEY = process.env.BROADCAST_API_KEY || 'verysecretkey'; // Load from env
    
    // Optional: Zod schema for validation
    // const broadcastSchema = z.object({
    //   phoneNumbers: z.array(z.string().regex(/^\+?[1-9]\d{1,14}$/)).min(1), // E.164 format regex (basic)
    //   message: z.string().min(1).max(1600), // SMS max length varies, 1600 is generous
    // });
    
    export default async function handler(req, res) {
      // 1. Authentication & Authorization (PLACEHOLDER - REPLACE THIS)
      const apiKey = req.headers['x-api-key'];
      if (req.method !== 'POST') {
        res.setHeader('Allow', ['POST']);
        return res.status(405).end(`Method ${req.method} Not Allowed`);
      }
      // **THIS IS NOT PRODUCTION-READY AUTHENTICATION.**
      if (!apiKey || apiKey !== DUMMY_API_KEY) {
         console.warn('Unauthorized attempt to access broadcast API.');
         return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key.' });
      }
      // In production, replace the above check with proper session, token, or service-to-service auth.
    
      // 2. Request Validation
      const { phoneNumbers, message } = req.body;
    
      // Basic validation (replace/enhance with Zod or similar)
      if (!Array.isArray(phoneNumbers) || phoneNumbers.length === 0 || typeof message !== 'string' || message.trim() === '') {
        return res.status(400).json({ error: 'Invalid input: ""phoneNumbers"" must be a non-empty array and ""message"" must be a non-empty string.' });
      }
      // Add more specific validation (e.g., E.164 format check)
      const invalidNumbers = phoneNumbers.filter(num => !/^\+?[1-9]\d{1,14}$/.test(num));
       if (invalidNumbers.length > 0) {
         return res.status(400).json({ error: `Invalid phone number format for: ${invalidNumbers.join(', ')}. Use E.164 format (e.g., +1xxxxxxxxxx).` });
       }
    
      // Optional: Use Zod for stricter validation
      // try {
      //   broadcastSchema.parse(req.body);
      // } catch (error) {
      //   return res.status(400).json({ error: 'Invalid input', details: error.errors });
      // }
    
      // 3. Call Core Logic
      try {
        console.log(`Received broadcast request for ${phoneNumbers.length} numbers.`);
        const result = await sendBulkSms(phoneNumbers, message);
        console.log(`Broadcast API Result: Success=${result.success.length}, Failed=${result.failed.length}`);
    
        // 4. Respond
        // Depending on needs, you might only return a summary or include full details
        return res.status(200).json({
          message: `Broadcast attempt finished.`,
          attempted: phoneNumbers.length,
          sentSuccessfully: result.success.length,
          failedToSend: result.failed.length,
          // Optionally include details (can be large):
          // successes: result.success,
          failures: result.failed, // Good to return failures for diagnostics
        });
      } catch (error) {
        console.error('Error processing broadcast request in API handler:', error);
        return res.status(500).json({ error: 'Internal Server Error processing broadcast request.' });
      }
    }
    • Authentication: Includes a very basic, placeholder API key check using the x-api-key header. This is NOT production-ready and MUST be replaced. Use proper authentication (e.g., JWT, OAuth, session cookies, dedicated auth providers) based on your application's security requirements. Load the placeholder key from environment variables (BROADCAST_API_KEY).
    • Validation: Performs basic checks on the request body (phoneNumbers array, message string). Includes a commented-out example using zod for more robust validation, including a basic E.164 regex check. Remember to npm install zod if you use it.
    • Error Handling: Uses try...catch to handle errors during the sendBulkSms call and returns appropriate HTTP status codes (400, 401, 405, 500).
    • Response: Returns a JSON response summarizing the outcome, including counts of successes and failures, and detailed information about the failures.
  2. Testing the API Endpoint: You can use curl or a tool like Postman/Insomnia.

    • Set API Key Env Var: Before running, make sure you add BROADCAST_API_KEY=verysecretkey (or your chosen key) to your .env.local file. Restart your Next.js dev server (npm run dev) after adding it.

    • curl Example: Replace +15551234567, +15557654321 with valid test phone numbers (use your own mobile number for initial testing).

      bash
      curl -X POST http://localhost:3000/api/broadcast \
      -H ""Content-Type: application/json"" \
      -H ""x-api-key: verysecretkey"" \
      -d '{
        ""phoneNumbers"": [""+15551234567"", ""+15557654321""],
        ""message"": ""Hello from our Next.js broadcast system! (Test)""
      }'
    • Expected Response (Success Scenario):

      json
      {
        ""message"": ""Broadcast attempt finished."",
        ""attempted"": 2,
        ""sentSuccessfully"": 2,
        ""failedToSend"": 0,
        ""failures"": []
      }
    • Expected Response (Partial Failure Scenario):

      json
      {
        ""message"": ""Broadcast attempt finished."",
        ""attempted"": 2,
        ""sentSuccessfully"": 1,
        ""failedToSend"": 1,
        ""failures"": [
          {
            ""id"": ""batch-1678886400000-1"",
            ""phoneNumber"": ""+15557654321"",
            ""code"": ""InvalidParameter"",
            ""message"": ""Invalid parameter: PhoneNumber""
          }
        ]
      }

5. Implementing Proper Error Handling, Logging, and Retry Mechanisms

  • Error Handling Strategy:
    • Use try...catch blocks in API handlers and helper functions.
    • Validate input rigorously at the API boundary (Section 4).
    • Return meaningful HTTP status codes (4xx for client errors, 5xx for server errors).
    • Provide clear error messages in API responses, potentially hiding internal details in production.
  • Logging:
    • Use console.log for informational messages (request received, batch sent) and console.error for errors.
    • Production: Integrate a structured logger (e.g., Pino, Winston). Log request IDs, batch IDs, success/failure details, and error stack traces. Send logs to a centralized service (AWS CloudWatch Logs, Datadog, Sentry).
    • Example (Conceptual Pino Integration):
      javascript
      // Replace console.log/error with logger calls
      // import pino from 'pino';
      // const logger = pino();
      // logger.info({ batchId: command.PublishBatchRequestEntries[0].Id, count: command.PublishBatchRequestEntries.length }, 'Sending SNS batch');
      // logger.error({ err: error, batchId: ... }, 'Error sending SNS batch');
  • Retry Mechanisms:
    • The publishBatchHelper includes exponential backoff for failed messages within a batch attempt.
    • It also retries the entire batch API call if the snsClient.send itself throws an error (e.g., network issue, throttling).
    • Improvement Needed: As noted in Section 3, the current retry logic is basic. A production-ready implementation should differentiate error types. Do not retry non-retryable errors like InvalidParameterValue (bad number) or OptedOut. Only retry potentially transient errors like ThrottlingException, InternalFailure, or ServiceUnavailable. This requires inspecting the failure.Code and failure.SenderFault properties from the response.Failed array.
    • Testing Errors:
      • Pass deliberately invalid phone numbers (e.g., "not-a-number", "+123") to the API to test validation and SNS failure responses.
      • Temporarily revoke IAM permissions to test authorization errors.
      • Send rapid-fire requests to potentially trigger SNS throttling (though batching reduces the likelihood).
      • Mock the snsClient.send method in unit tests to throw specific errors.
  • Log Analysis: Use CloudWatch Logs Insights (if logging to CloudWatch) to query logs for specific error codes, failed phone numbers, or batch IDs during troubleshooting.

6. Adding Security Features

  • IAM Best Practices:
    • Least Privilege: The IAM policy in Section 2 should be as restrictive as possible. If only using direct SMS, potentially remove permissions related to topics unless needed. Grant only sns:Publish.
    • IAM Roles: Strongly prefer IAM roles over access keys for applications deployed on AWS or Vercel. Configure your deployment environment to assume a role with the necessary SNS permissions.
  • API Endpoint Security:
    • Authentication: The basic API key (Section 4) is insufficient and insecure for production. Replace it with robust authentication (JWT, OAuth, session-based, mTLS, signed requests) depending on who/what is calling the API.
    • Authorization: Ensure the authenticated user/service has permission to trigger broadcasts.
  • Input Validation and Sanitization:
    • Use libraries like zod (Section 4) to strictly validate phoneNumbers (E.164 format) and message content/length.
    • Sanitize message content if it includes user-generated input, although SNS primarily handles text content. Be mindful of character limits and encoding.
  • Rate Limiting:
    • Protect your API endpoint (/api/broadcast) from abuse. Implement rate limiting based on IP address, API key, or user ID. Libraries like rate-limiter-flexible or Vercel's built-in features can help.
    • SNS also has its own service quotas (messages per second, API requests per second). PublishBatch helps stay within these, but be aware of them.
  • Common Vulnerabilities: While less common for a backend SMS service, ensure standard web security practices are followed (e.g., protection against DoS via large request bodies if not validated early, secure dependency management).
  • Secrets Management: Store API keys (for real auth methods), AWS credentials (if not using roles), and other secrets securely using environment variables loaded into the execution environment or a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault). Ensure .env.local is gitignored.

7. Handling Special Cases Relevant to the Domain (SMS)

  • Phone Number Formatting: Strictly enforce E.164 format (+ followed by country code and number, e.g., +14155552671). The validation in Section 4 includes a basic regex.
  • Internationalization: SNS supports sending SMS globally, but regulations, Sender ID requirements, and costs vary significantly by country. Research the specific rules for your target countries. You might need different Sender IDs or even different AWS regions for optimal delivery.
  • Opt-Out Handling: SNS automatically handles standard opt-out keywords (like STOP). When a user opts out, subsequent messages to that number from your AWS account will fail with an error like OptedOut. Your application should gracefully handle these failures reported by PublishBatch (or single Publish), log them, and potentially update the user's status in your own database to prevent future attempts. Do not retry messages that failed due to opt-out.
  • Message Encoding & Length: Standard SMS is 160 characters (GSM-7 encoding). Longer messages are split into multiple segments (concatenated SMS), billed individually. Using non-GSM characters (like some emoji) forces UCS-2 encoding, reducing the limit per segment to 70 characters. Keep messages concise. SNS handles segmentation, but be aware of potential cost implications. Max message size in SNS is generally much larger but gets chunked for SMS delivery.
  • Sender ID: As mentioned (Section 2), Sender ID behavior varies. Some countries require pre-registration, others allow dynamic alphanumeric IDs, and some default to a shared pool number if none is specified. Use the AWS.SNS.SMS.SenderID message attribute in PublishBatchRequestEntries if needed.
  • Transactional vs. Promotional: Use the AWS.SNS.SMS.SMSType message attribute ('Transactional' or 'Promotional') if you need to override the account default per message. Ensure compliance with regulations for promotional messages (e.g., opt-in requirements, sending hours).

8. Implementing Performance Optimizations

  • Batching (PublishBatch): This is the primary performance optimization for this use case, reducing API calls by up to 10x compared to individual Publish calls. The core logic (Section 3) already implements this.
  • Asynchronous Processing: (Content cut off in original)

Frequently Asked Questions

How to send bulk SMS with Next.js and AWS SNS?

Create a Next.js API route that uses the AWS SDK for JavaScript v3 to interact with the SNS `PublishBatch` API. This API allows sending up to 10 messages per request, improving speed and cost-efficiency compared to individual messages.

What is AWS SNS PublishBatch used for?

AWS SNS `PublishBatch` enables sending multiple SMS messages (up to 10) in a single API call. This method reduces overhead and improves performance, especially for bulk messaging, compared to individual API calls for each message.

Why use AWS SDK v3 for SNS integration?

AWS SDK v3 offers benefits like modular packages for reduced bundle size, improved TypeScript support, and better alignment with modern JavaScript practices, making it a more efficient choice than v2 for interacting with AWS services like SNS.

When should I use AWS IAM roles for SNS?

IAM roles are strongly recommended for production deployments on AWS services like EC2, ECS, or Lambda, or platforms supporting IAM role integration, like Vercel. They provide secure, temporary credentials without the risks associated with long-lived access keys stored in environment variables.

Can I send SMS directly to phone numbers with SNS?

Yes, you can send SMS messages directly to phone numbers using the `PublishBatch` API by specifying the `PhoneNumber` property for entries. This is suitable for broadcast scenarios where recipients are not subscribed to a specific SNS topic.

How to handle AWS credentials securely in Next.js?

Store AWS credentials (access key ID and secret access key) in a `.env.local` file for local development, which should be included in your `.gitignore` to avoid committing secrets. For production, utilize IAM roles instead of storing access keys directly in your application's environment.

What is the maximum batch size for SNS PublishBatch?

The maximum batch size for AWS SNS `PublishBatch` is 10 messages per API request. The provided implementation chunks larger lists into batches of this size to efficiently process bulk messages.

How to handle failed messages in SNS PublishBatch?

Implement retry logic with exponential backoff for failed messages within a batch. Differentiate between retryable errors (throttling) and non-retryable errors (invalid numbers) to avoid unnecessary retries and enhance reliability. Log failures for diagnostics.

What is the importance of message attributes in SNS?

Message attributes in SNS enable you to add metadata to your SMS messages, like specifying 'Transactional' or 'Promotional' message types, setting Sender ID, and including custom tags. This offers fine-grained control over message properties and behaviors.

How to secure the Next.js API endpoint for SMS broadcast?

Replace placeholder API key authentication with robust methods like JWT, OAuth, or session-based authentication. Implement input validation, rate limiting, and proper authorization to protect against unauthorized access and abuse.

What is the recommended phone number format for SNS?

Use the E.164 format (+[country code][number]) for phone numbers, such as +14155552671. This ensures consistent and reliable delivery of SMS messages across different regions.

How does SNS handle SMS opt-outs?

SNS automatically handles standard opt-out keywords (like STOP). Your application should gracefully handle and log these failures, preventing future attempts to opted-out numbers, and potentially update internal user statuses.

Why is E.164 formatting crucial for phone numbers?

E.164 format ensures consistent and reliable SMS delivery globally. It includes the country code, facilitating international messaging and preventing ambiguity in number interpretation by SNS and carriers.

How are long SMS messages handled by SNS?

SNS automatically segments long messages exceeding the standard SMS length (160 characters for GSM-7, 70 for UCS-2) into multiple segments, billed individually. Be aware of encoding and length for cost management.