messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / RedwoodJS

RedwoodJS Two-Way SMS with MessageBird: Inbound & Outbound Messaging Tutorial

Learn how to build production-ready two-way SMS messaging in RedwoodJS with MessageBird webhooks. Complete guide covering SMS sending, receiving inbound messages, webhook security, signature verification, and deployment for real-time SMS communication.

Build Two-Way SMS Messaging in RedwoodJS with MessageBird

Build production-ready two-way SMS messaging in your RedwoodJS application using MessageBird's SMS API and webhook system. This tutorial shows you how to send outbound SMS messages via GraphQL and receive inbound SMS through secure webhooks—perfect for customer support chat, appointment confirmations, OTP verification, automated responses, and real-time notifications.

You'll implement complete bidirectional SMS communication with webhook signature verification, message logging, replay attack prevention, and production deployment strategies for RedwoodJS applications.

What You'll Build: Two-Way SMS Communication System

Core Features:

  1. Outbound SMS Sending: Trigger SMS messages programmatically through GraphQL mutations from your RedwoodJS application
  2. Inbound SMS Reception: Handle incoming SMS messages sent to your MessageBird virtual number using secure webhook endpoints
  3. Message Persistence: Store complete conversation history of both inbound and outbound messages in your database
  4. Webhook Security: Implement HMAC-SHA256 signature verification and timestamp validation to prevent unauthorized access

Real-World Use Cases:

This two-way SMS system enables interactive communication for appointment reminders with confirmations, customer support conversations, two-factor authentication codes, order status updates with customer replies, survey responses, and automated chatbot interactions.

Technology Stack:

  • RedwoodJS v8.0+: Full-stack JavaScript/TypeScript framework with GraphQL and serverless functions. Requires Node.js v20.x or later
  • Node.js v20.x+: Runtime environment for the RedwoodJS API side (official source)
  • Yarn v1.22.21+: Package manager required by RedwoodJS
  • MessageBird SMS API: Communication platform providing SMS API and virtual phone numbers for two-way messaging
  • MessageBird Node.js SDK v3.8.0+: Simplifies interaction with the MessageBird API. Requires Node.js >=0.10 (npm package)
  • Prisma ORM: Database toolkit used by RedwoodJS for type-safe database access
  • Database: PostgreSQL, MySQL, or SQLite (PostgreSQL recommended for production)
  • ngrok v3.0+: Development tool to expose your local server for testing inbound SMS webhooks

System Architecture:

text
_____________________      ________________________      ____________________
| RedwoodJS Web App |----->| RedwoodJS API (GraphQL)|----->|     Database     |
| (Frontend)        |      | (Node.js / Prisma)   |      |   (PostgreSQL)   |
|___________________|      |________________________|      |____________________|
       ^                             | |  ^                          |
       |                             | |  |__________________________| (Log Messages)
       | (GraphQL Mutation)          | | (MessageBird SDK)
       |                             | |
___________________________      |_______________|       ________________________
| User's Mobile Device    |<----->|  MessageBird  |<----->| RedwoodJS Function   |
| (Sends/Receives SMS)    |      | (API/Webhook) |       | (Webhook Endpoint)   |
|___________________________|      |_______________|       |________________________|
                                      | |
                                      | | (Webhook Config in Flow Builder)
                                      | |
                               _____________
                               | `ngrok`   | (Development Only)
                               |___________|

Prerequisites:

  • Node.js v20.x or later: Download from nodejs.org. Verify installation: node --version
  • Yarn v1.22.21 or later: Install via npm: npm install -g yarn. Verify: yarn --version
  • RedwoodJS CLI: Install globally: yarn global add @redwoodjs/cli
  • MessageBird Account: Sign up at messagebird.com
  • Virtual Phone Number: Purchase an SMS-enabled number from MessageBird Dashboard → Numbers → Buy a number for receiving inbound SMS
  • ngrok: Download from ngrok.com/download. Free account required for webhook testing
  • Database: PostgreSQL recommended. Install locally or use hosted service (e.g., Railway, Render, Supabase)

Expected Outcome:

A fully functional RedwoodJS application that sends SMS via GraphQL and receives/processes/logs incoming SMS messages via secure webhooks for true two-way communication.

