code examples

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

Build an AWS SNS Marketing Campaign App with RedwoodJS

A step-by-step guide to building a full-stack RedwoodJS application for managing SMS marketing campaigns using AWS Simple Notification Service (SNS).

This guide provides a step-by-step walkthrough for building a full-stack application using RedwoodJS that enables you to manage marketing campaigns and send SMS messages to subscribers via AWS Simple Notification Service (SNS).

We will build a simple web interface to manage lists of subscribers (names and phone numbers) and campaigns (names and message templates). You'll be able to trigger a campaign, which sends the specified message to all subscribed phone numbers using AWS SNS. This provides a foundation for a scalable SMS marketing platform.

Technologies Used:

  • RedwoodJS: A full-stack, serverless-friendly JavaScript/TypeScript framework built on React, GraphQL, and Prisma. Chosen for its integrated structure, developer experience, and ease of deployment.
  • AWS SNS: A managed messaging service for sending messages (SMS, email, push notifications) at scale. Chosen for its reliability, scalability, and direct integration capabilities.
  • AWS SDK for JavaScript v3: Used within the RedwoodJS API side to interact with the AWS SNS service.
  • Prisma: A next-generation ORM for Node.js and TypeScript, Redwood's default data access layer.
  • GraphQL: The query language for your API, facilitated by Redwood's structure.
  • React: For building the frontend user interface.
  • Serverless Framework: Used by Redwood's setup command for deploying to AWS Lambda and S3/CloudFront (Note: See deployment section regarding deprecation status).

System Architecture:

text
+-----------------+      +---------------------+      +-----------------+      +-----------------+
|  User Browser   | ---> | Redwood Web (React) | ---> | Redwood API     | ---> |   AWS SNS       |
| (React UI)      |      | (Hosted on S3/CF)   |      | (GraphQL/Lambda)|      | (Sends SMS)     |
+-----------------+      +---------------------+      +--------+--------+      +--------+--------+
       ^                          ^                            |                       |
       |                          |                            |                       |
       |                          +----------------------------+                       |
       |                                    API Requests (GraphQL)                     |
       |                                                                              |
       +------------------------------------------------------------------------------+
                                       Database Interaction
                                               |
                                               v
                                       +-----------------+
                                       | Database (e.g., |
                                       |  PostgreSQL)    |
                                       | (Managed by Prisma)|
                                       +-----------------+

Prerequisites:

  • Node.js (>=18.x recommended)
  • Yarn (Classic v1 or Berry v3+, check RedwoodJS docs for current recommendations)
  • An AWS Account
  • AWS CLI installed and configured (aws configure)

By the end of this guide, you will have a deployed RedwoodJS application capable of managing subscribers and sending SMS campaigns via AWS SNS.

1. Project Setup and Configuration

Let's initialize our RedwoodJS project and set up the necessary configurations for AWS deployment.

  1. Create RedwoodJS App: Open your terminal and run:

    bash
    yarn create redwood-app redwood-sns-campaigns
    cd redwood-sns-campaigns

    Follow the prompts (choosing TypeScript is recommended).

  2. Configure AWS CLI: If you haven't already, configure your AWS CLI with credentials that have permissions for SNS, Lambda, S3, CloudFront, and API Gateway. Running aws configure will prompt you for your Access Key ID, Secret Access Key, default region, and output format.

    bash
    aws configure
    # Follow prompts:
    # AWS Access Key ID [None]: YOUR_ACCESS_KEY_ID
    # AWS Secret Access Key [None]: YOUR_SECRET_ACCESS_KEY
    # Default region name [None]: us-east-1  # Or your preferred region
    # Default output format [None]: json

    Why this region? Choose a region physically close to you or your users, and ensure SNS SMS support is available there. us-east-1 is a common default.

  3. Setup Serverless Deployment: RedwoodJS provides a setup command to configure deployment using the Serverless Framework for AWS.

    bash
    yarn rw setup deploy serverless

    This command adds serverless.yml configuration files to your api and web directories and installs necessary dependencies.

    Note on Deprecation: As mentioned in the RedwoodJS v5+ documentation, this Serverless Framework deployment method is deprecated. While it still functions as of this writing, RedwoodJS is exploring alternative AWS deployment options. For production systems requiring long-term support, monitor RedwoodJS announcements for updated deployment strategies.

  4. Configure AWS Credentials in .env: The Serverless deployment needs your AWS credentials. Add them to your .env file at the project root. Never commit your .env file to version control. Redwood adds .env to .gitignore by default.

    plaintext
    # .env
    
    # Add these lines:
    AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID_HERE
    AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY_HERE
    AWS_REGION=us-east-1 # Use the same region as configured in AWS CLI
    • AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY: Obtain these from AWS IAM when creating a user with programmatic access. Security Best Practice: Grant this user the least privilege necessary. While broader permissions like AmazonS3FullAccess, CloudFrontFullAccess, AWSLambda_FullAccess, AmazonAPIGatewayAdministrator, and AmazonSNSFullAccess might be simpler for initial setup, they are less secure. For this specific application's core function, the minimum required permission for sending messages is sns:Publish. You will need broader permissions for the deployment itself (handled by Serverless Framework using these credentials or potentially an IAM role), but the running Lambda function ideally only needs sns:Publish (see Section 6). Start with broader permissions if needed during setup, but plan to restrict them significantly for production.
    • AWS_REGION: The AWS region where your services (Lambda, SNS) will operate. Must match the region used during aws configure.

