This guide provides a comprehensive walkthrough for building a RedwoodJS application capable of receiving inbound SMS or WhatsApp messages via the Vonage Messages API and enabling two-way conversations. We will cover everything from initial project setup and Vonage configuration to handling webhooks securely, storing messages, sending replies, and deploying the application.
By the end of this tutorial, you will have a functional RedwoodJS application that can:
- Receive inbound messages sent to a Vonage virtual number via webhooks.
- Securely validate incoming webhook requests using Vonage signatures.
- Store message details (sender, recipient, content, timestamp, status) in a database using Prisma.
- Optionally send replies back to the original sender, enabling two-way communication.
- Log events and handle potential errors gracefully.
Target Audience: Developers familiar with JavaScript and Node.js. Prior experience with RedwoodJS is helpful but not strictly required, as we cover setup from scratch. Familiarity with basic API concepts and webhooks is assumed.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. It provides structure, tooling, and conventions for building modern applications, including API (GraphQL and serverless functions) and frontend components.
- Vonage Messages API: Enables sending and receiving messages across various channels like SMS, MMS, WhatsApp, and Facebook Messenger using a unified API.
- Prisma: A next-generation Node.js and TypeScript ORM used by RedwoodJS for database interactions.
ngrok
: A tool for exposing local development servers to the internet (used for testing webhooks locally, not part of the deployed application stack).- Node.js & Yarn: The underlying runtime and package manager.
System Architecture:
+-------------------+ Webhook POST +--------------------------+ Database Ops +-----------------+
| Vonage Platform | --------------------> | RedwoodJS API Function | ---------------------> | Prisma Database |
| (Messages API) | (Inbound & Status) | (api/src/functions/...) | (Save/Update Message)| (PostgreSQL/etc)|
+-------------------+ +--------------------------+ +-----------------+
^ |
| SMS/WhatsApp Message | API Call (Send Message)
| | using Vonage SDK
+-------+----------+ v
| End User Device | <-----------------------+--------+----------+
| (Phone) | (SMS/WhatsApp Reply) | RedwoodJS Service |
+------------------+ | (api/src/services/)|
+-------------------+
Prerequisites:
- Node.js: Version 18.x or later.
- Yarn: Version 1.x (Classic).
- RedwoodJS CLI: Install globally:
npm install -g redwoodjs-cli
. Note: While the RedwoodJS CLI is often installed globally using npm_ project-level commands and dependency management within a RedwoodJS project typically use Yarn_ following standard RedwoodJS conventions. - Vonage Account: Sign up for free at Vonage API Dashboard. You receive free credits for testing.
ngrok
: Download and install from ngrok.com. Authenticate your client if you need longer sessions.- A provisioned Vonage Number: You can acquire one via the Vonage Dashboard (Numbers > Buy numbers). Ensure it supports SMS or the channel you intend to use (like WhatsApp).
1. Setting up the RedwoodJS project
First, create a new RedwoodJS application. We'll use TypeScript for enhanced type safety, which RedwoodJS supports out of the box.
-
Create the Redwood App: Open your terminal and run:
yarn create redwood-app ./vonage-redwood --typescript
This command scaffolds a new RedwoodJS project in a directory named
vonage-redwood
with TypeScript enabled. -
Navigate into the Project:
cd vonage-redwood
-
Initialize Environment Variables: RedwoodJS uses a
.env
file for environment variables. Create one in the project root:touch .env
We will populate this file later with necessary Vonage credentials. Redwood automatically loads variables from
.env
intoprocess.env
. -
Install Vonage SDK: We need the Vonage Node.js Server SDK to interact with the API and validate webhooks. Install it in the
api
workspace:yarn workspace api add @vonage/server-sdk @vonage/jwt
@vonage/server-sdk
: The main SDK for API calls.@vonage/jwt
: Required for generating JWTs if you use features like Secure Inbound Media or JWT authentication for API calls (though basic key/secret auth is often sufficient for sending messages).
-
Verify Setup: Start the development server to ensure the basic Redwood app is working:
yarn rw dev
You should see output indicating both the frontend (web) and backend (api) servers are running, typically on
http://localhost:8910
andhttp://localhost:8911
respectively. Stop the server for now (Ctrl+C
).
2. Configuring Vonage
Before writing code to handle messages, we need to configure Vonage to send webhook events to our application.
-
Log in to Vonage Dashboard: Access your Vonage API Dashboard.
-
Retrieve API Credentials:
- Navigate to API Settings from the left-hand menu or your profile dropdown.
- Note down your API key and API secret.
- Add these to your
.env
file:(Replace# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET
YOUR_API_KEY
andYOUR_API_SECRET
with your actual credentials)
-
Create a Vonage Application: Vonage Applications group settings like webhook URLs and capabilities.
- Navigate to Applications > Create a new application.
- Give it a descriptive name (e.g., ""RedwoodJS Messaging App"").
- Enable the Messages capability.
- Leave the Inbound URL and Status URL blank for now. We'll add them later using
ngrok
. - Scroll down to Signed webhooks. Check the box Enable signed webhooks. A Signature secret will be generated. Copy this secret.
- Add the Signature Secret to your
.env
file:(Replace# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_WEBHOOK_SECRET=YOUR_SIGNATURE_SECRET
YOUR_SIGNATURE_SECRET
with the generated secret) - Click Generate new application.
- You will be shown the Application ID. Copy this ID.
- Add the Application ID to your
.env
file:(Replace# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_WEBHOOK_SECRET=YOUR_SIGNATURE_SECRET VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID # Add this line
YOUR_APPLICATION_ID
with your actual Application ID) - (Optional: Private Key) If you need JWT authentication or features like Secure Inbound Media, click Generate public and private key. Download the
private.key
file and save it securely (e.g., in your project root, ensuring it's added to.gitignore
). Add its path to.env
:# .env # ... other vars VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root
-
Link Your Vonage Number:
- Go back to the Applications list and find your newly created application.
- Click Link next to the number you purchased earlier. Select the number and confirm. This tells Vonage which number should use this application's configuration (including webhooks).
- Note down the Vonage number you linked. Add it to
.env
:(Replace# .env # ... other vars VONAGE_NUMBER=YOUR_VONAGE_NUMBER # e.g., 14155550100
YOUR_VONAGE_NUMBER
with your linked number)
-
Configure Webhooks (Requires
ngrok
): We'll configure the actual webhook URLs in the ""Local Development & Testing"" section once we have our Redwood function endpoint and anngrok
tunnel running.
3. Creating the database schema
We need a way to store the messages received and sent. Let's define a Prisma model.
-
Define the
Message
Model: Openapi/db/schema.prisma
and add the following model:// api/db/schema.prisma datasource db { provider = "postgresql" // Or your preferred database (sqlite, mysql) url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model Message { id String @id @default(cuid()) vonageMessageId String @unique // The UUID from Vonage direction String // "inbound" or "outbound" channel String // "sms", "whatsapp", etc. fromNumber String toNumber String body String? // Message content (text) status String? // Status from Vonage (e.g., delivered, read, failed) timestamp DateTime // Timestamp from Vonage or when created errorCode String? // Vonage error code on failure errorReason String? // Vonage error reason on failure createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
vonageMessageId
: Crucial for correlating messages and status updates.direction
: To distinguish between incoming and outgoing messages.errorCode
,errorReason
: To store failure details from status webhooks.- Other fields map directly to Vonage message properties.
-
Apply Migrations: Run the Prisma migrate command to create/update the database schema and generate the Prisma client.
yarn rw prisma migrate dev
Follow the prompts (e.g., provide a name for the migration like "add message model"). This will create/update the
Message
table in your development database (SQLite by default, unless configured otherwise).
4. Implementing the RedwoodJS webhook handler
This is the core component that receives data from Vonage. We'll create a RedwoodJS Function, which is a serverless function deployed alongside your API.
-
Generate the Function: Use the Redwood CLI to generate a new function. We'll call it
vonageWebhook
.yarn rw g function vonageWebhook --no-graphql
This creates
api/src/functions/vonageWebhook.ts
. The--no-graphql
flag indicates it's a standard HTTP endpoint, not part of the GraphQL API. -
Implement the Handler Logic: Open
api/src/functions/vonageWebhook.ts
and replace its contents with the following:// api/src/functions/vonageWebhook.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { logger } from 'src/lib/logger' import { db } from 'src/lib/db' // Redwood's Prisma client instance import { Signature } from '@vonage/server-sdk' import { createMessage, updateMessageStatus, sendVonageMessage } from 'src/services/messages/messages' // Service functions // Helper function to safely parse JSON const safeJsonParse = (data: string | null): Record<string_ any> | null => { if (!data) return null try { return JSON.parse(data) } catch (error) { logger.error({ error, body: data }, 'Failed to parse request body as JSON') return null } } export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Vonage webhook received') // 1. Verify Signature (Security) const signatureSecret = process.env.VONAGE_WEBHOOK_SECRET if (!signatureSecret) { logger.error('VONAGE_WEBHOOK_SECRET is not configured. Cannot verify signature.') return { statusCode: 500, body: 'Internal Server Error: Webhook secret missing' } } const vonageSignature = new Signature( { apiSecret: signatureSecret }, // Use the webhook signature secret here { algorithm: 'sha256', header: 'X-Vonage-Signature' } // Vonage uses sha256 HMAC ) const headers = Object.entries(event.headers).reduce((acc, [key, value]) => { acc[key.toLowerCase()] = value // Ensure lowercase header keys return acc }, {}) const body = safeJsonParse(event.body) if (!body) { logger.warn('Received empty or invalid JSON body') return { statusCode: 400, body: 'Bad Request: Invalid JSON body' } } // IMPORTANT: Vonage signature check uses the raw body string before parsing const isSignatureValid = vonageSignature.checkSignature(event.body, headers) if (!isSignatureValid) { logger.warn('Invalid Vonage signature received.') // In production, you might want stricter checks or logging here // For testing, sometimes headers/body might be slightly different if passing through proxies // Log details to debug if needed: logger.debug({ headers, body: event.body }, 'Signature validation details'); // If validation consistently fails, double-check your VONAGE_WEBHOOK_SECRET and how ngrok/proxies handle headers/body. return { statusCode: 401, body: 'Unauthorized: Invalid signature' } // !! Commenting out the return above is ONLY for debugging signature issues and should never be done in production. // logger.warn('!!! Proceeding with invalid signature (DEBUG ONLY) !!!'); // Keep this commented out } else { logger.info('Vonage signature verified successfully.') } // 2. Determine Webhook Type (Inbound Message or Status Update) // This check is basic; Vonage payloads can vary. Adapt as needed. if (body.message_uuid && body.status) { // Likely a Status Webhook logger.info({ data: body }, 'Processing Status Webhook') try { await updateMessageStatus({ vonageMessageId: body.message_uuid, status: body.status, timestamp: body.timestamp ? new Date(body.timestamp) : new Date(), errorCode: body.error?.code, // Pass error code if present errorReason: body.error?.reason, // Pass error reason if present }) } catch (error) { logger.error({ error, data: body }, 'Error processing status webhook') // Don't fail the webhook response for DB errors if possible } } else if (body.message_uuid && body.from && body.to && body.message?.content?.type === 'text') { // Likely an Inbound Text Message Webhook logger.info({ data: body }, 'Processing Inbound Message Webhook') try { await createMessage({ vonageMessageId: body.message_uuid, direction: 'inbound', channel: body.channel || 'unknown', // e.g., 'sms', 'whatsapp' fromNumber: body.from.number || body.from.id, // Structure varies by channel toNumber: body.to.number || body.to.id, body: body.message.content.text, timestamp: body.timestamp ? new Date(body.timestamp) : new Date(), status: 'delivered', // Inbound messages are inherently delivered to us }) // --- Optional: Send an automated reply --- // Uncomment the block below to enable simple auto-reply /* try { const replyText = `Thanks for your message! We received: ""${body.message.content.text}""`; await sendVonageMessage({ to: body.from.number || body.from.id, // Reply to the sender from: body.to.number || body.to.id, // Send from the number that received the message text: replyText, channel: body.channel || 'sms' // Use the same channel if possible }); logger.info(`Sent auto-reply to ${body.from.number || body.from.id}`); } catch (replyError) { logger.error({ error: replyError }, 'Failed to send auto-reply'); } */ // --- End Optional Reply --- } catch (error) { logger.error({ error, data: body }, 'Error processing inbound message webhook') // Don't fail the webhook response for DB errors if possible } } else { logger.warn({ data: body }, 'Received unrecognized Vonage webhook format') } // 3. Always Respond with 200 OK // Vonage expects a quick confirmation. Process data asynchronously if needed. return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Webhook received successfully' }), } }
Explanation:
- Signature Verification: This is critical for security. It ensures the request genuinely came from Vonage and hasn't been tampered with. We use the
VONAGE_WEBHOOK_SECRET
(Signature Secret from the Vonage Application settings) and the@vonage/server-sdk
'sSignature
class. Important: The signature check requires the raw, unparsed request body string and the request headers. - Webhook Type Detection: We differentiate between inbound messages and status updates based on common payload properties (
message_uuid
,status
,message.content.type
). This might need refinement based on the specific Vonage channels and events you handle. - Service Calls: We delegate database operations (
createMessage
,updateMessageStatus
) and sending replies (sendVonageMessage
) to Redwood Services (created next). This keeps the function focused on handling the HTTP request/response lifecycle. - Error Handling: Basic
try...catch
blocks log errors using Redwood's built-inlogger
. In production, you'd want more robust error tracking. Error details from status webhooks are now passed to the service. - 200 OK Response: Vonage requires a
200 OK
response within a short timeout (typically a few seconds) to acknowledge receipt. Failure to respond quickly can lead to Vonage retrying the webhook and eventually disabling it. Any time-consuming processing should ideally happen after sending the response. - Optional Auto-Reply: The commented-out section shows where you could trigger an outbound message using the imported
sendVonageMessage
service function.
- Signature Verification: This is critical for security. It ensures the request genuinely came from Vonage and hasn't been tampered with. We use the
5. Implementing RedwoodJS services
Services encapsulate business logic, including database interactions and calls to external APIs like Vonage.
-
Generate the Service:
yarn rw g service message
This creates
api/src/services/messages/messages.ts
and related test/scenario files. -
Implement Service Logic: Open
api/src/services/messages/messages.ts
and add the necessary functions:// api/src/services/messages/messages.ts import type { Prisma } from '@prisma/client' import { db } from 'src/lib/db' import { logger } from 'src/lib/logger' import { Vonage } from '@vonage/server-sdk' // Import Vonage SDK import { Auth } from '@vonage/auth' // Import Auth for credentials // Type definition for message data (can be refined) interface MessageInput extends Prisma.MessageCreateInput { // Add any specific types if needed, Prisma.MessageCreateInput is quite broad } interface StatusUpdateInput { vonageMessageId: string status: string timestamp: Date errorCode?: string | number // Vonage error codes can be numbers errorReason?: string } interface SendMessageInput { to: string from: string text: string channel?: 'sms' | 'whatsapp' | string // Allow specific channels or general string } // Initialize Vonage Client (only once) let vonage: Vonage | null = null try { const credentials = new Auth({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET, // Add application details if using JWT or specific features // applicationId: process.env.VONAGE_APPLICATION_ID, // privateKey: process.env.VONAGE_PRIVATE_KEY_PATH, // Or handle content from env var }) vonage = new Vonage(credentials) logger.info('Vonage SDK initialized successfully.') } catch (error) { logger.error({ error }, 'Failed to initialize Vonage SDK. Check API Key/Secret.') // Depending on your app's needs, you might throw here or handle later } /** * Creates a new message record in the database. */ export const createMessage = async (input: MessageInput) => { logger.debug({ custom: input }, 'Creating message record') try { return await db.message.create({ data: input, }) } catch (error) { logger.error({ error, custom: input }, 'Error creating message in DB') // Rethrow or handle as appropriate for your application throw error } } /** * Updates the status of an existing message based on Vonage status webhook. */ export const updateMessageStatus = async ({ vonageMessageId, status, timestamp, errorCode, errorReason, }: StatusUpdateInput) => { logger.debug({ vonageMessageId, status, errorCode }, 'Updating message status') try { return await db.message.update({ where: { vonageMessageId }, data: { status, errorCode: errorCode ? String(errorCode) : null, // Ensure errorCode is stored as string errorReason: errorReason, updatedAt: timestamp, // Use timestamp from Vonage if available }, }) } catch (error) { // Handle cases where the message might not exist (e.g., race condition) if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') { logger.warn({ vonageMessageId }, 'Message not found for status update.'); return null; // Or handle differently } logger.error({ error, vonageMessageId, status }, 'Error updating message status in DB') // Rethrow or handle throw error } } /** * Sends an outbound message using the Vonage Messages API. */ export const sendVonageMessage = async ({ to, from, text, channel = 'sms' }: SendMessageInput) => { if (!vonage) { logger.error('Vonage SDK not initialized. Cannot send message.'); throw new Error('Vonage service unavailable'); } logger.debug({ to, from, channel }, 'Attempting to send Vonage message'); try { // Use the generic Messages API endpoint const resp = await vonage.messages.send({ message_type: "text", // Use standard quotes text: text, to: to, from: from, // This should be your Vonage number channel: channel, // 'sms' or 'whatsapp', etc. }) logger.info({ response: resp }, `Message sent via Vonage to ${to}. Message UUID: ${resp.messageUuid}`); // Optionally, immediately create an 'outbound' message record in your DB await createMessage({ vonageMessageId: resp.messageUuid, direction: 'outbound', channel: channel, fromNumber: from, toNumber: to, body: text, status: 'submitted', // Initial status - Vonage will send 'delivered', 'failed', etc. later timestamp: new Date(), }); return resp; // Contains message_uuid } catch (error) { // Log detailed error from Vonage if available const vonageError = error?.response?.data || error?.message || error; logger.error({ error: vonageError, to, from }, 'Failed to send message via Vonage'); // Rethrow or handle error appropriately throw error; } }
Explanation:
- Vonage SDK Initialization: We initialize the Vonage client using credentials from
.env
. This should only happen once. Error handling is included in case credentials are missing or invalid. createMessage
: A straightforward function usingdb.message.create
to save message data.updateMessageStatus
: Usesdb.message.update
to find a message by its uniquevonageMessageId
and update itsstatus
,errorCode
, anderrorReason
. It includes basic handling for cases where the message might not be found (e.g., if the status webhook arrives before the inbound message webhook is fully processed). EnsureserrorCode
is stored as a string.sendVonageMessage
:- Checks if the Vonage SDK is initialized.
- Uses
vonage.messages.send()
which is the unified endpoint for various channels. - Specifies
message_type: "text"
,to
,from
(your Vonage number),text
, andchannel
. - Logs the response from Vonage, which includes the
messageUuid
. - Optionally creates a corresponding
outbound
record in the database immediately with status 'submitted'. The actual delivery status will arrive later via the Status webhook. - Includes improved error logging for Vonage API failures.
- Vonage SDK Initialization: We initialize the Vonage client using credentials from
ngrok
6. Local development & testing with To test the webhook locally, we need to expose our Redwood development server to the public internet using ngrok
.
-
Start Redwood Dev Server: Open a terminal in your project root and run:
yarn rw dev
Note the API server port (usually
8911
). -
Start
ngrok
: Open a second terminal and runngrok
, pointing it to your Redwood API server's port:ngrok http 8911
ngrok
will display forwarding URLs (e.g.,https://<random-subdomain>.ngrok-free.app
). Copy thehttps
URL.- The full URL needed for Vonage will be this
ngrok
HTTPS URL plus the path to your function, which is/api/vonageWebhook
.
-
Update Vonage Webhook URLs:
- Go back to your Vonage Application settings in the dashboard.
- Paste the full
ngrok
HTTPS URL including the path into both the Inbound URL and Status URL fields. The final URL should look like:https://<random-subdomain>.ngrok-free.app/api/vonageWebhook
. - Click Save changes.
-
Send a Test Message:
- Using your mobile phone, send an SMS (or WhatsApp message, if configured) to your Vonage virtual number.
-
Verify:
ngrok
Console: Check thengrok
terminal window. You should see aPOST /api/vonageWebhook
request with a200 OK
response.- Redwood Console: Check the terminal running
yarn rw dev
. You should see logs from thevonageWebhook
function:- ""Vonage webhook received""
- ""Vonage signature verified successfully.""
- ""Processing Inbound Message Webhook""
- Logs from the
createMessage
service. - If auto-reply is enabled: ""Attempting to send Vonage message"", ""Message sent via Vonage..."", logs from
createMessage
for the outbound record.
- Database: Check your
Message
table (e.g., usingyarn rw prisma studio
). You should see a new record for the inbound message (and potentially an outbound one if reply is enabled). - Your Phone: If auto-reply is enabled, you should receive the reply message back on your phone.
Troubleshooting
ngrok
:- If requests don't arrive, double-check the
ngrok
URL in Vonage (HTTPS, correct path/api/vonageWebhook
). - Ensure
ngrok
is still running. Freengrok
sessions time out. - Check firewalls aren't blocking
ngrok
. - If signature validation fails, log the headers and raw body in your function (
logger.debug
) and compare them carefully with what Vonage expects. EnsureVONAGE_WEBHOOK_SECRET
is correct. Sometimes proxies (includingngrok
under certain configurations) can subtly alter headers or body encoding.
7. Security considerations
- Webhook Signature Validation: This is paramount. Always verify the signature using the
VONAGE_WEBHOOK_SECRET
as shown in the function handler. Never disable this check in production. - Environment Variables: Keep API keys, secrets, and signature secrets out of your codebase. Use the
.env
file and ensure it's in your.gitignore
. Use your deployment platform's secret management for production. - Input Sanitization: While less critical for simply storing/relaying messages, if you use message content elsewhere (displaying in UI, further processing), sanitize it appropriately to prevent XSS or other injection attacks. Prisma helps prevent SQL injection at the database layer.
- Rate Limiting: Your webhook endpoint is publicly accessible. Consider adding rate limiting (e.g., using middleware or platform features) to prevent abuse, although Vonage itself has rate limits.
- Error Handling: Avoid leaking sensitive error details back in HTTP responses. Log detailed errors internally.
8. Error handling and logging
- Consistent Logging: Use Redwood's built-in
logger
(import { logger } from 'src/lib/logger'
). Log key events (webhook received, signature verified, message processed/sent) and any errors. Use appropriate log levels (info
,warn
,error
,debug
). - Vonage API Errors: The Vonage SDK throws errors for API failures. Catch these errors in your service functions (
sendVonageMessage
) and log relevant details (often found inerror.response.data
orerror.message
). - Database Errors: Catch errors during Prisma operations (
createMessage
,updateMessageStatus
). Log details and decide how to respond (e.g., retry, notify admin, ignore if non-critical like a duplicate status update). - Status Webhooks: Utilize the status webhook to track the delivery success or failure of outbound messages. Update your database accordingly, including
errorCode
anderrorReason
. Implement logic to handle specific error codes from Vonage if needed (e.g., number blocked, invalid number). - Retry Mechanisms: For transient errors (e.g., temporary network issues when calling Vonage API), implement a simple retry strategy with exponential backoff within your service function, potentially using a library like
async-retry
. However, be cautious not to block the webhook response. If sending fails, log it and potentially queue it for later retry via a background job system (like RedwoodJS Background Jobs).
9. Deployment
Deploying a RedwoodJS application is well-documented. Here are the key steps related to Vonage integration:
-
Choose a Platform: Select a deployment provider that supports Node.js applications (e.g., Vercel, Netlify, Render, Fly.io, AWS Lambda). RedwoodJS has deployment guides for popular platforms.
-
Configure Environment Variables: Set the following environment variables securely in your deployment platform's settings:
DATABASE_URL
(pointing to your production database)VONAGE_API_KEY
VONAGE_API_SECRET
VONAGE_WEBHOOK_SECRET
VONAGE_APPLICATION_ID
VONAGE_NUMBER
VONAGE_PRIVATE_KEY_PATH
(if using JWT/Secure Media): For production, it's highly recommended to store the content of the private key directly in a secure environment variable (e.g.,VONAGE_PRIVATE_KEY_CONTENT
perhaps base64 encoded) instead of deploying the key file. This avoids file system complexities and enhances security. Adjust your SDK initialization logic (Section 5.2) to read the key content from the environment variable if the path variable isn't set. Using the file path might be simpler for local development but less ideal for production.- Redwood's standard environment variables (like
SESSION_SECRET
if using auth). Check RedwoodJS deployment docs for specifics.