This guide provides a step-by-step walkthrough for integrating Infobip's SMS capabilities into a RedwoodJS application, enabling both sending outbound messages and receiving inbound messages via webhooks for a complete two-way communication system.
We'll build a RedwoodJS application that can:
- Send SMS messages programmatically using the Infobip Node.js SDK.
- Receive incoming SMS messages sent to your Infobip number via a RedwoodJS API function configured as a webhook endpoint.
- Store message history (both inbound and outbound) in a database using Prisma.
This setup solves the common need for applications to interact with users via SMS for notifications, alerts, verification, or conversational features, while leveraging the robust structure and developer experience of RedwoodJS.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the Jamstack. Provides structure for API (GraphQL/REST), services, and database interaction (Prisma).
- Node.js: The runtime environment for RedwoodJS's backend API.
- Infobip: A cloud communications platform providing SMS API services. We'll use their Node.js SDK.
- Prisma: A next-generation ORM used by RedwoodJS for database modeling and access.
- PostgreSQL (or other Prisma-compatible DB): For storing message logs.
Prerequisites:
- Node.js (LTS version recommended) and Yarn installed.
- An active Infobip account (a free trial account works, but has limitations).
- Your Infobip API Key and Base URL.
- A provisioned phone number within your Infobip account capable of sending and receiving SMS.
- Basic understanding of RedwoodJS concepts (API functions, services, Prisma). Users less familiar might want to review the official RedwoodJS documentation first.
- A way to expose your local development server to the internet for webhook testing (e.g., ngrok, cloudflared tunnel).
System Architecture:
graph LR
A[User/Client] --> B(RedwoodJS Frontend);
B --> C{RedwoodJS API};
C -- Send SMS Request --> D[Infobip Service];
D -- Use SDK --> E[Infobip API];
E -- Sends SMS --> F[End User's Phone];
F -- Sends Reply --> E;
E -- Inbound Webhook POST --> C;
C -- Store/Process Message --> G[Database (Prisma)];
D -- Store Outbound Status --> G;
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#ccf,stroke:#333,stroke-width:2px
style G fill:#9cf,stroke:#333,stroke-width:2px
style E fill:#f96,stroke:#333,stroke-width:2px
Ensure your target platform supports Mermaid rendering for this diagram.
Final Outcome:
By the end of this guide, you will have a functional RedwoodJS application capable of sending SMS messages via an API call and automatically receiving and processing inbound SMS replies sent to your Infobip number, storing relevant details in your database.
1. Setting up the RedwoodJS Project
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
-
Create a new RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-infobip-sms --typescript cd redwood-infobip-sms
This command scaffolds a new RedwoodJS project with TypeScript enabled in the
redwood-infobip-sms
directory. -
Install Infobip Node.js SDK: Navigate to the
api
directory and install the SDK:cd api yarn add @infobip-api/sdk cd ..
-
Configure Environment Variables: RedwoodJS uses
.env
files for environment variables. Create a.env
file in the project's root directory:touch .env
Add your Infobip credentials and a secret for webhook validation:
# .env INFOBIP_BASE_URL=<YOUR_INFOBIP_BASE_URL> # e.g., yggdd4.api.infobip.com INFOBIP_API_KEY=<YOUR_INFOBIP_API_KEY> INFOBIP_WEBHOOK_SECRET=<generate_a_strong_random_secret_string> # Used to verify incoming webhooks # Optional: Specify the Infobip number you are sending *from* if needed globally # INFOBIP_SENDER_ID=<YOUR_INFOBIP_PHONE_NUMBER_OR_SENDER_NAME>
INFOBIP_BASE_URL
/INFOBIP_API_KEY
: Find these in your Infobip account dashboard (usually under API Keys or similar).INFOBIP_WEBHOOK_SECRET
: Generate a strong, unique random string (e.g., using a password manager oropenssl rand -hex 32
). This secret will be shared with Infobip to verify webhook authenticity.
-
Initialize Database (Prisma): RedwoodJS uses Prisma. By default, it's configured for PostgreSQL. You can change the provider in
api/db/schema.prisma
if needed (e.g., SQLite for simple testing). Ensure your database connection string is correctly set in the.env
file (Redwood creates aDATABASE_URL
variable).# .env (add this if not present or using a different DB) DATABASE_URL="postgresql://user:password@host:port/database"
For this guide, we'll proceed with the default PostgreSQL setup assumption.
2. Implementing Core Functionality (Outbound SMS)
We'll create a RedwoodJS service to handle interactions with the Infobip SDK and an API function to expose this functionality.
-
Define Database Schema for Messages: Update
api/db/schema.prisma
to include a model for storing SMS messages:// api/db/schema.prisma datasource db { provider = ""postgresql"" // Or your chosen provider url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" } model SmsMessage { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt direction String // ""INBOUND"" or ""OUTBOUND"" sender String // Phone number (E.164 format) recipient String // Phone number (E.164 format) body String? // Message content status String? // Status from Infobip (e.g., PENDING_ACCEPTED, DELIVERED) infobipMessageId String? @unique // Infobip's ID for the message infobipBulkId String? // Infobip's ID if part of a bulk send processed Boolean @default(false) // Flag for inbound processing errorMessage String? // Store errors if sending/processing failed }
-
Apply Database Migrations: Run the migration command to apply the schema changes to your database:
yarn rw prisma migrate dev --name add-sms-message-model
This creates the
SmsMessage
table. -
Generate Infobip Service: Use Redwood's generator to create a service file:
yarn rw g service infobip --no-crud
This creates
api/src/services/infobip/infobip.ts
. -
Implement
sendSms
in the Service: Openapi/src/services/infobip/infobip.ts
and add the logic to send SMS messages using the SDK and store the record.// api/src/services/infobip/infobip.ts import { Infobip, AuthType } from '@infobip-api/sdk' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' // Initialize Infobip client instance let infobipClient: Infobip | null = null const getInfobipClient = () => { if (!infobipClient) { if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) { logger.error('Infobip Base URL or API Key not configured in .env') throw new Error('Infobip credentials not configured.') } // Verify AuthType.ApiKey is the recommended method in current Infobip SDK docs infobipClient = new Infobip({ baseUrl: process.env.INFOBIP_BASE_URL, apiKey: process.env.INFOBIP_API_KEY, authType: AuthType.ApiKey, }) logger.info('Infobip client initialized.') } return infobipClient } interface SendSmsArgs { to: string // Recipient phone number (E.164 format) text: string from?: string // Optional: Sender ID or number (defaults to INFOBIP_SENDER_ID or Infobip default) } // Define the structure expected for the GraphQL response interface SendSmsResponse { success: boolean infobipMessageId?: string status?: string errorMessage?: string dbId: string // ID of the created database record } export const sendSms = async ({ to, text, from, }: SendSmsArgs): Promise<SendSmsResponse> => { const client = getInfobipClient() const senderId = from || process.env.INFOBIP_SENDER_ID // Use provided 'from', fallback to env var logger.info({ recipient: to, sender: senderId }, `Attempting to send SMS`) let dbRecord // To store the DB record ID even on failure try { // --- IMPORTANT: Verify SDK Method and Payload --- // Ensure `client.channels.sms.send` and its payload structure ({ messages: [...] }) // match the current official Infobip Node.js SDK documentation. // API details can change between SDK versions. // --- const infobipResponse = await client.channels.sms.send({ messages: [ { destinations: [{ to }], from: senderId, // Optional: Can be configured in Infobip portal too text, }, ], }) logger.info( { response: infobipResponse.data }, 'Infobip SMS send response received' ) // --- IMPORTANT: Verify Response Structure --- // The parsing logic below (`infobipResponse.data.messages?.[0]`) assumes a specific // structure for the success response. Verify this against the actual API response // documented by Infobip for the SMS send endpoint. // --- const messageResult = infobipResponse.data.messages?.[0] // Store successful send attempt in DB dbRecord = await db.smsMessage.create({ data: { direction: 'OUTBOUND', sender: senderId || 'UNKNOWN', // Best effort sender ID recipient: to, body: text, status: messageResult?.status?.name || 'UNKNOWN_STATUS', infobipMessageId: messageResult?.messageId, infobipBulkId: infobipResponse.data.bulkId, processed: true, // Mark as processed since it's outbound }, }) logger.info({ dbId: dbRecord.id }, 'Outbound SMS record created in DB') return { success: true, infobipMessageId: messageResult?.messageId, status: messageResult?.status?.name, dbId: dbRecord.id, } } catch (error) { logger.error({ error }, 'Error sending SMS via Infobip') // Log failed attempt in DB dbRecord = await db.smsMessage.create({ data: { direction: 'OUTBOUND', sender: senderId || 'UNKNOWN', recipient: to, body: text, status: 'SEND_FAILED', errorMessage: error.message || JSON.stringify(error), processed: true, }, }) logger.error({ dbId: dbRecord.id }, 'Failed outbound SMS record created') // Return a structured error object for the API layer return { success: false, status: 'SEND_FAILED', errorMessage: error.message || JSON.stringify(error), dbId: dbRecord.id, // Return the DB record ID even on failure } } } // We will add the webhook handler function here later
- Explanation:
- We initialize the
Infobip
client lazily using credentials from.env
. We added notes to verifyAuthType
and the SDK method/payload/response structure against official documentation. - The
sendSms
function takes recipient (to
), messagetext
, and an optionalfrom
sender ID. - It calls the Infobip SDK's
channels.sms.send
method (user must verify this). - It logs the result (success or failure) to the database (
SmsMessage
table) along with relevant IDs and status information returned by Infobip (user must verify response structure). - If an error occurs during the API call, it logs the failure to the DB and returns a structured error object (
{ success: false, ... }
) instead of throwing, which is suitable for GraphQL resolvers. - Proper logging using Redwood's
logger
is included.
- We initialize the
- Explanation:
-
Expose Service via API Function (GraphQL Example): Let's create a GraphQL mutation to trigger the
sendSms
service.yarn rw g sdl sms --no-crud
This generates
api/src/graphql/sms.sdl.ts
. Define the mutation:// api/src/graphql/sms.sdl.ts export const schema = gql` type SmsSendResponse { success: Boolean! infobipMessageId: String status: String errorMessage: String # Included for clarity on failure dbId: String! } type Mutation { sendSms(to: String!, text: String!, from: String): SmsSendResponse! @requireAuth # Consider adding input validation directives if needed } `
- Note: The
sendSms
service function implemented above now correctly matches this SDL, returning theSmsSendResponse
type including theerrorMessage
on failure. The@requireAuth
directive implies you have Redwood's authentication set up. Remove it if you want an unprotected endpoint for testing, but secure it for production.
- Note: The
3. Building API Layer (Inbound Webhook)
This is the core of two-way messaging – receiving messages from Infobip. We'll create a standard Redwood API function (REST-like) to act as the webhook receiver.
-
Generate Webhook API Function:
yarn rw g function infobipWebhook --no-auth
This creates
api/src/functions/infobipWebhook.ts
. The--no-auth
flag makes it publicly accessible, which is necessary for Infobip to reach it. We will add security manually via signature verification. -
Implement Webhook Handler Logic: Open
api/src/functions/infobipWebhook.ts
and implement the handler. This function needs to:- Verify the incoming request originated from Infobip (using the shared secret).
- Parse the incoming SMS data.
- Store the message in the database.
- Return an appropriate HTTP status code to Infobip.
// api/src/functions/infobipWebhook.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import crypto from 'crypto' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' /** * Verifies the signature of the incoming webhook request from Infobip. * THIS IS CRUCIAL FOR SECURITY. * * --- VERY IMPORTANT --- * The header name ('x-infobip-signature') and hashing algorithm ('sha256') used below * are COMMON DEFAULTS but **MUST BE VERIFIED** against the official Infobip documentation * for *SMS Inbound Webhooks*. Infobip might use a different header or algorithm (e.g., SHA1). * Update this function accordingly based on their specific requirements. * Failure to implement this correctly exposes your endpoint to fake requests. * --- /VERY IMPORTANT --- */ const verifyInfobipSignature = (event: APIGatewayEvent): boolean => { // >>> VERIFY THIS HEADER NAME WITH INFOBIP DOCS <<< const signatureHeader = event.headers['x-infobip-signature'] const secret = process.env.INFOBIP_WEBHOOK_SECRET if (!signatureHeader || !secret) { logger.warn('Missing signature header or webhook secret. Denying request.') return false } if (!event.body) { logger.warn('Missing request body for signature verification.') return false } try { // >>> VERIFY THIS HASHING ALGORITHM WITH INFOBIP DOCS <<< const hmac = crypto.createHmac('sha256'_ secret) const digest = Buffer.from( // The prefix 'sha256=' might also vary based on Infobip's format. Check docs. 'sha256=' + hmac.update(event.body).digest('hex')_ 'utf8' ) const checksum = Buffer.from(signatureHeader_ 'utf8') if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest_ checksum)) { logger.warn('Invalid webhook signature.') return false; } logger.info('Webhook signature verified successfully.') return true } catch (error) { logger.error({ error }_ 'Error during webhook signature verification.') return false } } /** * Processes inbound SMS messages received via Infobip webhook. */ export const handler = async (event: APIGatewayEvent_ _context: Context) => { logger.info('Infobip webhook received') logger.debug({ headers: event.headers, body: event.body }, 'Webhook details') // 1. Verify Signature (SECURITY CRITICAL) // This check MUST be active in production. Only comment out for specific, isolated debugging. const isVerified = verifyInfobipSignature(event) if (!isVerified) { logger.error('Webhook signature verification failed. Unauthorized.') return { statusCode: 401, // Unauthorized body: JSON.stringify({ error: 'Unauthorized. Invalid signature.' }), } } // If you absolutely MUST bypass temporarily for initial local testing *only*: // logger.warn('!!! Webhook signature verification is currently BYPASSED for testing !!!'); // 2. Check HTTP Method if (event.httpMethod !== 'POST') { logger.warn(`Invalid HTTP method: ${event.httpMethod}`) return { statusCode: 405, // Method Not Allowed headers: { Allow: 'POST' }, body: JSON.stringify({ error: 'Method Not Allowed' }), } } // 3. Parse Request Body let payload try { payload = JSON.parse(event.body || '{}') logger.info({ payload }, 'Parsed webhook payload') // --- VERY IMPORTANT: Verify Payload Structure --- // The exact structure of the 'payload' object depends entirely on Infobip's format // for inbound SMS webhooks. **Consult the official Infobip documentation.** // The structure assumed below (with a 'results' array) is an *example* and may be incorrect. // Example *assumed* structure (VERIFY THIS): // { // "results": [ // { // "messageId": "abc...", // "from": "15551234567", // "to": "15559876543", // "text": "Hello from user!", // "receivedAt": "2025-04-20T10:30:00Z", // "keyword": "...", // etc. // "status": { "groupName": "DELIVERED", "name": "DELIVERED_TO_HANDSET" } // Example status // // ... other fields provided by Infobip // } // // Potentially multiple messages in one webhook call // ], // "messageCount": 1, // "pendingMessageCount": 0 // } // --- /VERY IMPORTANT --- // Adjust this validation based on the *actual* payload structure from Infobip docs if (!payload.results || !Array.isArray(payload.results)) { throw new Error('Invalid payload structure: Missing or invalid "results" array.') } } catch (error) { logger.error({ error, body: event.body }, 'Failed to parse webhook body') return { statusCode: 400, // Bad Request body: JSON.stringify({ error: 'Invalid JSON payload' }), } } // 4. Process Each Message in the Payload try { // Adjust loop based on the *actual* payload structure (e.g., maybe it's not `payload.results`) for (const message of payload.results) { // Validate essential fields based on *actual* payload structure from Infobip docs if (!message.messageId || !message.from || !message.to || !message.text) { logger.warn({ message }, 'Skipping message due to missing essential fields (messageId, from, to, text)') continue // Skip this message, process others } // Check if message already processed (using Infobip's ID) - Idempotency check const existing = await db.smsMessage.findUnique({ where: { infobipMessageId: message.messageId }, }) if (existing) { logger.warn({ infobipMessageId: message.messageId }, 'Duplicate message received (based on messageId), skipping.') continue } // Store the inbound message await db.smsMessage.create({ data: { direction: 'INBOUND', sender: message.from, // Ensure format matches (E.164 expected) recipient: message.to, // Ensure format matches (E.164 expected) body: message.text, status: message.status?.name || 'RECEIVED', // Use status if available, verify field name infobipMessageId: message.messageId, processed: false, // Mark as unprocessed initially for potential async work // Add other relevant fields from the *actual* payload if needed (e.g., receivedAt, keyword) }, }) logger.info({ infobipMessageId: message.messageId }, 'Inbound message stored.') // --- Application Specific Logic --- // This is where you add your own business logic based on the incoming message. // Examples: // - Auto-reply based on keywords. // - Handle STOP/HELP commands for compliance. // - Update user state in your application. // - Trigger notifications to administrators. // - Route message to a support system. // Consider moving complex logic to an async job queue if processing takes time. // TODO: Add business logic here based on `message.text`, `message.from`, etc. // Example: if (message.text.toUpperCase() === 'STOP') { /* handle opt-out logic */ } // --- /Application Specific Logic --- } // 5. Respond to Infobip // A 2xx status tells Infobip you received the webhook successfully. // Failure to respond with 2xx may cause Infobip to retry the webhook. return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Webhook received successfully' }), } } catch (dbError) { logger.error({ error: dbError }, 'Database error processing webhook message(s)') // Respond with 500 if there's an internal server error during processing // Infobip might retry in this case. return { statusCode: 500, body: JSON.stringify({ error: 'Internal Server Error processing message' }), } } }
- Key Points:
- Webhook Security (
verifyInfobipSignature
): This is paramount. The function is now uncommented. Extremely strong warnings were added emphasizing the need to verify the header name, algorithm, and signature format against official Infobip documentation for SMS webhooks. - Payload Parsing & Structure: Similar strong warnings were added about verifying the incoming JSON payload structure against official Infobip documentation. The code's reliance on a
results
array is explicitly called out as an assumption that needs verification. - Idempotency: The check for existing messages using
infobipMessageId
remains crucial for handling potential webhook retries. - Database Storage: Creates a record in
SmsMessage
withdirection: 'INBOUND'
. - Business Logic Placeholder: The
// TODO:
comment remains, but the surrounding text now better explains its purpose as the integration point for application-specific actions. - Response Code: Returning
statusCode: 200
acknowledges successful receipt to Infobip. Non-2xx codes signal issues.
- Webhook Security (
4. Integrating with Infobip (Webhook Configuration)
Now, tell Infobip where to send incoming messages.
-
Expose Local Endpoint: For development, you need to expose your RedwoodJS API endpoint to the public internet. Tools like
ngrok
orcloudflared
can do this.- Example using ngrok:
- Install ngrok.
- Start your RedwoodJS dev server:
yarn rw dev
(It usually runs on port 8911 for the API). - In a new terminal, run:
ngrok http 8911
- Ngrok will give you a public HTTPS URL (e.g.,
https://abcdef123456.ngrok.io
). Your webhook endpoint URL will be this base URL plus the Redwood function path:https://abcdef123456.ngrok.io/infobipWebhook
- Production Alternatives: When deploying, platforms like Vercel or Netlify automatically provide stable public HTTPS URLs for your functions. Other managed tunnel services can also be used if required.
- Example using ngrok:
-
Configure Webhook in Infobip:
- Log in to your Infobip account portal.
- The Infobip dashboard layout can change, so precise navigation steps might vary. Look for sections related to your SMS Numbers, Apps, or API Settings. Common locations might be under ""Numbers"" -> select your number -> ""Forwarding"" or ""Messaging Settings"", or under an ""SMS"" application configuration -> ""Inbound Rules"" or ""Webhook Settings"".
- Find the setting to specify a URL for receiving incoming SMS messages. This might be labelled ""Incoming Messages URL"", ""MO Forwarding URL"", or similar.
- Enter the public URL of your webhook function (e.g., your ngrok URL for testing, or your production URL like
https://your-app.com/api/infobipWebhook
). Ensure it uses HTTPS. - Crucially: Look for a field to enter your Webhook Secret (the value of
INFOBIP_WEBHOOK_SECRET
from your.env
). This allows Infobip to calculate the signature it sends in the header, enabling yourverifyInfobipSignature
function to work. If Infobip does not provide a specific field for a shared secret for signature generation for SMS webhooks, you must consult their documentation on how they recommend securing inbound SMS webhooks (e.g., maybe they use Basic Auth, IP allow-listing, or another mechanism). Do not proceed without confirming Infobip's documented security method. - Save the configuration in the Infobip portal.
5. Error Handling, Logging, and Retry Mechanisms
- Error Handling:
- The service and API functions include
try...catch
blocks. ThesendSms
service now returns structured errors. - Errors during SDK calls or database operations are logged using
logger.error
. - The webhook handler returns specific HTTP status codes (400, 401, 405, 500) to indicate different error types to Infobip.
- Store meaningful error messages in the
errorMessage
field of theSmsMessage
model.
- The service and API functions include
- Logging:
- RedwoodJS's built-in Pino logger (
logger
) is used. Configure log levels inapi/src/lib/logger.ts
as needed (e.g.,level: 'debug'
for development). - Log key events: initialization, sending attempts, webhook reception, payload parsing, database operations, errors. Include relevant context (IDs, payload snippets if safe).
- RedwoodJS's built-in Pino logger (
- Retry Mechanisms:
- Infobip Webhook Retries: Infobip typically retries sending webhooks if your endpoint doesn't respond with a 2xx status within a certain timeout. Ensure your webhook handler is reasonably fast and handles errors gracefully (returning correct status codes) to avoid unnecessary retries. The idempotency check (
findUnique
byinfobipMessageId
) handles duplicates caused by retries. - Outbound Send Retries: For critical outbound messages, you might implement retries within your
sendSms
service (e.g., using libraries likeasync-retry
with exponential backoff) if the initial Infobip API call fails due to transient network issues or temporary Infobip problems (e.g., 5xx errors from their API).
- Infobip Webhook Retries: Infobip typically retries sending webhooks if your endpoint doesn't respond with a 2xx status within a certain timeout. Ensure your webhook handler is reasonably fast and handles errors gracefully (returning correct status codes) to avoid unnecessary retries. The idempotency check (
6. Database Schema and Data Layer
- Schema: The
SmsMessage
model inschema.prisma
(defined in Section 2) provides the structure. - Data Layer: Prisma Client (
db
imported fromsrc/lib/db
) is used in the service and webhook function for database operations (create
,findUnique
). - Migrations:
yarn rw prisma migrate dev
handles schema changes. Ensure you commit migration files (api/db/migrations/*
) to version control. - Performance: For high volume, ensure indexes are present on commonly queried fields like
infobipMessageId
,sender
,recipient
, andcreatedAt
. Prisma adds an index on@id
and@unique
fields automatically. Add@index([sender, createdAt])
if you query by sender frequently.
7. Security Features
- API Key Security: Store
INFOBIP_API_KEY
andINFOBIP_BASE_URL
securely in.env
and never commit them to version control. Use environment variable management in your deployment environment. - Webhook Security:
- Signature Verification: This is the most critical part (covered in Section 3). Implement and test it thoroughly based on Infobip's official documentation. Verify the header name, algorithm, and secret configuration.
- HTTPS: Always use HTTPS for your webhook endpoint URL. Ngrok and production hosting providers typically handle this.
- IP Filtering (Allow-listing): If Infobip publishes a list of IPs they send webhooks from, you could configure your firewall or infrastructure (e.g., AWS WAF, Cloudflare firewall rules) to only allow requests from those IPs as an additional layer of security. This is generally less flexible and robust than signature verification.
- Input Validation:
- The webhook handler performs basic checks on the payload structure (user must verify expected structure).
- For the GraphQL
sendSms
mutation, Redwood automatically provides some input type validation. Add more specific validation (e.g., phone number format using a library likelibphonenumber-js
) in your service if needed. - Sanitize any user input used in responses to prevent injection attacks if you build conversational logic.
- Rate Limiting:
- Protect both your
sendSms
endpoint and potentially the webhook endpoint from abuse or accidental loops. RedwoodJS doesn't have built-in rate limiting for API functions/GraphQL. Implement it using:- Infrastructure-level services (Cloudflare Rate Limiting, API Gateway usage plans).
- Third-party Node.js libraries implementing algorithms like token bucket or fixed window counter (e.g.,
rate-limiter-flexible
,express-rate-limit
adapted for serverless contexts). Search for libraries compatible with your deployment target (e.g., "rate limiting middleware for AWS Lambda" or specific solutions for Vercel/Netlify Edge Functions).
- Protect both your
- Authentication: The example
sendSms
GraphQL mutation uses@requireAuth
. Ensure proper authentication and authorization are enforced on endpoints that trigger actions or expose sensitive data.
8. Handling Special Cases
- Phone Number Formatting: Always strive to store and handle phone numbers in E.164 format (e.g.,
+15551234567
). Use libraries likelibphonenumber-js
for parsing and validation if necessary, especially for user-provided input. - Message Encoding & Length: Standard SMS messages have length limits (160 characters for GSM-7, fewer for UCS-2/Unicode). Infobip typically handles concatenation for longer messages, but be aware of billing implications (multiple message segments). Ensure text content doesn't contain unsupported characters or handle encoding explicitly via Infobip API options if needed (SDK defaults are often sufficient).
- STOP/HELP Keywords: Implement handling for standard keywords like STOP (opt-out) and HELP as required by regulations (e.g., TCPA in the US) and carrier guidelines. Your webhook logic (
// TODO: Add business logic here
) should parse inbound messages for these keywords and update user preferences or trigger appropriate responses.