1. Setting up the Project

Initialize a new RedwoodJS project, set up the database, and install dependencies.

Step 1.1: Install Prerequisites

Node.js and Yarn:

bash
# Verify Node.js version (must be v20.x or later)
node --version

# If needed, download from https://nodejs.org/

# Install Yarn globally
npm install -g yarn

# Verify Yarn installation
yarn --version

Database Setup (PostgreSQL recommended):

For local development, install PostgreSQL:

  • macOS: brew install postgresql@14 && brew services start postgresql@14
  • Ubuntu/Debian: sudo apt-get install postgresql postgresql-contrib
  • Windows: Download installer from postgresql.org

Or use a hosted database service like Railway, Render, or Supabase.

Step 1.2: Create RedwoodJS Project

Open your terminal and run:

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

Using TypeScript provides better type safety and developer experience, especially for larger applications.

Step 1.3: Configure Database Connection

Update your .env file with your database connection string:

ini
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/messagebird_dev"
# Or for SQLite (development only):
# DATABASE_URL="file:./dev.db"

Run initial database migration:

bash
yarn rw prisma migrate dev

Step 1.4: Install MessageBird SDK

Navigate to the api directory and add the MessageBird Node.js SDK:

bash
cd api
yarn add messagebird
cd ..

SDK version: The messagebird package (v3.8.0+) is compatible with Node.js >=0.10, including Node.js v20.x.

Step 1.5: Set Up MessageBird Account

Obtain API Keys:

  1. Log in to your MessageBird Dashboard at dashboard.messagebird.com
  2. Navigate to Developer SettingsAPI access
  3. Copy your Live API Key (starts with live_) for production use
  4. Copy your Test API Key (starts with test_) for development

Test keys simulate API requests without sending actual SMS or consuming credits. Live keys send real messages and deduct from your balance (official documentation).

Obtain Signing Key:

  1. In Dashboard, go to Developer SettingsWebhook Signing
  2. Click Generate signing key
  3. Copy the signing key (used to verify webhook authenticity)

Purchase Virtual Number:

  1. Go to NumbersBuy a number
  2. Select your country and ensure SMS capability is enabled
  3. Choose and purchase a number
  4. Note the number in E.164 format (e.g., +14155552671)

Step 1.6: Configure Environment Variables

Never commit API keys to version control. Use environment variables.

Create a .env file in the project root (redwood-messagebird-app/.env):

ini
# .env
# MessageBird API Keys
MESSAGEBIRD_ACCESS_KEY=YOUR_LIVE_OR_TEST_ACCESS_KEY
MESSAGEBIRD_SIGNING_KEY=YOUR_WEBHOOK_SIGNING_KEY

# Your purchased MessageBird number (E.164 format)
MESSAGEBIRD_ORIGINATOR_NUMBER=+14155552671

# (Optional) Your test mobile number (E.164 format)
TEST_RECIPIENT_NUMBER=+12125551234

# Database
DATABASE_URL="postgresql://user:password@localhost:5432/messagebird_dev"

E.164 Format: Phone numbers must be in international format without spaces or special characters:

  • US number: +12145551234 (country code +1, area code 214, number 5551234)
  • UK number: +442012345678 (country code +44, area code 20, number 12345678)
  • Format: +[country code][area code][local number] (max 15 digits) (official documentation)

Add .env to .gitignore:

Ensure .gitignore contains .env to prevent committing keys. RedwoodJS includes this by default.

Step 1.7: Accessing Environment Variables

RedwoodJS automatically loads variables from .env into process.env. Access them in your API-side code (services, functions) like process.env.MESSAGEBIRD_ACCESS_KEY.

2. Implementing Core Functionality (Outbound SMS)

Create the service for sending SMS messages.

Step 2.1: Create the Outbound SMS Service

Generate a new service to encapsulate the outbound SMS logic:

bash
yarn rw g service sms

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

RedwoodJS services contain business logic and data access. GraphQL resolvers call them, and you can reuse them throughout your application (RedwoodJS documentation).

Configure the sms.ts service:

typescript
// api/src/services/sms/sms.ts
import messagebird from 'messagebird';

