code examples

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

Developer Guide: Sending MMS with AWS Pinpoint and RedwoodJS

A complete guide to integrating AWS Pinpoint MMS capabilities into a RedwoodJS application, covering AWS setup (IAM, S3, Pinpoint), RedwoodJS service implementation, GraphQL mutations, error handling, and deployment considerations.

This guide provides a complete walkthrough for integrating AWS Pinpoint's MMS capabilities into a RedwoodJS application. We'll cover everything from setting up your AWS resources and RedwoodJS project to implementing the sending logic, handling errors, testing, and deployment. By the end, you'll have a functional service capable of sending text messages with media attachments (images, videos, etc.) via AWS.

Target Audience: Developers familiar with RedwoodJS and basic AWS concepts. Goal: Build a production-ready RedwoodJS service to send MMS messages using AWS Pinpoint and S3.


Project Overview and Goals

Sending rich media content like images or videos alongside text messages (MMS) can significantly enhance user engagement compared to standard SMS. However, integrating MMS sending requires specific cloud infrastructure setup and careful handling of media files.

This guide solves the problem of reliably sending MMS messages from a RedwoodJS backend by leveraging AWS services.

Specific Goals:

  1. Set up necessary AWS resources: IAM permissions, an S3 bucket for media storage, and an AWS Pinpoint origination identity (phone number) capable of sending MMS.
  2. Configure a RedwoodJS project with the AWS SDK v3.
  3. Implement a RedwoodJS service function to send MMS messages using the AWS Pinpoint SMS and Voice v2 API.
  4. Expose this functionality via a GraphQL mutation.
  5. Handle media uploads to S3 (though the upload mechanism itself is out of scope for this guide, we'll assume files are already in S3).
  6. Implement basic error handling and logging.
  7. Provide guidance on testing, security, and deployment.

Technologies Involved:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. Chosen for its integrated structure (API, Web, Prisma) and developer experience features (generators, cells, services).
  • AWS Pinpoint (SMS and Voice v2 API): AWS's communication service, used here specifically for its SendMediaMessage API action to dispatch MMS messages. Chosen for its scalability and integration with other AWS services. Note: Pinpoint MMS sending is primarily available for US and Canadian destinations.
    • Important Limitation: While Pinpoint can send MMS (media), it currently cannot receive inbound media messages (like images sent to your Pinpoint number). It can typically receive inbound text (SMS) replies on MMS-capable numbers if configured.
  • AWS S3 (Simple Storage Service): Used to store the media files (images, videos, PDFs) that will be attached to the MMS messages. Chosen for its durability, availability, and seamless integration with Pinpoint.
  • AWS IAM (Identity and Access Management): Manages permissions, ensuring our RedwoodJS backend has the necessary rights to interact with Pinpoint and S3 securely.
  • AWS SDK for JavaScript v3: The official library for interacting with AWS services from Node.js applications.
  • TypeScript: For type safety and improved developer experience within RedwoodJS.
  • GraphQL: RedwoodJS's default API layer, used to expose the MMS sending functionality.

System Architecture:

(Note: This text-based diagram illustrates the flow. For complex systems, a visual diagram (e.g., using tools like draw.io, Lucidchart, or Mermaid) is recommended.)

text
+-----------------+      +---------------------+      +-----------------------+      +-----------------+
| RedwoodJS       | ---> | RedwoodJS API       | ---> | AWS SDK v3            | ---> | AWS Pinpoint    |
| Frontend (Web)  |      | (GraphQL Mutation)  |      | (Pinpoint Client)     |      | SendMediaMessage|
+-----------------+      +---------------------+      +-----------------------+      +-------+---------+
       ^                       |                                                          |
       | User Interaction        | Calls Service Function                                   | Reads Media
       v                       v                                                          v
+-----------------+      +---------------------+                                +-----------------+
| User            |      | RedwoodJS Service   |                                | AWS S3 Bucket   |
| (Initiates Send)|      | (mmsService.ts)     |                                | (Media Files)   |
+-----------------+      +---------------------+                                +-----------------+
                                   | Uses Credentials
                                   v
                         +-----------------------+
                         | AWS IAM Permissions   |
                         +-----------------------+

Prerequisites:

  • Node.js (LTS version recommended) and Yarn installed.
  • An AWS account with appropriate permissions to create IAM users/roles, S3 buckets, and manage Pinpoint resources.
  • AWS CLI installed and configured locally (for initial setup/verification).
  • Basic understanding of RedwoodJS project structure and services.
  • Familiarity with command-line interfaces.

Expected Outcome:

A RedwoodJS application with a GraphQL mutation sendMms that accepts a destination phone number, message body, and an array of S3 URIs for media files. Calling this mutation will trigger an MMS message send via AWS Pinpoint.


1. Setting up AWS Resources

Before writing any RedwoodJS code, we need the necessary AWS infrastructure.

Step 1: Create an IAM User/Role for RedwoodJS Backend

It's crucial to follow the principle of least privilege. Create an IAM user (or role, if deploying to EC2/ECS/Lambda) specifically for your RedwoodJS application.

  1. Navigate to the IAM console in your AWS account.

  2. Go to ""Users"" and click ""Create user"".

  3. Provide a username (e.g., redwoodjs-mms-app-user).

  4. Choose ""Provide user access to the AWS Management Console"" - Optional. This is only necessary if a human needs to log into the AWS console with this user's credentials. It's not required for the application's programmatic access via access keys.

  5. Select ""Attach policies directly"".

  6. Click ""Create policy"".

  7. Switch to the ""JSON"" editor.

  8. Paste the following policy, replacing YOUR_S3_BUCKET_NAME and YOUR_AWS_REGION:

    json
    {
        ""Version"": ""2012-10-17"",
        ""Statement"": [
            {
                ""Sid"": ""AllowPinpointMMS"",
                ""Effect"": ""Allow"",
                ""Action"": ""sms-voice:SendMediaMessage"",
                ""Resource"": ""*"" // Ideally, restrict this to the ARN of your specific Pinpoint application or origination identity if possible, following the principle of least privilege. Example: ""arn:aws:mobiletargeting:REGION:ACCOUNT_ID:apps/PINPOINT_APP_ID/*"" or similar based on the specific identity type.
            },
            {
                ""Sid"": ""AllowReadFromMMSBucket"",
                ""Effect"": ""Allow"",
                ""Action"": ""s3:GetObject"",
                ""Resource"": ""arn:aws:s3:::YOUR_S3_BUCKET_NAME/*""
            },
            {
               ""Sid"": ""AllowListMMSBucket"",
                ""Effect"": ""Allow"",
                ""Action"": ""s3:ListBucket"",
                ""Resource"": ""arn:aws:s3:::YOUR_S3_BUCKET_NAME"",
                 ""Condition"": {
                    ""StringLike"": {
                       ""s3:prefix"": [
                           ""*""
                         ]
                     }
                 }
            }
        ]
    }
    • Explanation:
      • sms-voice:SendMediaMessage: Allows sending MMS via the Pinpoint SMS/Voice v2 API.
      • s3:GetObject: Allows reading the media files from your designated S3 bucket. Pinpoint needs this to fetch the media.
      • s3:ListBucket: While not strictly required by SendMediaMessage itself, it's often useful for debugging or listing objects if your app needs it. You can remove it if only GetObject is needed. Ensure YOUR_S3_BUCKET_NAME is correctly replaced.
  9. Click ""Next"".

  10. Give the policy a name (e.g., RedwoodJSMMSSendingPolicy).

  11. Click ""Create policy"".

  12. Go back to the user creation tab, refresh the policy list, and select the RedwoodJSMMSSendingPolicy you just created.

  13. Click ""Next"".

  14. Review and click ""Create user"".

  15. Crucially: Navigate to the newly created user, go to the ""Security credentials"" tab, and under ""Access keys"", click ""Create access key"".

  16. Select ""Application running outside AWS"" (or the appropriate option).

  17. Click ""Next"". Add a description tag if desired.

  18. Click ""Create access key"".

  19. Immediately copy the ""Access key ID"" and ""Secret access key"". You won't be able to see the secret key again. Store these securely. We'll use them in our RedwoodJS environment variables later.

Step 2: Create an S3 Bucket for Media Files

Pinpoint needs access to your media files stored in S3.

  1. Navigate to the S3 console.
  2. Click ""Create bucket"".
  3. Enter a globally unique bucket name (e.g., your-company-mms-media-unique-id). Remember this name.
  4. Select the same AWS Region where you plan to register your Pinpoint phone number. This is mandatory. MMS sending requires the S3 bucket and the Pinpoint originator to be in the same region.
  5. Block Public Access: Keep ""Block all public access"" checked. We are granting access via the IAM policy, which is more secure.
  6. Leave other settings as default unless you have specific requirements (versioning, encryption, etc.).
  7. Click ""Create bucket"".

Step 3: Obtain an MMS-Capable Pinpoint Origination Identity

You need a phone number (Short Code, Toll-Free Number, or 10DLC for the US; Long Code or Short Code for Canada) registered in Pinpoint and enabled for MMS.

  1. Navigate to the Pinpoint console. Switch to the same AWS Region used for your S3 bucket.
  2. In the left navigation, under ""SMS and voice"", click ""Phone numbers"".
  3. Click ""Request phone number"".
  4. Select the target country (US or Canada for MMS).
  5. Choose the number type (Toll-Free, 10DLC, Short Code). Toll-Free numbers often have the simplest setup for testing, but 10DLC is standard for application-to-person (A2P) messaging in the US. Short codes require a separate, more involved application process.
  6. Follow the on-screen instructions for registration. This may involve providing business information and use case details, especially for 10DLC and Short Codes. Approval times vary.
  7. Verification: Once the number is provisioned and Active, check its capabilities. Click on the phone number in the Pinpoint console. Look for ""MMS"" listed under ""Capabilities"". If it only shows ""SMS"", the number cannot send MMS, and you might need to request a new one specifically ensuring MMS support (most numbers requested after May 2024 should include it automatically if available for the type/region).
  8. Note down the full E.164 formatted phone number (e.g., +12065550100). This is your OriginationIdentity.

2. Setting up the RedwoodJS Project

Now, let's configure our RedwoodJS application.

Step 1: Create or Use Existing RedwoodJS App

If you don't have one, create a new RedwoodJS project:

bash
yarn create redwood-app ./redwood-mms-app --typescript
cd redwood-mms-app

Step 2: Install AWS SDK v3

We need the specific client for the Pinpoint SMS and Voice v2 API.

bash
# Navigate to the API side
cd api

# Install the AWS SDK v3 client for Pinpoint SMS/Voice
yarn add @aws-sdk/client-pinpoint-sms-voice-v2

# Navigate back to the project root (optional)
cd ..

Step 3: Configure Environment Variables

Store your AWS credentials and configuration securely. Create or edit the .env file in the root of your RedwoodJS project. Never commit this file to version control if it contains secrets. Use a .env.example and .gitignore.

dotenv
# .env

# AWS Credentials for MMS Sending
# Replace with the keys generated in AWS Setup Step 1
AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY="YOUR_SECRET_ACCESS_KEY"

# AWS Region where Pinpoint number and S3 bucket reside
# Example: us-east-1
AWS_REGION="YOUR_AWS_REGION"

# Pinpoint Origination Identity (MMS-capable phone number)
# Example: +12065550100
PINPOINT_ORIGINATION_NUMBER="YOUR_PINPOINT_PHONE_NUMBER_E164"

# S3 Bucket Name for Media Files
# Example: your-company-mms-media-unique-id
S3_MMS_BUCKET_NAME="YOUR_S3_BUCKET_NAME"
  • Explanation:
    • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: Credentials for the IAM user created earlier. The SDK will automatically pick these up.
    • AWS_REGION: Essential for the SDK to target the correct AWS region where your Pinpoint number and S3 bucket exist.
    • PINPOINT_ORIGINATION_NUMBER: The E.164 formatted phone number registered in Pinpoint that will send the MMS.
    • S3_MMS_BUCKET_NAME: The name of the S3 bucket created for media storage.

Important: Ensure your root .gitignore file includes .env.


3. Implementing Core Functionality (RedwoodJS Service)

We'll create a RedwoodJS service to encapsulate the logic for sending MMS messages.

Step 1: Generate the Service

Use the RedwoodJS CLI to generate a service for MMS operations.

bash
yarn rw g service mms

This creates api/src/services/mms/mms.ts and api/src/services/mms/mms.test.ts.

Step 2: Implement the sendMms Service Function

Open api/src/services/mms/mms.ts and add the following code:

typescript
// api/src/services/mms/mms.ts

import {
  PinpointSMSVoiceV2Client,
  SendMediaMessageCommand,
  SendMediaMessageCommandInput,
  SendMediaMessageCommandOutput,
} from '@aws-sdk/client-pinpoint-sms-voice-v2'

import { logger } from 'src/lib/logger' // Redwood's built-in logger

// Initialize the Pinpoint client
// The SDK automatically picks up credentials and region from environment variables
// (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)
const pinpointClient = new PinpointSMSVoiceV2Client({})

interface SendMmsArgs {
  destinationPhoneNumber: string // E.164 format (e.g., +14155550101)
  mediaUrls?: string[] // Array of S3 URIs (e.g., s3://bucket-name/image.jpg)
  messageBody?: string // The text part of the message
}

export const sendMms = async ({
  destinationPhoneNumber,
  mediaUrls,
  messageBody,
}: SendMmsArgs): Promise<SendMediaMessageCommandOutput> => {
  // Validate essential inputs
  if (!destinationPhoneNumber) {
    throw new Error('Destination phone number is required.')
  }
  if (!mediaUrls?.length && !messageBody) {
    throw new Error('Either mediaUrls or messageBody must be provided.')
  }

  // Retrieve configuration from environment variables
  const originationNumber = process.env.PINPOINT_ORIGINATION_NUMBER
  const s3BucketName = process.env.S3_MMS_BUCKET_NAME // Used for validation

  if (!originationNumber) {
    logger.error('PINPOINT_ORIGINATION_NUMBER environment variable is not set.')
    throw new Error('MMS sending configuration is incomplete.')
  }
  if (!s3BucketName) {
    logger.error('S3_MMS_BUCKET_NAME environment variable is not set.')
    throw new Error('MMS sending configuration is incomplete.')
  }

  // Basic validation for S3 URIs (recommended)
  if (mediaUrls) {
    for (const url of mediaUrls) {
      if (!url.startsWith(`s3://${s3BucketName}/`)) {
        logger.error(
          { url, expectedBucket: s3BucketName },
          'Provided media URL does not match the configured S3 bucket or format.'
        )
        // For production readiness, throw an error for invalid S3 URLs
        throw new Error(`Invalid S3 URL format or bucket: ${url}`);
      }
    }
  }

  const commandInput: SendMediaMessageCommandInput = {
    DestinationPhoneNumber: destinationPhoneNumber,
    OriginationIdentity: originationNumber,
    MessageBody: messageBody,
    MediaUrls: mediaUrls,
    // ConfigurationSetName: 'YourOptionalConfigurationSet', // Optional: For event streaming
  }

  logger.info({ commandInput }, 'Attempting to send MMS via Pinpoint')

  try {
    const command = new SendMediaMessageCommand(commandInput)
    const response = await pinpointClient.send(command)

    logger.info({ response }, 'Successfully sent MMS command to Pinpoint')

    // The response contains a MessageId if the request was accepted by Pinpoint.
    // This *does not* guarantee delivery to the handset.
    // Delivery status tracking requires setting up event streaming (e.g., via SNS or Kinesis).
    if (!response.MessageId) {
      logger.warn({ response }, 'Pinpoint accepted the request but did not return a MessageId.')
    }

    return response
  } catch (error) {
    logger.error({ error, commandInput }, 'Failed to send MMS via Pinpoint')

    // Re-throw the error so it can be handled by the GraphQL layer or calling function
    // You might want to wrap specific AWS errors for cleaner handling upstream
    throw new Error(`MMS sending failed: ${error.message || 'Unknown AWS error'}`)
    // Consider checking error.name for specific AWS exceptions like
    // 'ValidationException', 'AccessDeniedException', 'ResourceNotFoundException', etc.
  }
}

// You can add other MMS related functions here if needed
// export const getMmsStatus = async (messageId: string) => { ... } // Requires event streaming setup
  • Explanation:
    • We import the necessary client and command from the AWS SDK v3.
    • The PinpointSMSVoiceV2Client is instantiated. It automatically finds credentials (via environment variables, EC2 instance profile, etc.) and the region (AWS_REGION).
    • The sendMms function takes the destination number, an optional array of S3 URIs (s3://bucket/object.ext), and an optional message body.
    • It performs basic input validation and retrieves necessary config from .env.
    • It includes S3 URI validation that throws an error if the format or bucket is incorrect.
    • It constructs the SendMediaMessageCommandInput object, mapping our arguments to the API parameters.
      • OriginationIdentity is set to our configured sending number.
      • MediaUrls expects an array of strings in the format s3://YOUR_BUCKET_NAME/path/to/your/file.jpg. Pinpoint fetches these files.
    • The pinpointClient.send() method dispatches the command to AWS.
    • Basic logging is included using Redwood's built-in logger.
    • Error handling uses a try...catch block, logs the error, and re-throws it.
    • The successful response from AWS includes a MessageId, which confirms AWS accepted the request, not that the message was delivered.

4. Building a Complete API Layer (GraphQL Mutation)

Let's expose the sendMms service function through Redwood's GraphQL API.

Step 1: Define the GraphQL Schema

Open or create api/src/graphql/mms.sdl.ts:

typescript
// api/src/graphql/mms.sdl.ts

export const schema = gql`
  type MmsResponse {
    messageId: String
    # Add other relevant fields from SendMediaMessageCommandOutput if needed
    # Consider adding fields like 'Message' (containing status info) if useful,
    # though detailed status often requires event streaming setup.
  }

  type Mutation {
    """
    Sends an MMS message with optional media attachments via AWS Pinpoint.
    Requires destinationPhoneNumber and at least one of mediaUrls or messageBody.
    """
    sendMms(
      destinationPhoneNumber: String!
      mediaUrls: [String!]
      messageBody: String
    ): MmsResponse! @requireAuth # Or @skipAuth depending on your needs
  }
`
  • Explanation:
    • We define a MmsResponse type to represent the data returned (primarily the MessageId). Added a comment about potentially including other fields.
    • We define a sendMms mutation within the Mutation type.
    • It accepts destinationPhoneNumber, mediaUrls (an array of strings), and messageBody as arguments. ! denotes required fields.
    • @requireAuth ensures only authenticated users can call this mutation. Change to @skipAuth if authentication is not needed for this specific action, but be cautious about potential abuse.

Step 2: Link Service to GraphQL (Implicit)

RedwoodJS automatically maps GraphQL resolvers to service functions based on naming conventions. Since our mutation is named sendMms and our service file (api/src/services/mms/mms.ts) exports a function named sendMms, Redwood handles the connection. No explicit resolver code is needed in api/src/functions/graphql.ts unless you need custom logic before or after calling the service.

Step 3: Testing the Mutation

  1. Ensure you have uploaded a test file (e.g., test.jpg) to your S3 bucket (s3://YOUR_S3_BUCKET_NAME/test.jpg). Make sure the IAM user has s3:GetObject permission for this file.

  2. Start your RedwoodJS development server: yarn rw dev.

  3. Navigate to the GraphQL Playground, usually http://localhost:8911/graphql.

  4. If using @requireAuth, ensure you configure the Authorization: Bearer <token> header appropriately based on your auth setup.

  5. Execute the following mutation (replace placeholders):

    graphql
    mutation SendTestMms {
      sendMms(
        destinationPhoneNumber: "+1YOUR_TEST_RECIPIENT_NUMBER" # E.164 format
        messageBody: "Hello from RedwoodJS MMS! Check the image."
        mediaUrls: ["s3://YOUR_S3_BUCKET_NAME/test.jpg"]
      ) {
        messageId
      }
    }
  6. Check the response in the Playground. You should see a messageId if successful.

  7. Check the recipient phone; the MMS message should arrive shortly.

  8. Check the API server logs (yarn rw dev console output) for log messages from the service.

Example cURL Request:

bash
curl 'http://localhost:8911/graphql' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_AUTH_TOKEN' \ # Include ONLY if using @requireAuth
  --data-binary '{"query":"mutation SendTestMms($dest: String!, $body: String, $media: [String!]) {\n  sendMms(destinationPhoneNumber: $dest, messageBody: $body, mediaUrls: $media) {\n    messageId\n  }\n}","variables":{"dest":"+1YOUR_TEST_RECIPIENT_NUMBER","body":"Hello via cURL MMS!","media":["s3://YOUR_S3_BUCKET_NAME/test.jpg"]}}' \
  --compressed

Example JSON Response:

json
{
  "data": {
    "sendMms": {
      "messageId": "pinpoint-message-id-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  }
}

5. Integrating with Necessary Third-Party Services (AWS)

This section summarizes the critical integration points covered in the setup sections.

  • AWS Pinpoint:
    • Configuration: Requires an active, MMS-capable Origination Identity (phone number) in the same region as the S3 bucket.
    • Credentials: Handled via IAM User Access Key ID and Secret Access Key stored in .env (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
    • Region: Specified in .env (AWS_REGION).
    • Integration: Via @aws-sdk/client-pinpoint-sms-voice-v2 in the RedwoodJS service.
  • AWS S3:
    • Configuration: Requires an S3 bucket in the same region as the Pinpoint Origination Identity. Bucket access should not be public.
    • Credentials: Access granted via the IAM policy attached to the IAM user (s3:GetObject). The same AWS keys are used.
    • Integration: Media files are referenced by their S3 URI (s3://BUCKET_NAME/OBJECT_KEY) in the MediaUrls parameter of the SendMediaMessageCommand.
  • AWS IAM:
    • Configuration: A dedicated IAM user/role with a least-privilege policy allowing sms-voice:SendMediaMessage and s3:GetObject on the specific bucket.
    • Credentials: Access Key ID and Secret stored securely.
    • Integration: The AWS SDK automatically uses these credentials to sign API requests.

Fallback Mechanisms: AWS services are generally reliable, but for critical messaging:

  • Consider implementing retry logic (see Section 6).
  • Monitor AWS Health Dashboard for service outages.
  • For extreme high availability, you might consider multi-region setups or alternative providers, but this significantly increases complexity.

6. Implementing Error Handling, Logging, and Retry Mechanisms

Robust applications need proper error handling.

Error Handling Strategy:

  • Service Layer: The sendMms service function includes a try...catch block. It catches errors from the AWS SDK, logs detailed information (including the input parameters and the error itself), and then re-throws a generic error. This prevents leaking sensitive AWS error details directly to the client while still signaling failure.
  • GraphQL Layer: RedwoodJS automatically handles errors thrown from services. By default, it will return a GraphQL error response to the client, including the message from the thrown error. You can customize this behavior using Redwood's error handling capabilities if needed.
  • Specific AWS Errors: You can enhance the catch block in the service to check for specific AWS error codes (e.g., error.name === 'ValidationException') to provide more context or potentially trigger different actions (like alerting on AccessDeniedException).

Logging:

  • We used RedwoodJS's built-in logger (import { logger } from 'src/lib/logger').
  • Log Levels:
    • logger.info: Used for successful operations or key steps (e.g., attempting send, successful send).
    • logger.warn: Used for potential issues that don't stop the process (e.g., missing MessageId in response).
    • logger.error: Used for critical failures (e.g., failed AWS SDK call, invalid S3 URL).
  • Log Format: Redwood's default logger provides structured JSON logging, which is ideal for log aggregation tools (like Datadog, Logtail, AWS CloudWatch Logs). Include relevant context (like commandInput or response) in the log objects.

Retry Mechanisms:

Network issues or temporary AWS glitches can occur. Implementing retries can improve reliability.

typescript
// Example modification within api/src/services/mms/mms.ts

// Simple retry function (consider using a library like 'async-retry' for more features)
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function retryOperation<T>(
  operation: () => Promise<T>,
  maxRetries = 3,
  delayMs = 500, // Initial delay
  backoffFactor = 2
): Promise<T> {
  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      return await operation();
    } catch (error) {
      attempt++;
      if (attempt >= maxRetries) {
        logger.error({ error, attempt }, `Operation failed after ${maxRetries} attempts.`);
        throw error; // Rethrow after final attempt
      }
      // Check if error is retryable (e.g., network errors, throttling)
      // Avoid retrying on validation errors or permission errors.
      // **IMPORTANT CAVEAT:** This check is very basic. Developers MUST carefully
      // identify which specific AWS SDK v3 errors (by name or code) are genuinely
      // transient and safe to retry for Pinpoint's SendMediaMessage API.
      // Consult AWS documentation on error handling and retries for the specific service.
      const isRetryable = error.name === 'ThrottlingException' || error.message.includes('Network Error'); // Example checks - **Refine this based on AWS docs and testing!**
      if (!isRetryable) {
          logger.error({ error, attempt }, `Non-retryable error encountered. Failing operation.`); // Added more info
          throw error;
      }

      const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
      logger.warn({ error, attempt, waitTime }, `Attempt ${attempt} failed. Retrying in ${waitTime}ms...`);
      await sleep(waitTime);
    }
  }
  // Should not be reached if maxRetries > 0, but satisfies TypeScript
  throw new Error('Retry logic failed unexpectedly.');
}


// --- Inside the sendMms function ---
// Replace the direct call:
// try {
//   const command = new SendMediaMessageCommand(commandInput);
//   const response = await pinpointClient.send(command);
// ...
// With the retry logic wrapped around the send command inside the try block:
  try {
    const response = await retryOperation(async () => {
      const command = new SendMediaMessageCommand(commandInput);
      return await pinpointClient.send(command);
    });
// --- Rest of the try...catch block ---
    logger.info({ response }, 'Successfully sent MMS command to Pinpoint')
    // ... (rest of the success handling) ...
    return response
  } catch (error) {
    // ... (existing error logging and re-throw) ...
    logger.error({ error, commandInput }, 'Failed to send MMS via Pinpoint')
    throw new Error(`MMS sending failed: ${error.message || 'Unknown AWS error'}`)
  }
// } // This closing brace was missing in the original snippet, assuming it belongs here to close the sendMms function
  • Explanation:
    • A basic retryOperation helper is added.
    • It takes the operation (the pinpointClient.send call) and retry parameters.
    • It uses exponential backoff (delayMs * Math.pow(backoffFactor, attempt - 1)) to avoid overwhelming the service.
    • Crucially: It includes a basic check (isRetryable) with a strong caveat that this check must be refined based on AWS documentation and testing to only retry genuinely transient errors. Do not retry on errors like ValidationException or AccessDeniedException.
    • Libraries like async-retry offer more sophisticated features (jitter, custom retry conditions).

Testing Error Scenarios:

  • Mocking: In unit tests, configure the AWS SDK mock to throw specific exceptions (ValidationException, ThrottlingException, AccessDeniedException) and verify that your error handling and retry logic behave as expected.
  • Manual: Temporarily revoke IAM permissions, provide invalid phone numbers, or incorrect S3 URIs to trigger real errors during development testing.

7. Database Schema and Data Layer

While not strictly required just to send an MMS, a real-world application would likely store information about sent messages.

Potential Schema (using Prisma in schema.prisma):

prisma
// schema.prisma

model MmsMessage {
  id                   String    @id @default(cuid())
  createdAt            DateTime  @default(now())
  updatedAt            DateTime  @updatedAt

  destinationPhoneNumber String
  originationIdentity  String
  messageBody          String?
  mediaUrls            String[]  // Array of S3 URIs sent
  status               MmsStatus @default(PENDING) // PENDING, SENT_TO_PROVIDER, DELIVERED, FAILED, etc.
  providerMessageId    String?   @unique // The MessageId from Pinpoint response
  errorMessage         String?   // Store error details if sending failed initially

  // Optional: Link to a User or Contact model
  // userId             String?
  // user               User?     @relation(fields: [userId], references: [id])
}

enum MmsStatus {
  PENDING          // Request initiated in our system
  SENT_TO_PROVIDER // Request accepted by AWS Pinpoint (got MessageId)
  DELIVERED        // Confirmed delivery (Requires Event Streaming setup)
  FAILED           // Sending failed (either initial API call or later delivery failure)
  // Add other relevant statuses
}

Implementation:

  1. Add the model to your schema.prisma.
  2. Run yarn rw prisma migrate dev to apply the changes to your database.
  3. Modify the sendMms service function:
    • Create an MmsMessage record in the database before calling pinpointClient.send(), setting the status to PENDING.
    • If the pinpointClient.send() call (or retry operation) is successful, update the record with the providerMessageId and set the status to SENT_TO_PROVIDER.
    • If the call fails, update the record with the errorMessage and set the status to FAILED.
typescript
// Example snippet within api/src/services/mms/mms.ts
import { db } from 'src/lib/db' // Import Redwood's Prisma client

// ... inside sendMms, before the try block ...
let dbMessage;
try {
   dbMessage = await db.mmsMessage.create({
    data: {
      destinationPhoneNumber: destinationPhoneNumber,
      originationIdentity: originationNumber, // Ensure this is available here
      messageBody: messageBody,
      mediaUrls: mediaUrls,
      status: 'PENDING', // Assuming MmsStatus enum is available or use string 'PENDING'
    },
  });

  // ... existing try block containing the pinpointClient.send call ...
  // Inside the success path of the try block (after getting response):
  await db.mmsMessage.update({
    where: { id: dbMessage.id },
    data: {
      providerMessageId: response.MessageId,
      status: 'SENT_TO_PROVIDER', // Assuming MmsStatus enum or use string
    },
  });

  // Inside the catch block:
  if (dbMessage) { // Check if initial create succeeded
    await db.mmsMessage.update({
      where: { id: dbMessage.id },
      data: {
        status: 'FAILED', // Assuming MmsStatus enum or use string
        errorMessage: error.message || 'Unknown AWS error',
      },
    });
  }
  // ... rest of catch block (logging, re-throwing error) ...

} catch (error) { // This outer catch handles the initial DB create error
   logger.error({ error }, 'Failed to create initial MmsMessage record in DB');
   // Decide how to handle this - maybe throw, maybe return specific error
   throw new Error(`Database operation failed: ${error.message}`);
}

Frequently Asked Questions

How to send MMS messages with RedwoodJS?

You can send MMS messages with RedwoodJS by integrating with AWS Pinpoint and S3. This involves setting up AWS resources (IAM user, S3 bucket, Pinpoint number), installing the AWS SDK v3, configuring environment variables, implementing a RedwoodJS service function, and exposing the functionality via a GraphQL mutation. The service function interacts with the Pinpoint API to send messages, using S3 to store media attachments.

What is AWS Pinpoint used for in MMS sending?

AWS Pinpoint is used for sending the actual MMS messages. Its `SendMediaMessage` API action, part of the SMS and Voice v2 API, handles message dispatch. Pinpoint integrates with S3 for media handling and requires specific IAM permissions for secure access. Note: Pinpoint's primary MMS support is for US and Canadian destinations.

Why does MMS sending require an S3 bucket?

MMS messages include media attachments (images, videos, etc.). An S3 bucket is used to store these media files, which AWS Pinpoint then accesses and includes when sending the MMS. The S3 bucket and your Pinpoint origination number *must* be in the same AWS region.

When should I use a Toll-Free number for MMS with Pinpoint?

Toll-free numbers are often easiest for initial testing and development with Pinpoint. However, for production application-to-person (A2P) messaging in the US, 10DLC (10-digit long code) is the standard and generally recommended for better deliverability and compliance. Short codes require a more complex application process.

Can I receive MMS media replies with AWS Pinpoint?

No, AWS Pinpoint cannot receive inbound MMS media (like images sent to your Pinpoint number). While it can typically receive inbound *text* (SMS) replies, if the number and Pinpoint are configured to do so, it does not support receiving media attachments in replies.

How to set up IAM permissions for RedwoodJS MMS sending?

Create a dedicated IAM user or role with a least-privilege policy. This policy should grant `sms-voice:SendMediaMessage` permission for Pinpoint and `s3:GetObject` (and optionally `s3:ListBucket`) permission for your S3 bucket containing the media files. Restricting permissions enhances security.

What AWS SDK version is used for Pinpoint MMS?

The AWS SDK for JavaScript v3 is used with the specific client package `@aws-sdk/client-pinpoint-sms-voice-v2`. This provides the necessary functions and types to interact with the Pinpoint SMS and Voice v2 API. Install it with: `yarn add @aws-sdk/client-pinpoint-sms-voice-v2` within the `api` side of your redwood project.

Why does the S3 bucket need to be in the same region as Pinpoint?

AWS Pinpoint needs to access the media files stored in your S3 bucket to include them in the MMS message. For efficiency and to avoid cross-region data transfer costs, these resources must be in the same AWS region. This is a mandatory requirement for MMS sending.

What is the format for media URLs in Pinpoint's SendMediaMessage?

Media URLs must be S3 URIs in the format `s3://YOUR_BUCKET_NAME/path/to/your/file.jpg`. Ensure your IAM user has `s3:GetObject` permission for the specified objects in the bucket. The bucket must be in the same region as your Pinpoint origination number.

How to handle errors when sending MMS with Pinpoint?

Use a `try...catch` block in your service function. Catch errors from the AWS SDK, log details using `logger.error`, and re-throw a more generic error to the GraphQL layer. You can check for specific AWS error types (e.g., `ValidationException`) to refine handling.

What does the MessageId in the Pinpoint response indicate?

The `MessageId` in the Pinpoint `SendMediaMessage` response only confirms that AWS Pinpoint has *accepted* the send request. It does *not* guarantee message delivery to the handset. Tracking delivery requires setting up event streaming (e.g., using SNS or Kinesis).

How to implement retry logic for MMS sending?

Use a retry mechanism (a simple loop or library like `async-retry`) to handle transient errors like network issues or throttling. Include exponential backoff and ensure you *only* retry on appropriate, transient errors. Avoid retrying on validation or permission errors.

Where should AWS credentials be stored in RedwoodJS?

Store AWS credentials securely in a `.env` file in the root of your RedwoodJS project. This file should *never* be committed to version control. Use environment variables like `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.

What RedwoodJS service is used for MMS?

The RedwoodJS service, typically named `mmsService` (or similar) and located at `api/src/services/mms/mms.ts`, encapsulates all logic for sending MMS messages using the AWS SDK and handling responses or errors. The service function is automatically linked to your GraphQL mutation if you follow Redwood naming conventions.