Developer Guide: Implementing RedwoodJS SMS with Vonage Delivery Status
This guide provides a step-by-step walkthrough for building a RedwoodJS application capable of sending SMS messages via the Vonage Messages API, receiving inbound SMS messages, and handling delivery status callbacks. We'll cover everything from project setup and Vonage configuration to database integration, error handling, and deployment considerations.
By the end of this tutorial, you will have a functional RedwoodJS application with API endpoints to send SMS, and webhooks correctly configured to process incoming messages and delivery status updates from Vonage, storing relevant information in a database.
Project Overview and Goals
We aim to build a robust system within a RedwoodJS application that leverages the Vonage Messages API for SMS communication. This solves the common need for applications to programmatically send notifications, alerts, or engage in two-way conversations via SMS, while also providing visibility into message delivery success or failure.
Key Features:
- Send outbound SMS messages via a RedwoodJS API function.
- Receive inbound SMS messages directed to a Vonage virtual number.
- Receive delivery status updates for outbound messages.
- Store message details and statuses in a database (using Prisma).
- Utilize webhooks for real-time updates from Vonage.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. Chosen for its integrated structure (React frontend, GraphQL API, Prisma ORM) and developer experience features.
- Node.js: The runtime environment for the RedwoodJS API side.
- Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. Chosen for its flexibility and features like delivery receipts.
@vonage/server-sdk
: The official Vonage Node.js SDK for simplified API interaction.- Prisma: RedwoodJS's default ORM for database interaction.
ngrok
: A tool to expose local development servers to the internet, essential for testing webhooks.
System Architecture:
(Note: For a final published version, consider replacing this ASCII diagram with a clearer graphical representation.)
+-----------------+ +-----------------+ +-----------------+ +----------+ +-----------+
| User / Frontend | ---> | RedwoodJS API | ---> | Vonage Platform | ---> | Carrier | ---> | End User |
| (React) | | (Node.js/GraphQL| | (Messages API) | | Network | | Phone |
| | | /Prisma) | <--- | | <--- | | <--- | |
+-----------------+ +-----------------+ +-----------------+ +----------+ +-----------+
| ^ ^ |
| Send SMS Request | | Inbound SMS / Status Update |
+----------------------+ +--(Webhook via ngrok/Public URL)-----------+
|
+-----------------+
| Database |
| (PostgreSQL/etc)|
+-----------------+
Prerequisites:
- Node.js (LTS version recommended) and Yarn installed.
- A Vonage API account (Sign up for free).
- A Vonage virtual phone number capable of sending/receiving SMS.
ngrok
installed and authenticated (Download ngrok).
1. Setting Up the Project
Let's initialize a new RedwoodJS project and configure our environment.
-
Create RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-vonage-sms cd redwood-vonage-sms
This scaffolds a new RedwoodJS project in the
redwood-vonage-sms
directory. -
Install Vonage SDK: Navigate to the API workspace and install the Vonage Node.js SDK:
yarn workspace api add @vonage/server-sdk
-
Configure Vonage Account:
- API Credentials: Log in to your Vonage API Dashboard. Find your API Key and API Secret on the main page. Note: While visible_ this guide uses Application ID/Private Key authentication for the Messages API.
- Create Vonage Application:
- Navigate to Applications -> Create a new application.
- Give it a name (e.g.,
"Redwood SMS App"
). - Click
"Generate public and private key"
. Save theprivate.key
file securely, for example, in the root directory of your Redwood project (outside theapi
orweb
folders). Do not commit this file to Git. - Enable the Messages capability.
- For now, you can leave the
"Inbound URL"
and"Status URL"
blank or use placeholders likehttp://example.com/inbound
. We'll update these withngrok
URLs later. - Click
"Generate application"
. Note down the Application ID.
- Link Number: Go back to the Application settings, scroll down to
"Link virtual numbers"
, and link your purchased Vonage virtual number to this application. - Set Default SMS API: Important: Navigate to your main Vonage Dashboard Settings. Scroll down to
"SMS settings"
. Ensure that the API preference is set to Messages API. This is crucial for the webhooks and SDK usage in this guide. Click"Save changes"
. (Ensure this setting is correct in your dashboard; it determines how SMS features behave).
-
Environment Variables: Create a
.env
file in the root of your RedwoodJS project (alongsideredwood.toml
). Add your Vonage credentials and number:# .env # Vonage Credentials (Used for SDK initialization with Messages API) VONAGE_APPLICATION_ID="YOUR_VONAGE_APPLICATION_ID" # Path relative to project root. Ensure this resolves correctly at runtime (see sendSms.ts). VONAGE_PRIVATE_KEY_PATH="./private.key" # Vonage API Key/Secret (Not used for Messages API auth in this guide, but potentially for other Vonage APIs) # VONAGE_API_KEY="YOUR_VONAGE_API_KEY" # VONAGE_API_SECRET="YOUR_VONAGE_API_SECRET" # Your Vonage Virtual Number VONAGE_NUMBER="YOUR_VONAGE_VIRTUAL_NUMBER" # e.g., 14155551212
- Replace placeholders with your actual Application ID and Vonage number.
- Ensure
VONAGE_PRIVATE_KEY_PATH
points correctly to where you saved theprivate.key
file relative to the project root. Note the potential runtime path resolution issue discussed later. - Crucially: Add
.env
andprivate.key
to your.gitignore
file to prevent committing secrets.
# .gitignore # ... other entries .env private.key api/db/dev.db # Example for SQLite, adjust as needed api/db/dev.db-journal *.key
-
Setup
ngrok
: Webhooks require a publicly accessible URL.ngrok
tunnels requests to your local machine.-
Open a new terminal window in your project root.
-
Run
ngrok
to forward to Redwood's default API port (8911):ngrok http 8911
-
ngrok
will display forwarding URLs (e.g.,https://<unique-code>.ngrok-free.app
). Copy thehttps
URL. (Check your terminal for output similar to the example shown in ngrok documentation). -
Note:
ngrok
is for development/testing. For production, you'll need a stable public URL for your deployed API endpoint, managed via your hosting platform's ingress, load balancer, or direct server exposure.
-
-
Update Vonage Webhook URLs:
- Go back to your Vonage Application settings.
- Set the Inbound URL to
YOUR_NGROK_HTTPS_URL/api/webhooks/inbound
. - Set the Status URL to
YOUR_NGROK_HTTPS_URL/api/webhooks/status
. - Ensure the HTTP method for both is set to
POST
. - Save the changes.
Now, Vonage will send POST requests to these
ngrok
URLs, which will be forwarded to your local RedwoodJS API running on port 8911.
2. Database Schema and Data Layer
We need a way to store information about the SMS messages we send and receive.
-
Define Prisma Schema: Open
api/db/schema.prisma
and add the following model:// api/db/schema.prisma datasource db { provider = ""sqlite"" // Or postgresql, mysql url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" } model SmsMessage { id String @id @default(cuid()) vonageMessageId String? @unique // Vonage's message_uuid direction String // 'inbound' or 'outbound' fromNumber String toNumber String body String? status String // e.g., 'submitted', 'delivered', 'failed', 'received', 'rejected', 'accepted' errorCode String? // Vonage error code if status is 'failed' or 'rejected' statusTimestamp DateTime? // Timestamp from the status webhook createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Add indexes for frequently queried fields if needed beyond vonageMessageId // index([status]) // index([createdAt]) }
vonageMessageId
: Stores the unique ID returned by Vonage, crucial for correlating status updates. The@unique
attribute automatically creates a database index.direction
: Tracks if the message was sent or received by our app.status
: Holds the latest known status.errorCode
: Stores error details if delivery fails.
-
Apply Migrations: Run the Prisma migration command to apply these changes to your database:
yarn rw prisma migrate dev --name create_sms_message
This creates/updates the database schema and generates the Prisma client.
3. Building the API Layer (RedwoodJS Functions)
RedwoodJS Functions are serverless functions deployed alongside your API. We'll create functions to send SMS and handle incoming webhooks.
> Important Production Consideration: Vonage webhooks expect a quick 200 OK
response (within a few seconds). Performing database operations directly within the webhook handler, as shown here for simplicity, is risky in production. If the database write fails temporarily, you'll return 200 OK
(to prevent Vonage retries), but the data might be lost. For production, strongly consider using a background job queue (e.g., BullMQ, Redis Streams) to process webhook payloads asynchronously. This allows the handler to return 200 OK
immediately while ensuring reliable data processing.
-
Send SMS Function: Generate a function to handle sending messages:
yarn rw g function sendSms
Implement the logic in
api/src/functions/sendSms.ts
(or.js
):// api/src/functions/sendSms.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { Vonage } from '@vonage/server-sdk' // For better type safety, import specific types: // import { MessagesSendRequest } from '@vonage/messages' import path from 'path' // Required for resolving private key path import fs from 'fs' // Needed if reading key content from env var import { db } from 'src/lib/db' // Redwood's Prisma client import { logger } from 'src/lib/logger' // --- Private Key Handling --- // Option 1: Resolve path (Default in this guide) // This path is relative to the *built* API output directory (e.g., api/dist), // NOT the source directory. Adjust the relative path ('../private.key') if needed based on your build structure. const privateKeyPath = process.env.VONAGE_PRIVATE_KEY_PATH ? path.resolve(process.cwd(), process.env.VONAGE_PRIVATE_KEY_PATH) : path.resolve(process.cwd(), '../private.key') // Default assumption // Option 2: Read key content directly from environment variable (Recommended for deployment) // const privateKeyContent = process.env.VONAGE_PRIVATE_KEY_CONTENT; // if (!privateKeyContent) { // logger.error('VONAGE_PRIVATE_KEY_CONTENT environment variable is not set.'); // throw new Error('Missing private key configuration.'); // } // Initialize Vonage client using Application ID and Private Key for Messages API const vonage = new Vonage( { applicationId: process.env.VONAGE_APPLICATION_ID, // Use Option 1 (Path) or Option 2 (Content) privateKey: privateKeyPath, // For Option 1 // privateKey: privateKeyContent, // For Option 2 }, { logger: logger } // Optional: Use Redwood's logger ) export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Received sendSms request') if (event.httpMethod !== 'POST') { return { statusCode: 405, body: 'Method Not Allowed' } } let requestBody try { requestBody = JSON.parse(event.body || '{}') } catch (error) { logger.error('Failed to parse request body:', error) return { statusCode: 400, body: 'Bad Request: Invalid JSON' } } const { to, text } = requestBody const from = process.env.VONAGE_NUMBER if (!to || !text || !from) { logger.error('Missing required parameters: to, text, or VONAGE_NUMBER') return { statusCode: 400, body: 'Bad Request: Missing "to", "text" parameters, or VONAGE_NUMBER configuration.', } } logger.info(`Attempting to send SMS from ${from} to ${to}`) let initialDbRecord try { // Create an initial record before sending initialDbRecord = await db.smsMessage.create({ data: { direction: 'outbound', fromNumber: from, toNumber: to, body: text, status: 'submitted', // Initial status }, }) logger.info(`Created initial DB record with ID: ${initialDbRecord.id}`) // Prepare the payload. Use specific types for better safety. const messagePayload /*: MessagesSendRequest */ = { channel: 'sms', message_type: 'text', to: to, from: from, text: text, // client_ref: initialDbRecord.id // Optional: include DB ID for correlation } // Using 'as any' for simplicity in this example. // Replace with the actual type (e.g., MessagesSendRequest) for production code. const resp = await vonage.messages.send(messagePayload as any) logger.info(`Vonage API response: ${JSON.stringify(resp)}`) // Update the DB record with the Vonage message ID await db.smsMessage.update({ where: { id: initialDbRecord.id }, data: { vonageMessageId: resp.message_uuid }, }) logger.info(`SMS submitted successfully to Vonage. Message UUID: ${resp.message_uuid}`) return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, message: 'SMS submitted successfully.', messageId: resp.message_uuid, // Vonage's ID dbId: initialDbRecord.id, // Your internal DB ID }), } } catch (error) { logger.error('Error sending SMS via Vonage or updating DB:', error) // Attempt to update the DB record status to 'failed' if possible if (initialDbRecord) { try { await db.smsMessage.update({ where: { id: initialDbRecord.id }, data: { status: 'failed_submission' }, // Indicate failure during submission }) } catch (dbError) { logger.error('Failed to update DB record status after submission error:', dbError) } } // Check if the error is from the Vonage SDK and has specific details let errorMessage = 'Failed to send SMS.' // Type assertion needed as 'error' is 'unknown' in catch block const vonageError = error as any; if (vonageError?.response?.data) { errorMessage = `Vonage API Error: ${JSON.stringify(vonageError.response.data)}`; } else if (vonageError?.message) { errorMessage = vonageError.message; } return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: false, message: errorMessage, errorDetails: error.toString(), // Provide generic error string dbId: initialDbRecord?.id, }), } } }
- Key Points:
- We initialize Vonage using the Application ID and Private Key (either path or content). Path resolution needs care during build/deployment.
- We parse the
to
number andtext
from the POST request body. - We immediately create a database record with
status: 'submitted'
. - We call
vonage.messages.send()
. Using specific SDK types likeMessagesSendRequest
is recommended overas any
. - On success, we update the database record with the
vonageMessageId
returned by the API. - We include robust error handling and logging.
- Key Points:
-
Inbound SMS Webhook: Generate a function to handle incoming messages:
# Note: Redwood maps the path to the function name by default # We specify the path explicitly to match the Vonage configuration yarn rw g function webhooks --path /webhooks/inbound
Implement the logic in
api/src/functions/webhooks/inbound.ts
:// api/src/functions/webhooks/inbound.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Inbound webhook received') if (event.httpMethod !== 'POST') { logger.warn('Received non-POST request to inbound webhook') return { statusCode: 405, body: 'Method Not Allowed' } } let body try { // Redwood typically parses application/json automatically into event.body. // Check Content-Type header (via logs/ngrok) if Vonage sends urlencoded. // If urlencoded and not auto-parsed, add parsing logic here. body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body logger.debug({ custom: body }, 'Inbound webhook payload') if (!body || !body.message_uuid || !body.from || !body.to || !body.text) { logger.error('Inbound webhook missing required fields.') // Still return 200 OK to prevent Vonage retries return { statusCode: 200 } } // Store the inbound message // Production recommendation: Offload this DB write to a background queue. await db.smsMessage.create({ data: { vonageMessageId: body.message_uuid, direction: 'inbound', fromNumber: body.from, toNumber: body.to, // Your Vonage number body: body.text, status: 'received', // Status for inbound statusTimestamp: body.timestamp ? new Date(body.timestamp) : new Date(), createdAt: new Date(), // Use current time or webhook timestamp if reliable }, }) logger.info(`Stored inbound SMS from ${body.from} with message ID ${body.message_uuid}`) } catch (error) { logger.error('Error processing inbound webhook (e.g., DB write failed):', error) // Still return 200 OK even if DB write fails to stop Vonage retries. // Implement proper error monitoring/alerting and consider queuing for reliability. } // IMPORTANT: Always return 200 OK quickly to acknowledge receipt by Vonage return { statusCode: 200 } }
- Key Points:
- The function listens at
/api/webhooks/inbound
(matching Vonage config). - It expects a
POST
request. - It parses the request body (check
Content-Type
if issues arise). - It extracts relevant fields (
message_uuid
,from
,to
,text
,timestamp
). - It creates a new record in the
SmsMessage
table withdirection: 'inbound'
andstatus: 'received'
. - It must return a
200 OK
status code promptly. Offload heavy processing (like DB writes) to queues in production.
- The function listens at
- Key Points:
-
Status Update Webhook: Generate a function to handle delivery status updates:
yarn rw g function webhooks --path /webhooks/status
Implement the logic in
api/src/functions/webhooks/status.ts
:// api/src/functions/webhooks/status.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Status webhook received') if (event.httpMethod !== 'POST') { logger.warn('Received non-POST request to status webhook') return { statusCode: 405, body: 'Method Not Allowed' } } let body try { // Similar parsing logic as inbound webhook body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body logger.debug({ custom: body }, 'Status webhook payload') if (!body || !body.message_uuid || !body.status) { logger.error('Status webhook missing required fields.') // Still return 200 OK return { statusCode: 200 } } const { message_uuid, status, timestamp, error } = body // Verify the exact structure of the 'error' object against Vonage docs if needed. const errorCode = error?.['error_code'] ?? error?.['code'] ?? null; // Find the corresponding outbound message and update its status // Production recommendation: Offload this DB update to a background queue. const updatedMessage = await db.smsMessage.updateMany({ where: { vonageMessageId: message_uuid, direction: 'outbound', // Ensure we only update outbound messages }, data: { status: status, // e.g., 'delivered', 'failed', 'rejected', 'accepted' statusTimestamp: timestamp ? new Date(timestamp) : new Date(), errorCode: errorCode, // Capture error code if present updatedAt: new Date(), }, }) if (updatedMessage.count > 0) { logger.info(`Updated status for message ${message_uuid} to ${status}`) } else { logger.warn(`Could not find outbound message with Vonage ID ${message_uuid} to update status. (Might be an inbound status, or timing issue)`) // This might happen if the status arrives before the sendSms function updates the DB, // or if it's a status for an inbound message (which we usually ignore here). } } catch (error) { logger.error('Error processing status webhook (e.g., DB update failed):', error) // Still return 200 OK. Implement monitoring/alerting and consider queuing. } // IMPORTANT: Always return 200 OK quickly return { statusCode: 200 } }
- Key Points:
- Listens at
/api/webhooks/status
. - Expects
POST
. - Parses the body to get
message_uuid
,status
,timestamp
, and potentialerror
details. Verify error structure in Vonage docs. - Uses
db.smsMessage.updateMany
with thevonageMessageId
to find the original outbound message. - Updates the
status
,statusTimestamp
, anderrorCode
fields. - Logs whether the update was successful or if the message wasn't found.
- Must return
200 OK
promptly. Offload heavy processing to queues in production.
- Listens at
- Key Points:
4. Integrating with Vonage (Recap & Details)
- SDK Initialization: Done within the
sendSms
function usingnew Vonage({ applicationId, privateKey })
. Ensure theprivateKey
source (path or env var content) is correctly configured and accessible at runtime. The path resolution (path.resolve(process.cwd(), ...)
insendSms.ts
) needs careful testing, especially after building the API (yarn rw build api
), asprocess.cwd()
will point to the build output directory (e.g.,api/dist
). Consider using environment variable content or copying the key during build for robustness. - API Keys/Secrets: Handled via
.env
file for local development. Never commit these files. Use environment variable management provided by your deployment platform for production. The Messages API specifically uses Application ID and Private Key for authentication in this setup. - Dashboard Configuration:
- Application created with Messages capability enabled.
- Correct Vonage number linked to the application.
- Webhook URLs (
/api/webhooks/inbound
,/api/webhooks/status
) correctly set using your public URL (ngrok
for dev, actual domain for prod). Method must bePOST
. - Account default SMS setting configured to Messages API.
- Environment Variables:
VONAGE_APPLICATION_ID
: Your Vonage application's unique ID.VONAGE_PRIVATE_KEY_PATH
: Relative path from the project root to your savedprivate.key
file (if using path method).VONAGE_NUMBER
: Your purchased Vonage virtual number in E.164 format (e.g., 14155551212).VONAGE_PRIVATE_KEY_CONTENT
: (Alternative/Recommended for Deployment) The actual content of the private key file.
5. Error Handling, Logging, and Retry Mechanisms
- Error Handling:
try...catch
blocks are used in all functions interacting with the Vonage API or database.- Specific errors from the Vonage SDK (e.g.,
error.response.data
) are logged for better debugging. - Database interaction failures are caught and logged.
- Clear error messages are returned in the API response for
sendSms
.
- Logging:
- RedwoodJS's built-in
logger
(api/src/lib/logger.js
) is used. - Informative logs track the request flow, API calls, webhook processing, and errors.
- Use
logger.info
,logger.warn
,logger.error
, andlogger.debug({ custom: payload })
for detailed payloads. Configure log levels and destinations (e.g., structured logging) for production.
- RedwoodJS's built-in
- Retry Mechanisms (Vonage Side):
- Vonage automatically retries webhook delivery if it doesn't receive a
200 OK
response within its timeout period. This is why our webhook handlers must return200 OK
quickly. - Crucially: If your internal processing (like DB writes) fails after you return
200 OK
, Vonage will not retry. This highlights the need for asynchronous processing (queues) for critical webhook tasks in production to handle transient internal failures reliably.
- Vonage automatically retries webhook delivery if it doesn't receive a
- Retry Mechanisms (Client Side - Optional):
- If your frontend calls the
/api/sendSms
endpoint, implement retry logic there (e.g., using exponential backoff) for transient network errors or 5xx responses from your API.
- If your frontend calls the
6. Database Schema (Recap)
The Prisma schema (api/db/schema.prisma
) defines the SmsMessage
model with fields like vonageMessageId
(indexed), direction
, fromNumber
, toNumber
, body
, status
, errorCode
, and timestamps. The migration (yarn rw prisma migrate dev
) applies this schema. Consider adding indexes to other fields based on query patterns.
7. Security Features
- Secrets Management: API keys, Application ID, and Private Key are stored in
.env
locally and never committed to source control. Use secure environment variable management or secret stores in your deployment environment (see Section 12). - Input Validation: Basic validation is done in
sendSms
(checking forto
andtext
). Implement more robust validation (e.g., phone number format, length checks) for production using libraries likezod
. - Webhook Security (Essential for Production):
- Vonage supports signing webhooks using JWT or signature secrets. This allows your webhook handler to verify that the request genuinely originated from Vonage and wasn't tampered with.
- Implementing webhook signature verification is essential for securing your application against malicious or spoofed requests. While detailed implementation is outside this guide's scope, consult the Vonage documentation for
"Signed Webhooks"
or"Webhook Security"
. You'll typically need to configure a secret in the Vonage dashboard and use a library (likejsonwebtoken
for JWT) in your webhook handlers to validate theAuthorization
header or signature provided in the request. Do not run unsigned webhooks in production.
- Rate Limiting: Implement rate limiting on the
/api/sendSms
endpoint to prevent abuse and manage costs. RedwoodJS doesn't have built-in rate limiting; use platform features (like API Gateway limits) or middleware patterns with external stores (like Redis).
8. Handling Special Cases
- Phone Number Formatting: Vonage expects numbers in E.164 format (e.g.,
+14155551212
). Ensure input numbers are normalized before sending to the API. Libraries likelibphonenumber-js
can help. - Character Limits & Encoding: Standard SMS messages have character limits (160 GSM-7 characters, fewer for non-GSM alphabets using UCS-2). The Vonage Messages API handles concatenation for longer messages, but be mindful of potential increased costs.
- Delivery Receipt Support: Delivery receipts (DLRs) are carrier-dependent and not guaranteed globally. The
status
might remain 'accepted' even if delivered. Check Vonage's country-specific feature documentation. Do not rely solely on 'delivered' status for critical logic if operating in regions with poor DLR support. - Webhook Order: Status updates might occasionally arrive before your
sendSms
function finishes updating the database with thevonageMessageId
. Thestatus
webhook handler includes a warning log for this. More robust solutions might involve retries within the status handler or temporary caching if this becomes a frequent issue. Usingclient_ref
in the send request might also help correlation.
9. Performance Optimizations
- Database Indexing: The
@unique
directive onvonageMessageId
creates an essential index. Add indexes to other frequently queried fields (status
,createdAt
,fromNumber
,toNumber
) inschema.prisma
based on your application's needs. - Asynchronous Processing (Webhooks): As mentioned previously, move database writes and other potentially slow operations within webhook handlers (
inbound.ts
,status.ts
) to a background job queue (e.g., using Redis/BullMQ, platform-specific queues like AWS SQS) to ensure the required fast200 OK
response to Vonage and improve reliability. - Caching: If you build API endpoints to query message status frequently, consider implementing caching strategies (e.g., Redis, Memcached) to reduce database load.
10. Monitoring, Observability, and Analytics
- Logging: Configure Redwood's logger for production (e.g., JSON format, appropriate log level) and ship logs to a centralized logging platform (e.g., Datadog, Logtail, Grafana Loki, ELK stack). This is crucial for debugging API behavior and webhook processing.
- Health Checks: Implement a basic health check endpoint (e.g.,
/api/health
) that verifies API and database connectivity, usable by monitoring systems. - Error Tracking: Integrate error tracking services (e.g., Sentry, Bugsnag) to capture, aggregate, and alert on exceptions in your RedwoodJS functions.
- Metrics: Monitor key performance indicators (KPIs): function execution times, invocation counts, error rates, database query latency, and webhook processing times. Use your deployment platform's monitoring tools or integrate dedicated observability platforms (Prometheus/Grafana, Datadog). Track Vonage API usage and costs via the Vonage Dashboard.
11. Troubleshooting and Caveats
ngrok
Issues:- Ensure
ngrok
is running and forwardinghttp
traffic to the correct Redwood API port (default:8911
). - Verify the
https
URL fromngrok
is correctly configured in the Vonage Application settings for both Inbound and Status webhooks. - Check the
ngrok
web interface (usuallyhttp://localhost:4040
) to inspect incoming requests and responses.
- Ensure
- Private Key Path Issues:
- If using
VONAGE_PRIVATE_KEY_PATH
, double-check the path relative to the built API directory (api/dist
) where the function executes, not the source directory. - Consider using
VONAGE_PRIVATE_KEY_CONTENT
environment variable for deployment to avoid path issues. Ensure the variable contains the exact key content, including newlines.
- If using
- Webhook Not Triggering:
- Confirm the Vonage number is correctly linked to the Application.
- Ensure the account default SMS setting is "Messages API".
- Check
ngrok
logs/interface for incoming requests. If none arrive, the issue might be with Vonage configuration orngrok
setup. - Send a test SMS to your Vonage number to trigger the inbound webhook.
- Send a test SMS from your application to trigger the status webhook (delivery status might take time).
- Database Errors:
- Ensure the database is running and accessible from the API function environment.
- Verify
DATABASE_URL
in.env
is correct. - Check Prisma migrations (
yarn rw prisma migrate dev
) were applied successfully.
405 Method Not Allowed
: Ensure Vonage is configured to sendPOST
requests to your webhooks. Check your function code isn't incorrectly rejectingPOST
.- Missing
vonageMessageId
on Update: This can happen due to timing (status webhook arrives beforesendSms
DB update completes) or if the status update is for an inbound message. The warning log helps identify this. Consider usingclient_ref
or more robust correlation logic if needed. - Authentication Errors: Double-check
VONAGE_APPLICATION_ID
and the private key (path or content) are correct and accessible. Ensure you are using Application ID/Private Key auth for the Messages API as intended.