// Initialize the MessageBird client using API key from environment variable
const client = messagebird(process.env.MESSAGEBIRD_ACCESS_KEY);

/**
 * Send an SMS message via MessageBird
 * @param {string} to - Recipient phone number in E.164 format (e.g., +12345678901)
 * @param {string} body - Message content (160 chars for GSM-7, 70 for Unicode)
 * @param {string} originator - Sender ID (phone number or alphanumeric, max 11 chars)
 * @returns {Promise<object>} - MessageBird API response with message ID and status
 */
export const sendSMS = ({ to, body, originator }) => {
  return new Promise((resolve, reject) => {
    // Validate required parameters
    if (!to || !body || !originator) {
      return reject(new Error('Missing required parameters: to, body, and originator are required'));
    }

    // Validate E.164 format (basic check)
    const e164Regex = /^\+[1-9]\d{1,14}$/;
    if (!e164Regex.test(to)) {
      return reject(new Error('Recipient number must be in E.164 format (e.g., +12125551234)'));
    }

    // Check message length and warn about concatenation
    const isUnicode = /[^\x00-\x7F]/.test(body);
    const maxLength = isUnicode ? 70 : 160;
    const partCount = Math.ceil(body.length / maxLength);

    if (partCount > 1) {
      console.warn(`Message will be split into ${partCount} parts. Cost will be ${partCount}x per message.`);
    }

    const params = {
      originator,
      recipients: [to],
      body
    };

    client.messages.create(params, (err, response) => {
      if (err) {
        console.error('MessageBird API Error:', err);

        // Common error types
        if (err.errors) {
          const errorCodes = err.errors.map(e => `${e.code}: ${e.description}`).join(', ');
          return reject(new Error(`MessageBird API Error: ${errorCodes}`));
        }

        return reject(err);
      }

      console.log('SMS sent successfully:', response.id);
      resolve(response);
    });
  });
};

SMS Character Limits:

  • GSM-7 encoding: 160 characters per SMS, up to 1377 characters per concatenated message
  • Unicode encoding: 70 characters per SMS, up to 603 characters per concatenated message
  • Concatenated messages are billed as multiple SMS (official documentation)

Common API Errors:

  • Code 2: Incorrect access key (authentication failed)
  • Code 9: Missing required parameters
  • Code 21: Insufficient balance
  • Code 25: Invalid phone number format

3. Building the API Layer (GraphQL Mutation)

Create a GraphQL mutation to send SMS messages from your web application.

Step 3.1: Define GraphQL Schema

Create or update api/src/graphql/sms.sdl.ts:

typescript
// api/src/graphql/sms.sdl.ts
export const schema = gql`
  type SMSResponse {
    success: Boolean!
    messageId: String
    error: String
  }

  type Mutation {
    sendSMS(
      to: String!
      body: String!
      originator: String!
    ): SMSResponse! @requireAuth
  }
`;

@requireAuth ensures only authenticated users can send SMS. Remove it if you don't need authentication.

Step 3.2: Implement GraphQL Resolver

Update api/src/services/sms/sms.ts to add the resolver:

typescript
// Add to api/src/services/sms/sms.ts

interface SendSMSArgs {
  to: string;
  body: string;
  originator: string;
}

export const sendSMS = async ({ to, body, originator }: SendSMSArgs) => {
  try {
    const response = await new Promise((resolve, reject) => {
      // Validate parameters
      if (!to || !body || !originator) {
        return reject(new Error('Missing required parameters'));
      }

      const e164Regex = /^\+[1-9]\d{1,14}$/;
      if (!e164Regex.test(to)) {
        return reject(new Error('Recipient must be in E.164 format'));
      }

      const params = {
        originator,
        recipients: [to],
        body
      };

      client.messages.create(params, (err, response) => {
        if (err) {
          console.error('MessageBird API Error:', err);
          return reject(err);
        }
        resolve(response);
      });
    });

    return {
      success: true,
      messageId: response.id,
      error: null
    };
  } catch (error) {
    console.error('SMS sending failed:', error);
    return {
      success: false,
      messageId: null,
      error: error.message
    };
  }
};

4. Building the Webhook Handler (Inbound SMS)