2. Database Schema and Data Layer

We'll use Prisma to define our database models for Campaigns and Subscribers.

  1. Define Prisma Schema: Open api/db/schema.prisma and define the models:

    prisma
    // api/db/schema.prisma
    
    datasource db {
      provider = ""postgresql"" // Or ""sqlite"", ""mysql""
      url      = env(""DATABASE_URL"")
    }
    
    generator client {
      provider      = ""prisma-client-js""
      binaryTargets = ""native""
    }
    
    model Campaign {
      id        Int      @id @default(autoincrement())
      name      String   @unique
      message   String
      createdAt DateTime @default(now())
    }
    
    model Subscriber {
      id          Int      @id @default(autoincrement())
      name        String?
      phoneNumber String   @unique // Ensure phone numbers are unique
      subscribed  Boolean  @default(true)
      createdAt   DateTime @default(now())
    }

    Why these models? Campaign stores the message content, and Subscriber stores contact information. phoneNumber is unique to avoid duplicates. subscribed allows for opt-outs later.

  2. Set Database URL: Ensure your DATABASE_URL in .env points to your development database (e.g., postgresql://user:password@localhost:5432/mydb). For SQLite (default), it's usually file:./dev.db.

  3. Run Migrations: Apply the schema changes to your database and generate the Prisma client.

    bash
    yarn rw prisma migrate dev

    Enter a name for the migration when prompted (e.g., create_campaigns_subscribers).

3. API Layer (GraphQL)

Now, let's build the GraphQL API endpoints to manage campaigns and subscribers, including the logic to send campaigns.

  1. Generate GraphQL Scaffolding: Use Redwood's generators to create SDL (Schema Definition Language) files and services.

    bash
    yarn rw g sdl Campaign --crud
    yarn rw g sdl Subscriber --crud

    This creates basic CRUD (Create, Read, Update, Delete) operations for both models.

  2. Implement Campaign Sending Logic: We need a mutation to trigger sending a specific campaign to all active subscribers.

    • Install AWS SDK for SNS: Add the SNS client package to your API workspace.

      bash
      yarn workspace api add @aws-sdk/client-sns
    • Define the Mutation SDL: Add the sendCampaign mutation to api/src/graphql/campaigns.sdl.ts.

      graphql
      # api/src/graphql/campaigns.sdl.ts
      # Note: Redwood uses gql tagged template literals in TS files,
      # but the content itself is GraphQL SDL.
      
      type Campaign {
        id: Int!
        name: String!
        message: String!
        createdAt: DateTime!
      }
      
      type Query {
        campaigns: [Campaign!]! @requireAuth
        campaign(id: Int!): Campaign @requireAuth
      }
      
      input CreateCampaignInput {
        name: String!
        message: String!
      }
      
      input UpdateCampaignInput {
        name: String
        message: String
      }
      
      # Result type for the send operation
      type SendCampaignResult {
        successCount: Int!
        failureCount: Int!
        message: String!
        failedNumbers: [String!]
      }
      
      type Mutation {
        createCampaign(input: CreateCampaignInput!): Campaign! @requireAuth
        updateCampaign(id: Int!, input: UpdateCampaignInput!): Campaign! @requireAuth
        deleteCampaign(id: Int!): Campaign! @requireAuth
      
        # Add this mutation
        sendCampaign(id: Int!): SendCampaignResult! @requireAuth(roles: [""admin""]) # Example: Restrict access
      }

      Why SendCampaignResult? It provides feedback to the user about the outcome of the send operation. Why @requireAuth(roles: [""admin""])? Sending campaigns often has costs and impact, so restricting it to specific user roles (once authentication is added) is crucial. We'll use basic @requireAuth for now.

    • Implement the Service Logic: Add the sendCampaign function to api/src/services/campaigns/campaigns.ts.

      typescript
      // api/src/services/campaigns/campaigns.ts
      import { SNSClient, PublishCommand } from '@aws-sdk/client-sns'
      import type {
        QueryResolvers,
        MutationResolvers,
        CampaignResolvers,
      } from 'types/graphql'
      
      import { db } from 'src/lib/db'
      import { logger } from 'src/lib/logger' // Import Redwood logger
      import { requireAuth } from 'src/lib/auth' // Ensure user is logged in
      
      // Initialize SNS Client - Reads region and credentials from env vars automatically
      // Ensure AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY are in .env
      const snsClient = new SNSClient({})
      
      export const campaigns: QueryResolvers['campaigns'] = () => {
        return db.campaign.findMany()
      }
      
      export const campaign: QueryResolvers['campaign'] = ({ id }) => {
        return db.campaign.findUnique({
          where: { id },
        })
      }
      
      export const createCampaign: MutationResolvers['createCampaign'] = ({ input }) => {
        return db.campaign.create({
          data: input,
        })
      }
      
      export const updateCampaign: MutationResolvers['updateCampaign'] = ({ id, input }) => {
        return db.campaign.update({
          data: input,
          where: { id },
        })
      }
      
      export const deleteCampaign: MutationResolvers['deleteCampaign'] = ({ id }) => {
        return db.campaign.delete({
          where: { id },
        })
      }
      
      // Add this function
      export const sendCampaign: MutationResolvers['sendCampaign'] = async ({ id }) => {
        requireAuth({ roles: ['admin'] }) // Or adjust roles as needed
      
        const campaignToSend = await db.campaign.findUnique({ where: { id } })
        if (!campaignToSend) {
          throw new Error(`Campaign with id ${id} not found.`)
        }
      
        const subscribers = await db.subscriber.findMany({
          where: { subscribed: true }, // Only send to active subscribers
        })
      
        if (subscribers.length === 0) {
          return { successCount: 0, failureCount: 0, message: 'No active subscribers found.', failedNumbers: [] }
        }
      
        let successCount = 0
        let failureCount = 0
        const failedNumbers: string[] = []
      
        logger.info(`Starting campaign send for campaign ID: ${id} to ${subscribers.length} subscribers.`)
      
        for (const subscriber of subscribers) {
          // Basic validation - SNS requires E.164 format (e.g., +12223334444)
          if (!subscriber.phoneNumber || !/^\+\d{1,15}$/.test(subscriber.phoneNumber)) {
              logger.warn(`Skipping invalid phone number: ${subscriber.phoneNumber} for subscriber ID: ${subscriber.id}`)
              failureCount++
              failedNumbers.push(subscriber.phoneNumber || 'MISSING_NUMBER')
              continue;
          }
      
          const params = {
            Message: campaignToSend.message,
            PhoneNumber: subscriber.phoneNumber,
            // Optional: Add MessageAttributes for SenderID, etc.
            // MessageAttributes: {
            //   'AWS.SNS.SMS.SenderID': {
            //     'DataType': 'String',
            //     'StringValue': 'MyBrand' // Requires registration in some regions
            //   },
            //  'AWS.SNS.SMS.SMSType': {
            //      'DataType': 'String',
            //      'StringValue': 'Transactional' // Or 'Promotional'
            //  }
            // }
          }
      
          try {
            const command = new PublishCommand(params)
            await snsClient.send(command)
            logger.debug(`Successfully sent SMS to ${subscriber.phoneNumber} for campaign ID: ${id}`)
            successCount++
          } catch (error) {
            logger.error(`Failed to send SMS to ${subscriber.phoneNumber}: ${error.message}`, error)
            failureCount++
            failedNumbers.push(subscriber.phoneNumber)
          }
        }
      
        logger.info(`Finished campaign send for campaign ID: ${id}. Success: ${successCount}, Failures: ${failureCount}`)
      
        return {
          successCount,
          failureCount,
          message: `Campaign send complete. Success: ${successCount}, Failures: ${failureCount}.`,
          failedNumbers,
        }
      }
      
      export const Campaign: CampaignResolvers = {
        // If you need resolvers for related fields, add them here
      }

      Why loop and send individually? While SNS supports batch publishing to topics, sending individually to phone numbers gives granular control and error tracking per subscriber. For very large lists, consider SNS topics or batch APIs for efficiency. Why E.164 format? AWS SNS requires phone numbers in the international E.164 format (e.g., +15551234567) for reliable delivery. The code includes a basic regex check. Why Logging? Detailed logging helps diagnose issues during sending (e.g., invalid numbers, permission errors, SNS throttling).

4. Frontend Implementation (React)

Let's build a simple UI to interact with our API.

  1. Generate Pages and Components:

    bash
    yarn rw g page HomePage /
    yarn rw g cell CampaignsCell
    yarn rw g cell SubscribersCell
    yarn rw g component CampaignForm
    yarn rw g component SubscriberForm
  2. Implement CampaignsCell: (web/src/components/CampaignsCell/CampaignsCell.tsx) Fetch and display campaigns, add a form, and include a button to trigger the sendCampaign mutation.

    typescript
    // web/src/components/CampaignsCell/CampaignsCell.tsx
    import type { FindCampaigns, SendCampaignResult, SendCampaignMutationVariables } from 'types/graphql' // Added SendCampaignMutationVariables
    import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
    import { useMutation } from '@redwoodjs/web'
    import { toast } from '@redwoodjs/web/toast'
    import CampaignForm from 'src/components/CampaignForm' // Assuming you create this
    
    export const QUERY = gql`
      query FindCampaigns {
        campaigns {
          id
          name
          message
          createdAt
        }
      }
    `
    
    // Mutation for sending
    const SEND_CAMPAIGN_MUTATION = gql`
      mutation SendCampaignMutation($id: Int!) {
        sendCampaign(id: $id) {
          successCount
          failureCount
          message
          failedNumbers
        }
      }
    `
    
    export const Loading = () => <div>Loading campaigns...</div>
    export const Empty = () => <div>No campaigns yet. <CampaignForm /></div> // Show form if empty
    export const Failure = ({ error }: CellFailureProps) => <div style={{ color: 'red' }}>Error: {error?.message}</div>
    
    export const Success = ({ campaigns }: CellSuccessProps<FindCampaigns>) => {
      const [sendCampaign, { loading }] = useMutation<SendCampaignResult, SendCampaignMutationVariables>( // Specify variable type
        SEND_CAMPAIGN_MUTATION,
        {
          onCompleted: (data) => {
            toast.success(`Campaign Send Status: ${data.sendCampaign.message}`)
            if (data.sendCampaign.failureCount > 0) {
              toast.error(`Failed to send to: ${data.sendCampaign.failedNumbers?.join(', ')}`)
            }
          },
          onError: (error) => {
            toast.error(`Error sending campaign: ${error.message}`)
          },
          // Optional: Refetch campaigns if needed, though not strictly necessary here
          // refetchQueries: [{ query: QUERY }],
          // awaitRefetchQueries: true,
        }
      )
    
      const handleSendClick = (id: number, name: string) => {
        if (confirm(`Are you sure you want to send campaign ""${name}""?`)) { // Fixed quote escaping
          sendCampaign({ variables: { id } })
        }
      }
    
      return (
        <div>
          <h2>Campaigns</h2>
          <ul>
            {campaigns.map((campaign) => (
              <li key={campaign.id}>
                <strong>{campaign.name}</strong>: ""{campaign.message}"" {/* Fixed quote escaping */}
                <button
                  onClick={() => handleSendClick(campaign.id, campaign.name)}
                  disabled={loading}
                  style={{ marginLeft: '10px' }}
                >
                  {loading ? 'Sending...' : 'Send Campaign'}
                </button>
              </li>
            ))}
          </ul>
          <hr />
          <h3>Create New Campaign</h3>
          <CampaignForm /> {/* Assuming CampaignForm handles create mutation */}
        </div>
      )
    }

    Why useMutation? This Redwood hook simplifies calling GraphQL mutations and handling loading/error/completed states. Why toast? Provides user feedback without disrupting the flow. Run yarn rw setup ui <your-preferred-library> (e.g., chakra-ui) if you haven't already, or use browser alert().

  3. Implement SubscribersCell: (web/src/components/SubscribersCell/SubscribersCell.tsx) Similar structure to CampaignsCell, displaying subscribers and including SubscriberForm.

    typescript
    // web/src/components/SubscribersCell/SubscribersCell.tsx
    import type { FindSubscribers } from 'types/graphql'
    import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
    import SubscriberForm from 'src/components/SubscriberForm' // Assuming you create this
    
    export const QUERY = gql`
      query FindSubscribers {
        subscribers {
          id
          name
          phoneNumber
          subscribed
          createdAt
        }
      }
    `
    
    export const Loading = () => <div>Loading subscribers...</div>
    export const Empty = () => <div>No subscribers yet. <SubscriberForm /></div>
    export const Failure = ({ error }: CellFailureProps) => <div style={{ color: 'red' }}>Error: {error?.message}</div>
    
    export const Success = ({ subscribers }: CellSuccessProps<FindSubscribers>) => {
      return (
        <div>
          <h2>Subscribers</h2>
          <ul>
            {subscribers.map((subscriber) => (
              <li key={subscriber.id}>
                {subscriber.name || 'No Name'} - {subscriber.phoneNumber} ({subscriber.subscribed ? 'Subscribed' : 'Unsubscribed'})
                {/* Add Edit/Delete buttons later if needed */}
              </li>
            ))}
          </ul>
          <hr />
          <h3>Add New Subscriber</h3>
          <SubscriberForm /> {/* Assuming SubscriberForm handles create mutation */}
        </div>
      )
    }
  4. Implement Forms: (CampaignForm.tsx, SubscriberForm.tsx) Use Redwood's form helpers (@redwoodjs/forms) to create forms that call the respective createCampaign and createSubscriber mutations. Remember to include validation (e.g., required fields, phone number format). Refer to RedwoodJS Forms documentation for detailed implementation. Ensure the phone number input encourages or enforces the E.164 format (e.g., using a placeholder +15551234567).

  5. Update HomePage: (web/src/pages/HomePage/HomePage.tsx) Include the cells on the main page.

    typescript
    // web/src/pages/HomePage/HomePage.tsx
    import { MetaTags } from '@redwoodjs/web'
    import CampaignsCell from 'src/components/CampaignsCell'
    import SubscribersCell from 'src/components/SubscribersCell'
    
    const HomePage = () => {
      return (
        <>
          <MetaTags title=""SNS Campaign Manager"" description=""Manage and send SMS campaigns"" /> {/* Fixed empty title */}
    
          <h1>SNS Campaign Manager</h1>
          <CampaignsCell />
          <br />
          <SubscribersCell />
        </>
      )
    }
    
    export default HomePage

5. Deployment to AWS

Let's deploy the application using the configured Serverless setup.

  1. First Deployment: The initial deployment requires a special flag to set up both API and Web sides correctly.

    bash
    yarn rw deploy serverless --first-run
    • During this process, the API side is deployed first.
    • The command will then detect the deployed API's URL.
    • Crucially: You will be prompted: Add API_URL=<your-api-url> to .env.production? (Y/n). Type Y and press Enter. This saves the API endpoint URL so the web frontend knows where to send GraphQL requests in the deployed environment.
    • The command will then proceed to build and deploy the web side to S3/CloudFront.
  2. Configure Production Environment Variables: After the first deploy, a .env.production file is created. This file is critical for the deployed application and should NOT be committed to Git. You MUST add necessary environment variables here, as .env is not used in production deployment.

    plaintext
    # .env.production
    
    # Added by --first-run:
    API_URL=https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/ # Example URL
    
    # *** YOU MUST ADD THESE MANUALLY ***
    DATABASE_URL=postgresql://prod_user:prod_password@your_prod_db_host:5432/prod_db # Your PRODUCTION database URL
    AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID_HERE     # Key needed by the running Lambda
    AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY_HERE # Secret needed by the running Lambda
    AWS_REGION=us-east-1                          # Region for the running Lambda
    
    # Add session secrets if using Redwood Auth
    # SESSION_SECRET=your_strong_random_secret_here

    Why manually add? The deploy command only adds API_URL. Production database credentials and AWS keys/region needed by the running Lambda function (specifically, for the AWS SDK to interact with SNS at runtime) must be explicitly added here. These variables are packaged during the build/deploy process and made available to the Lambda environment. The credentials used by the Serverless Framework during deployment might be different (e.g., from your local aws configure or CI/CD environment) or could be the same, but the Lambda needs its own runtime access defined here.

  3. Apply Production Migrations: Connect to your production database and apply migrations. How you do this depends on your database hosting. You might SSH into a bastion host or use a managed database service's console. The command itself is:

    bash
    # Run this command in an environment connected to your PRODUCTION database
    yarn rw prisma migrate deploy
  4. Subsequent Deploys: For future updates after making code changes:

    bash
    yarn rw deploy serverless

    This command rebuilds and redeploys both the API and Web sides using the settings in serverless.yml and variables from .env.production.

  5. Removing Your Deploy: To tear down the deployed AWS resources and avoid costs, run the serverless remove command in both the api and web directories.

    bash
    cd api
    yarn serverless remove --stage production # 'production' is the default stage
    cd ../web
    yarn serverless remove --stage production
    cd ..

6. Security Considerations

  • Authentication: This guide omits authentication for simplicity. Use RedwoodJS Auth (yarn rw setup auth <provider>) to protect your API and UI. Restrict the sendCampaign mutation to specific roles (e.g., admin) as shown in the code example.
  • Input Validation: Sanitize all inputs. Use Redwood's built-in validators or libraries like zod for robust validation on both frontend forms and backend services (e.g., ensuring message content is safe, validating phone number formats strictly).
  • AWS Credentials: Never hardcode credentials. Use .env and .env.production correctly. Grant the IAM user the least privilege necessary (e.g., only sns:Publish if that's all the running Lambda needs). Consider using IAM Roles for Lambda functions if deploying via methods other than Serverless Framework's default credential handling, as roles are generally more secure than long-lived access keys.
  • Rate Limiting: Implement rate limiting on the sendCampaign mutation (and potentially others) to prevent abuse. This can be done within the service logic or using API Gateway features.
  • SNS Costs: Be aware of SNS SMS pricing. Implement checks or limits to prevent accidental large sends. Consider SNS sandbox limits initially.

7. Error Handling and Logging

  • Service Logic: Use try...catch blocks around critical operations like database calls and AWS SDK calls (snsClient.send).
  • Redwood Logger: Utilize Redwood's built-in logger (import { logger } from 'src/lib/logger') for structured logging within your API services. Log informative messages for successful operations and detailed error messages/stack traces for failures. Levels (debug, info, warn, error) help filter logs later.
  • API Responses: Provide meaningful error messages back through GraphQL, but avoid leaking sensitive details.
  • CloudWatch: After deployment, Lambda function logs (including output from Redwood's logger) and SNS delivery status logs are available in AWS CloudWatch. Use these for debugging production issues.

8. Testing

  • Unit/Integration Tests: Write tests for your services, especially the sendCampaign logic. Mock the Prisma client (@redwoodjs/testing/api) and the AWS SNS client (jest.mock('@aws-sdk/client-sns')) to test the logic without actual DB calls or sending SMS messages.
    bash
    yarn rw test api
  • Scenario Tests: Test edge cases like invalid phone numbers, non-existent campaigns, or empty subscriber lists.
  • E2E Tests: Consider tools like Cypress (which Redwood can set up: yarn rw setup testing cypress) to test the full user flow from the UI.

9. Monitoring and Observability

  • AWS CloudWatch:
    • Logs: Monitor Lambda execution logs for errors and output from logger. Monitor SNS delivery status logs.
    • Metrics: Track Lambda invocations, duration, errors. Track SNS metrics like messages sent, delivery success/failure rates.
    • Alarms: Set CloudWatch Alarms on key metrics (e.g., high Lambda error rate, high SNS failure rate) to get notified of problems.
  • Redwood Logger: Ensure sufficient logging is implemented (as described in Error Handling).

10. Troubleshooting and Caveats

  • Serverless Deprecation: As stated, yarn rw setup deploy serverless is deprecated. Monitor RedwoodJS for future AWS deployment recommendations. The current setup might break in future Redwood versions without community maintenance.
  • Deployment Errors:
    • Error: No auth.zip file found...: Often means the dev server (yarn rw dev) is running. Stop it and retry deployment.
    • IAM Permissions: Ensure the AWS credentials used have all necessary permissions (Lambda, API Gateway, S3, CloudFront, SNS). Check CloudFormation stack events in the AWS console for detailed errors during deployment.
    • serverless.yml: If you add custom functions outside the standard GraphQL endpoint, you might need to manually add them to api/serverless.yml.
  • Runtime Errors (Lambda):
    • Missing Env Vars: Double-check that DATABASE_URL, AWS_REGION, and AWS credentials are correctly set in .env.production and redeploy if changed.
    • Database Connectivity: Ensure the Lambda function can reach your production database (check security groups, VPC settings, connection strings).
    • SNS Errors: Check CloudWatch logs. Common issues include invalid phone number format (must be E.164), hitting SNS spending limits (check SNS console), or lack of sns:Publish permission.
  • SNS Sandbox: New AWS accounts are often in the SNS sandbox, limiting sending to verified numbers and imposing spending caps. Request removal from the sandbox via AWS Support for production use.
  • Phone Number Formatting: SNS is strict about E.164 format (+ followed by country code and number, no dashes or spaces). Implement robust validation on input.
  • API_URL Mismatch: If the frontend can't reach the backend after deployment, verify the API_URL in .env.production matches the actual deployed API Gateway URL.

11. Verification

After deployment:

  1. Access the CloudFront URL provided at the end of the yarn rw deploy serverless command.
  2. Verify the HomePage loads, showing empty lists initially.
  3. Use the UI to add a new Campaign with a test message.
  4. Use the UI to add a new Subscriber with your own phone number (ensure it's your own number and in the required E.164 format, e.g., +15551234567).
  5. Click the ""Send Campaign"" button for the test campaign.
  6. Confirm you receive the SMS message on your phone.
  7. Check the application UI for success/failure toasts.
  8. (Optional) Check AWS CloudWatch Logs for the Lambda function associated with your GraphQL endpoint to see log messages from the sendCampaign service. Check SNS delivery logs in CloudWatch.

Next Steps

This guide provides a functional foundation. Potential enhancements include:

  • Authentication & Authorization: Secure the application properly.
  • Subscriber Management: Add features for editing, unsubscribing (and honoring the subscribed flag), and importing/exporting subscribers.
  • Campaign Scheduling: Implement background jobs (using Redwood Background Functions or external services like AWS Step Functions/EventBridge) to schedule campaigns.
  • Analytics: Track send times, delivery rates, and potentially link clicks (requires URL shortening and tracking).
  • Error Retries: Implement a retry mechanism (e.g., with exponential backoff) for failed SNS sends within the service or via background jobs.
  • Advanced SNS Features: Explore Sender IDs, Promotional vs Transactional types, and SNS Topics for segmenting subscribers.
  • Deployment Alternatives: Investigate alternative AWS deployment methods as they become recommended by RedwoodJS (e.g., Flightcontrol, SST, manual CDK/CloudFormation).

Frequently Asked Questions

How to send SMS messages with RedwoodJS?

You can send SMS messages using RedwoodJS by integrating with AWS SNS. Build a RedwoodJS app, configure AWS credentials, define your data models with Prisma, and implement a GraphQL API endpoint to trigger the sending process via the AWS SDK for JavaScript. This allows you to manage subscribers and send targeted campaigns directly from your application.

What is RedwoodJS used for in this project?

RedwoodJS is the core framework for building this full-stack, serverless-friendly application. It provides the structure for the frontend (React), backend (GraphQL API with Prisma ORM), and simplifies deployment to AWS Lambda and S3/CloudFront. Its integrated nature streamlines development and deployment processes.

Why use AWS SNS for SMS marketing?

AWS SNS is a managed messaging service chosen for its reliability, scalability, and direct SMS integration capabilities. It handles the complexities of sending messages at scale, allowing your RedwoodJS application to focus on campaign and subscriber management.

When should I configure AWS credentials in .env?

AWS credentials should be configured in the `.env` file at the project root during the initial setup. This file is used during development and is never committed to version control due to security concerns. For production, use `.env.production` which is created after the first deployment.

Can I use a database other than PostgreSQL?

Yes, you can use SQLite, MySQL, or PostgreSQL as your database. Define your preferred provider in the `api/db/schema.prisma` file and update the `DATABASE_URL` in your `.env` file accordingly. Prisma, Redwood's ORM, supports multiple database providers.

How to deploy a RedwoodJS app to AWS?

Use the `yarn rw deploy serverless` command. Note: This approach with the serverless framework is deprecated. For the first deployment, the `--first-run` flag is required to set up API and Web sides and to correctly store the `API_URL` in the .env.production file.

What is the role of Prisma in this project?

Prisma is an ORM (Object-Relational Mapper) that simplifies database interactions. It's used to define data models (Campaign and Subscriber), manage database migrations, and generate the client used within the RedwoodJS services to query and modify the database.

Why is the Serverless Framework deployment deprecated?

The RedwoodJS documentation mentions that Serverless Framework deployment is deprecated in RedwoodJS v5+. While functional as of November 2023, monitor RedwoodJS announcements for updated deployment strategies to ensure compatibility.

How to structure GraphQL API for managing campaigns?

Use Redwood's generators to scaffold CRUD operations (`yarn rw g sdl Campaign --crud`). Implement a `sendCampaign` mutation within `api/src/graphql/campaigns.sdl.ts` and its logic in `api/src/services/campaigns/campaigns.ts` using the AWS SDK for SNS. Return a `SendCampaignResult` type for detailed feedback.

How do I handle sending to multiple phone numbers?

The provided implementation sends messages individually in a loop for granular control and error handling. For large lists, consider SNS topics or batch sending for efficiency, although this may come with additional setup on the SNS side.

What is the best format for phone numbers in SNS?

The recommended format for phone numbers when using SNS is the international E.164 format. This includes a plus sign followed by the country code and subscriber number, without any other characters like spaces or hyphens. For example, +15551234567.

How do I configure a production database URL?

Add the `DATABASE_URL` environment variable pointing to your production database in the `.env.production` file. This file is created after the first deployment. Do not commit this file to git because it contains sensitive information.

How to secure AWS credentials in a Redwood app?

Never hardcode AWS credentials. Store them in .env during development and .env.production for deployment, and do not commit these files to version control. Use IAM roles for Lambda functions for enhanced security and least privilege principles, granting only necessary permissions like 'sns:Publish'.

What are some best practices for error handling in RedwoodJS?

Use `try...catch` blocks around API operations, log informative messages with Redwood's `logger`, return meaningful error messages through GraphQL without revealing sensitive data, and monitor CloudWatch for production issues.

Where can I find logs for my deployed RedwoodJS application?

Logs from your deployed RedwoodJS application, including Lambda function execution logs and SNS delivery status logs, can be found in AWS CloudWatch. Redwood's logger integrates with CloudWatch, providing structured logs for easier debugging.