Receive and process incoming SMS messages through a webhook endpoint—the key component for two-way SMS functionality.

Step 4.1: Create the Webhook Handler Function

Generate a serverless function to handle incoming webhooks:

bash
yarn rw g function webhook

This creates api/src/functions/webhook/webhook.ts.

RedwoodJS serverless functions are independent API endpoints ideal for webhooks, background jobs, and external integrations (RedwoodJS documentation).

Implement the webhook handler:

typescript
// api/src/functions/webhook/webhook.ts
import crypto from 'crypto';
import { db } from 'src/lib/db';
import type { APIGatewayEvent, Context } from 'aws-lambda';

/**
 * Verify MessageBird webhook signature with timestamp validation
 * Prevents unauthorized requests and replay attacks
 */
const verifySignature = (event: APIGatewayEvent, body: string): boolean => {
  const signature = event.headers['messagebird-signature'];
  const timestamp = event.headers['messagebird-request-timestamp'];

  if (!signature || !timestamp) {
    console.error('Missing signature or timestamp headers');
    return false;
  }

  // Validate timestamp to prevent replay attacks
  // Accept requests within 5 minutes (300 seconds) of creation
  const requestTime = parseInt(timestamp, 10) * 1000; // Convert to milliseconds
  const currentTime = Date.now();
  const timeDiff = Math.abs(currentTime - requestTime);
  const TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes

  if (timeDiff > TOLERANCE_MS) {
    console.error(`Timestamp outside tolerance: ${timeDiff}ms difference`);
    return false;
  }

  // Construct the signing payload
  const payload = `${timestamp}\n${body}`;

  // Calculate expected signature using your signing key
  const expectedSignature = crypto
    .createHmac('sha256', process.env.MESSAGEBIRD_SIGNING_KEY)
    .update(payload)
    .digest('hex');

  // Compare signatures using timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch (err) {
    console.error('Signature comparison failed:', err);
    return false;
  }
};

/**
 * Handler for incoming webhook requests from MessageBird
 */
export const handler = async (event: APIGatewayEvent, context: Context) => {
  // Only accept POST requests
  if (event.httpMethod !== 'POST') {
    return {
      statusCode: 405,
      body: JSON.stringify({ error: 'Method not allowed' })
    };
  }

  try {
    const body = event.body;

    // Verify the webhook signature
    if (!verifySignature(event, body)) {
      console.error('Invalid webhook signature');
      return {
        statusCode: 401,
        body: JSON.stringify({ error: 'Unauthorized' })
      };
    }

    const payload = JSON.parse(body);

    // Extract message data from webhook payload
    // Webhook payload structure:
    // { id, originator, body, recipient, createdDatetime, type, direction }
    const {
      id,
      originator,
      body: messageBody,
      recipient,
      createdDatetime
    } = payload;

    // Check for duplicate webhook delivery (idempotency)
    const existingMessage = await db.message.findUnique({
      where: { messageBirdId: id }
    });

    if (existingMessage) {
      console.log('Duplicate webhook ignored:', id);
      return {
        statusCode: 200,
        body: JSON.stringify({ success: true, duplicate: true })
      };
    }

    // Store the incoming message in database
    await db.message.create({
      data: {
        messageBirdId: id,
        sender: originator,
        recipient: recipient,
        body: messageBody,
        direction: 'inbound',
        receivedAt: new Date(createdDatetime)
      }
    });

    console.log('Inbound message processed:', id);

    // Return 200 within 30 seconds to acknowledge receipt
    // MessageBird will retry up to 10 times if not acknowledged
    return {
      statusCode: 200,
      body: JSON.stringify({ success: true })
    };

  } catch (error) {
    console.error('Webhook processing error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
};

MessageBird Webhook Payload Structure:

Inbound SMS webhooks contain the following fields (official documentation):

  • id: Unique message identifier
  • originator: Sender's phone number (E.164 format)
  • recipient: Your MessageBird virtual number
  • body: Message content
  • createdDatetime: Timestamp in RFC3339 format
  • type: Message type (usually "sms")
  • direction: "mo" (mobile originated = inbound)

Webhook Retry Behavior:

MessageBird retries failed webhooks up to 10 times with exponential backoff. Return HTTP 200 within 30 seconds to acknowledge receipt (webhook documentation).

5. Securing Your Implementation

Protect your webhook endpoint from unauthorized access with signature verification.

Webhook Signature Verification

Verify every webhook request using MessageBird's signing key to prevent unauthorized requests and confirm message authenticity.

How signature verification works:

  1. MessageBird signs each webhook request using your signing key with HMAC-SHA256
  2. The signature arrives in the messagebird-signature header
  3. The timestamp arrives in the messagebird-request-timestamp header (Unix timestamp)
  4. Calculate the expected signature: HMAC-SHA256(signing_key, timestamp + "\n" + body)
  5. Compare signatures using timing-safe comparison to prevent timing attacks

Timestamp Validation (Prevents Replay Attacks):

Validate the timestamp to prevent replay attacks. Accept requests within a 5-minute tolerance window (security best practices):

typescript
const requestTime = parseInt(timestamp, 10) * 1000;
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - requestTime);
const TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes

if (timeDiff > TOLERANCE_MS) {
  return false; // Reject expired requests
}

Best practices:

  • Reject requests with invalid signatures (return 401 Unauthorized)
  • Validate timestamp within 5-minute tolerance
  • Log signature verification failures
  • Rotate signing keys periodically
  • Store signing keys in environment variables
  • Use timing-safe comparison (crypto.timingSafeEqual)
  • Check for duplicate message IDs before processing

Environment Variable Security

Protect sensitive credentials by storing them as environment variables:

bash
# .env
MESSAGEBIRD_ACCESS_KEY=your_api_key_here
MESSAGEBIRD_SIGNING_KEY=your_signing_key_here
MESSAGEBIRD_ORIGINATOR_NUMBER=+12345678901

Security checklist:

  • Add .env to .gitignore
  • Use different keys for development, staging, and production
  • Rotate API keys quarterly or after security incidents
  • Monitor API key usage for unusual activity
  • Implement rate limiting on webhook endpoints
  • Use HTTPS for webhook URLs (required by MessageBird)
  • Validate and sanitize incoming data before database insertion

6. Database Schema and Message Logging

Store incoming and outgoing SMS for tracking, analytics, and conversation history.

Step 6.1: Create the Prisma Schema

Define a Message model in your Prisma schema to store SMS data:

prisma
// api/db/schema.prisma

model Message {
  id              String   @id @default(cuid())
  messageBirdId   String   @unique // MessageBird's message ID (for idempotency)
  sender          String   // Phone number in E.164 format
  recipient       String   // Phone number in E.164 format
  body            String   // Message content
  direction       String   // "inbound" or "outbound"
  status          String?  // Delivery status (for outbound messages)
  receivedAt      DateTime @default(now())
  sentAt          DateTime?
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  // Indexes for common queries
  @@index([sender])
  @@index([recipient])
  @@index([direction])
  @@index([receivedAt])
}

Schema improvements:

  • @@unique constraint on messageBirdId prevents duplicate webhook processing
  • Indexes on sender, recipient, direction, and receivedAt optimize common queries
  • status field stores delivery status for outbound messages (sent, delivered, failed)

Step 6.2: Apply Database Migrations

Run Prisma migrations to create the database table:

bash
yarn rw prisma migrate dev --name add_message_model

This:

  1. Creates a migration file in api/db/migrations/
  2. Applies the migration to your development database
  3. Regenerates Prisma Client with the new model

Migration conflicts: Use yarn rw prisma migrate reset to reset the database. This deletes all data in development.

Step 6.3: Query Messages

Use Prisma Client to query message history:

typescript
// Get all messages for a specific phone number
const messages = await db.message.findMany({
  where: {
    OR: [
      { sender: '+12345678901' },
      { recipient: '+12345678901' }
    ]
  },
  orderBy: {
    receivedAt: 'desc'
  }
});

// Get conversation between two numbers
const conversation = await db.message.findMany({
  where: {
    OR: [
      { sender: '+12345678901', recipient: '+10987654321' },
      { sender: '+10987654321', recipient: '+12345678901' }
    ]
  },
  orderBy: {
    receivedAt: 'asc'
  }
});

// Pagination for large message volumes
const PAGE_SIZE = 50;
const page = 1;

const paginatedMessages = await db.message.findMany({
  take: PAGE_SIZE,
  skip: (page - 1) * PAGE_SIZE,
  orderBy: { receivedAt: 'desc' }
});

7. Testing Your Implementation

Test your SMS integration before deploying to production.

Testing Outbound SMS

Test SMS sending through your GraphQL API:

graphql
mutation SendTestSMS {
  sendSMS(
    to: "+12125551234"
    body: "Test message from RedwoodJS"
    originator: "+14155552671"
  ) {
    success
    messageId
    error
  }
}

Verification steps:

  1. Check your phone for the received SMS
  2. Verify the message appears in MessageBird dashboard (Messages section)
  3. Confirm the message saves to your database: yarn rw prisma studio
  4. Check API logs for any errors: yarn rw dev

Testing with Test Keys:

Use MessageBird test keys (test_xxxxxxx) for development. Test keys simulate API calls without sending SMS or consuming credits. The API returns success responses, but delivers no messages (official documentation).

Testing Inbound Webhooks Locally

Use ngrok to expose your local development server for webhook testing:

Step 1: Install and start ngrok:

bash
# Install ngrok
# macOS: brew install ngrok
# Linux: snap install ngrok
# Windows: Download from https://ngrok.com/download

# Authenticate (required for ngrok v3+)
ngrok config add-authtoken YOUR_NGROK_AUTH_TOKEN

# Start ngrok tunnel on RedwoodJS API port (default 8911)
ngrok http 8911

Step 2: Configure MessageBird webhook:

  1. Copy the HTTPS URL from ngrok (e.g., https://abc123.ngrok.io)
  2. Go to MessageBird Dashboard → Flow Builder
  3. Create a new flow or select existing flow
  4. Select your virtual number as the trigger
  5. Add "Call HTTP endpoint with SMS" step
  6. Set webhook URL to: https://abc123.ngrok.io/.redwood/functions/webhook
  7. Method: POST
  8. Click Publish to activate

Step 3: Send test SMS:

Send an SMS from your phone to your MessageBird number. Monitor:

  • ngrok console for incoming requests (shows headers and payload)
  • RedwoodJS logs (yarn rw dev) for webhook processing
  • Database (Prisma Studio: yarn rw prisma studio) for stored message records

Simulating Webhook Requests

Test webhook handling without sending actual SMS using curl with a valid signature:

Calculate valid test signature:

bash
# 1. Get current Unix timestamp
TIMESTAMP=$(date +%s)

# 2. Create payload file
cat > payload.json << EOF
{
  "id": "test-msg-12345",
  "originator": "+12125551234",
  "body": "Test inbound message",
  "recipient": "+14155552671",
  "createdDatetime": "2024-01-15T10:30:00Z"
}
EOF

# 3. Calculate signature
PAYLOAD=$(cat payload.json)
SIGNING_KEY="your_signing_key_here"
SIGNATURE=$(echo -n "${TIMESTAMP}\n${PAYLOAD}" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}')

# 4. Send request with valid signature
curl -X POST http://localhost:8911/.redwood/functions/webhook \
  -H "Content-Type: application/json" \
  -H "messagebird-signature: $SIGNATURE" \
  -H "messagebird-request-timestamp: $TIMESTAMP" \
  -d @payload.json

Common Testing Issues

Problem: Webhook receives requests but returns 401

  • Verify your signing key matches the MessageBird dashboard value
  • Check timestamp format in signature calculation
  • Ensure signing key is set in .env

Problem: Messages don't appear in database

  • Check Prisma Client is initialized
  • Verify database connection string in .env
  • Run migrations: yarn rw prisma migrate dev
  • Check for duplicate messageBirdId constraint violations

Problem: Outbound SMS fails with 401

  • Verify API key in environment variables
  • Use a live key (not test key) for actual sending
  • Check API key permissions in MessageBird dashboard

Problem: ngrok tunnel disconnects

  • ngrok v3+ requires authentication
  • Register at ngrok.com and add authtoken: ngrok config add-authtoken YOUR_TOKEN
  • Restart ngrok and update webhook URL in MessageBird dashboard

Problem: SMS character limit exceeded

  • Messages over 160 characters (GSM-7) or 70 characters (Unicode) split into multiple SMS
  • Implement a character counter in your UI
  • Warn users when approaching limits

8. Deployment Considerations

Deploy your RedwoodJS SMS application to production.

Environment Variables

Configure production environment variables in your hosting platform:

bash
MESSAGEBIRD_ACCESS_KEY=live_your_production_key
MESSAGEBIRD_SIGNING_KEY=your_production_signing_key
MESSAGEBIRD_ORIGINATOR_NUMBER=+14155552671
DATABASE_URL=postgresql://user:password@host:5432/database

Platform-specific configuration:

  • Vercel: Project Settings → Environment Variables. Add variables for Production, Preview, and Development
  • Netlify: Site Settings → Build & Deploy → Environment. Set variables and deploy contexts
  • AWS/Render: Use platform-specific environment variable management UI
  • Railway: Add variables in project dashboard Variables tab

Webhook URL Configuration

Update your MessageBird webhook URL to point to your production domain:

  1. Deploy your application to get the production URL (e.g., https://your-app.vercel.app)
  2. Navigate to MessageBird Dashboard → Flow Builder
  3. Select your flow associated with your virtual number
  4. Update "Call HTTP endpoint with SMS" step
  5. Set webhook URL to: https://your-app.vercel.app/.redwood/functions/webhook
  6. Click Publish to save changes
  7. Test the webhook with a real SMS to verify connectivity

Zero-downtime updates: When deploying updates:

  1. Deploy new version to staging environment first
  2. Test webhook with staging URL
  3. Deploy to production
  4. Update webhook URL in MessageBird (< 1 minute downtime)
  5. Or use webhook URL routing to switch between versions

HTTPS Requirements

MessageBird requires HTTPS for all webhook endpoints.

Automatic HTTPS support:

  • Vercel, Netlify, and Railway provide automatic HTTPS with free SSL certificates
  • Custom domains require SSL certificate configuration (usually automatic)
  • Self-hosted: Use Let's Encrypt for free SSL certificates via Certbot

Verify HTTPS is working: curl -I https://your-domain.com/.redwood/functions/webhook

Database Migrations

Apply database migrations before deploying new code:

bash
# Production migration command (does not prompt)
yarn rw prisma migrate deploy

CI/CD pipeline configuration (GitHub Actions example):

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: yarn install

      - name: Run database migrations
        run: yarn rw prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - name: Build application
        run: yarn rw build

      - name: Deploy to Vercel
        run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}

Monitoring and Logging

Implement monitoring to track SMS delivery and webhook processing.

Log critical events:

  • Outbound SMS failures (API errors, invalid numbers)
  • Webhook signature verification failures (potential attacks)
  • Database write errors (connection issues, constraint violations)
  • MessageBird API errors (rate limiting, insufficient balance)
  • Webhook timeout errors (processing >30 seconds)

Key metrics to track:

  • SMS delivery success rate (delivered / sent)
  • Webhook processing latency (p50, p95, p99)
  • Error rate by type (authentication, validation, API)
  • API response times
  • Database query performance

Recommended monitoring tools:

  • Sentry: Error tracking and performance monitoring (sentry.io)
  • LogRocket: Session replay and error tracking for full-stack apps
  • Datadog: Application performance monitoring with dashboards
  • CloudWatch/Cloud Logging: Cloud platform native monitoring

RedwoodJS logging configuration:

typescript
// api/src/lib/logger.ts
import { createLogger } from '@redwoodjs/api/logger'

export const logger = createLogger({
  options: {
    level: process.env.LOG_LEVEL || 'info',
    redact: ['MESSAGEBIRD_ACCESS_KEY', 'MESSAGEBIRD_SIGNING_KEY'],
    prettyPrint: process.env.NODE_ENV === 'development'
  }
})

Use in your code:

typescript
import { logger } from 'src/lib/logger'

logger.info({ messageId: response.id }, 'SMS sent successfully')
logger.error({ error: err }, 'Webhook signature verification failed')

Rate Limiting

Implement rate limiting to prevent API abuse and protect your budget:

bash
# Install rate limiting library
yarn workspace api add limiter
typescript
// api/src/functions/webhook/webhook.ts
import { RateLimiterMemory } from 'rate-limiter-flexible';

// Allow 100 webhook requests per minute per IP
const rateLimiter = new RateLimiterMemory({
  points: 100,
  duration: 60,
});

export const handler = async (event, context) => {
  // Extract IP address
  const ip = event.headers['x-forwarded-for']?.split(',')[0] ||
             event.requestContext?.identity?.sourceIp ||
             'unknown';

  try {
    // Consume 1 point per request
    await rateLimiter.consume(ip);
  } catch (rejRes) {
    console.warn(`Rate limit exceeded for IP: ${ip}`);
    return {
      statusCode: 429,
      body: JSON.stringify({ error: 'Too many requests' })
    };
  }

  // ... existing webhook code ...
};

Rate limit recommendations:

  • Webhooks: 100 requests/minute per IP (adjust based on expected traffic)
  • SMS sending: 10 requests/minute per user (prevent abuse)
  • MessageBird API limits: 500 POST requests/second (SMS API) (official documentation)

CRITICAL: SMS marketing is heavily regulated. Non-compliance can result in fines of $500+ per message.

TCPA Compliance (United States)

The Telephone Consumer Protection Act (TCPA) requires:

  1. Obtain Express Written Consent: Get explicit written permission before sending marketing SMS. Customers must opt-in via web form, keyword, or checkbox (TCPA guide)

  2. Provide Clear Disclosure: After opt-in, send a disclosure message:

    [Your Business]: You've subscribed to marketing messages. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel, HELP for help. Terms: yoursite.com/terms
  3. Honor Opt-Out Requests: Process STOP, UNSUBSCRIBE, CANCEL, END, and QUIT keywords within 10 business days (as of April 2025) (TCPA 2025 updates)

  4. Respect Quiet Hours: Don't send messages before 8:00 AM or after 9:00 PM recipient's local time

  5. Include Business Name: Every message must identify your business

  6. Maintain Do-Not-Contact List: Keep records of opt-outs for at least 4 years

Example opt-out implementation:

typescript
// api/src/functions/webhook/webhook.ts
const handleOptOut = async (sender: string, messageBody: string) => {
  const optOutKeywords = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT'];

  if (optOutKeywords.includes(messageBody.trim().toUpperCase())) {
    // Add to do-not-contact list
    await db.optOut.create({
      data: { phoneNumber: sender, optedOutAt: new Date() }
    });

    // Send confirmation
    await sendSMS({
      to: sender,
      body: 'You have been unsubscribed. You will not receive further messages.',
      originator: process.env.MESSAGEBIRD_ORIGINATOR_NUMBER
    });

    return true;
  }

  return false;
};

International Regulations

  • GDPR (EU): Obtain consent, provide privacy policy, honor deletion requests
  • Canada (CASL): Requires express consent for commercial messages
  • Australia (Spam Act): Consent required, must include business identity and unsubscribe method

Cost Considerations

MessageBird uses pay-as-you-go pricing:

  • SMS pricing varies by country (typically $0.02-$0.10 per message)
  • Concatenated messages (>160 characters) cost multiple credits
  • Virtual numbers have monthly fees ($1-$10/month depending on country)
  • Check current pricing: messagebird.com/pricing

10. Conclusion

You have successfully integrated two-way SMS messaging into your RedwoodJS application using MessageBird. You can send messages via GraphQL and receive incoming messages through a secure webhook function, with all communication logged to your database.

What you built:

  • Outbound SMS via GraphQL with validation and error handling
  • Inbound SMS webhook with signature verification and replay attack prevention
  • Message logging with database persistence and idempotency
  • Security best practices including environment variable management and rate limiting

Next steps:

  • Implement auto-responses based on message content keywords
  • Add message templates for common notifications
  • Build conversation threading and history UI
  • Integrate with user authentication system
  • Add delivery status tracking via MessageBird status reports
  • Implement SMS-based two-factor authentication (2FA)
  • Set up monitoring and alerting for delivery failures

Related topics:

Prioritize security (especially webhook verification), handle errors gracefully, comply with TCPA and international regulations, and monitor SMS costs and delivery rates.

Resources: