code examples
code examples
Implementing AWS SNS Delivery Status Callbacks in Next.js
A guide on building a Next.js application to send SMS via AWS SNS and track delivery status using CloudWatch Logs, Lambda, and DynamoDB.
This guide provides a step-by-step walkthrough for building a system within a Next.js application to send SMS messages via Amazon Simple Notification Service (SNS) and track their delivery status using AWS CloudWatch Logs and AWS Lambda.
Project Overview and Goals
We will build a Next.js application that enables users to send an SMS message to a specified phone number. After sending, the application will track the delivery status (e.g., SUCCESS, FAILURE) of that message.
- Problem Solved: AWS SNS does not natively provide direct HTTP callbacks to your application with delivery status updates for SMS messages. This guide implements a robust mechanism to capture this status information indirectly.
- Core Functionality:
- A Next.js frontend to input a phone number and message.
- A Next.js API route to trigger the SNS message sending.
- An SNS Topic configured to send SMS and log delivery status to CloudWatch.
- A DynamoDB table to store message details and track status.
- An AWS Lambda function triggered by CloudWatch Logs containing SNS status updates.
- The Lambda function parses the log data, looks up the message using a Global Secondary Index (GSI), and updates the corresponding message status in DynamoDB.
- Technologies Used:
- Next.js: React framework for frontend and API routes.
- AWS SNS: Managed messaging service for sending SMS.
- AWS CloudWatch Logs: Service for monitoring and logging AWS resources; used here to capture SNS delivery status.
- AWS Lambda: Serverless compute service to process CloudWatch Logs.
- AWS DynamoDB: NoSQL database for storing message status.
- AWS IAM: Identity and Access Management for permissions.
- AWS SDK for JavaScript v3: For interacting with AWS services from Next.js and Lambda.
- Prerequisites:
- Node.js and npm/yarn installed.
- An AWS account with appropriate permissions to create SNS topics, Lambda functions, DynamoDB tables, CloudWatch Log Groups, and IAM roles.
- AWS CLI configured locally (
aws configure). - Basic understanding of Next.js, React, and AWS concepts.
- Final Outcome: A functional Next.js application capable of sending SMS messages and reliably updating the delivery status associated with each message in a database.
System Architecture Diagram
graph TD
A[User Browser] -- 1. Submit Form --> B(Next.js Frontend);
B -- 2. POST /api/send-sms --> C(Next.js API Route);
C -- 3. Store Initial Record (internalMessageId, PENDING) --> D[(DynamoDB)];
C -- 4. Publish SMS (snsMessageId returned) --> E(AWS SNS Topic);
E -- 5. Send SMS --> F([Recipient Phone]);
E -- 6. Log Delivery Status --> G(CloudWatch Logs);
G -- 7. Trigger on Log Event --> H(AWS Lambda Function);
H -- 8. Parse Log & Query GSI (using snsMessageId) --> D;
H -- 9. Update Status (using internalMessageId) --> D;1. Setting up the Project
Let's initialize the Next.js project and install necessary dependencies.
-
Create Next.js App: Open your terminal and run:
bashnpx create-next-app@latest sns-status-tracker --typescript cd sns-status-trackerChoose the defaults when prompted.
-
Install AWS SDK v3: We need the AWS SDK clients for SNS and DynamoDB.
bashnpm install @aws-sdk/client-sns @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb@aws-sdk/client-sns: For sending messages via SNS.@aws-sdk/client-dynamodb: Base client for DynamoDB.@aws-sdk/lib-dynamodb: Utility library simplifying DynamoDB interactions.
-
Environment Variables: Create a file named
.env.localin the project root. This file stores sensitive credentials and configuration. Never commit this file to version control. Replace the placeholder values with your actual configuration.plaintext# .env.local # AWS Credentials (Use IAM Roles for production deployments) # For local development, provide your IAM User keys here. # For production (Vercel, AWS Amplify, EC2, ECS), leave these blank # and configure IAM Roles or platform-specific secret management. AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY AWS_REGION=us-east-1 # Replace with your preferred AWS region # AWS Service Configuration (Replace with your resource names/ARNs after creation) SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:YourSnsTopicName # Usually not needed for direct SMS, but good practice if using topics DYNAMODB_TABLE_NAME=SnsMessageStatus # The name of your DynamoDB table LAMBDA_FUNCTION_NAME=SnsStatusUpdater # The name of your Lambda function # Optional: For Lambda function ARN if needed elsewhere # LAMBDA_FUNCTION_ARN=arn:aws:lambda:us-east-1:123456789012:function:SnsStatusUpdater- Obtaining Credentials: For local development, you can create an IAM user with programmatic access. Go to AWS Console -> IAM -> Users -> Create user. Attach appropriate policies (e.g.,
AmazonSNSFullAccess,AmazonDynamoDBFullAccess,AWSLambda_FullAccess,CloudWatchLogsFullAccess- restrict these tightly in production). Download the access key and secret key. For production, always prefer IAM roles assigned to your compute environment (e.g., Vercel environment variables, EC2 instance profile, ECS task role). - Region: Choose the AWS region where you'll create your resources.
SNS_TOPIC_ARN,DYNAMODB_TABLE_NAME,LAMBDA_FUNCTION_NAME: We will create these resources in later steps and update these values. TheSNS_TOPIC_ARNis often not directly used when sending SMS viaPhoneNumber, but might be relevant for other configurations.
- Obtaining Credentials: For local development, you can create an IAM user with programmatic access. Go to AWS Console -> IAM -> Users -> Create user. Attach appropriate policies (e.g.,
-
Project Structure: Your
src/directory (if using/src) will eventually contain:pages/: Frontend pages and API routes.components/: Reusable React components.lib/: Utility functions (like AWS client setup).lambda/: Directory for Lambda function code (created later).
-
AWS Client Configuration (Optional Utility): Create
src/lib/awsClients.tsto centralize SDK client initialization.typescript// src/lib/awsClients.ts import { SNSClient } from ""@aws-sdk/client-sns""; import { DynamoDBClient } from ""@aws-sdk/client-dynamodb""; import { DynamoDBDocumentClient } from ""@aws-sdk/lib-dynamodb""; const region = process.env.AWS_REGION!; const credentials = { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }; // Basic validation or configuration selection let clientConfig: { region: string; credentials?: typeof credentials } = { region }; if (process.env.NODE_ENV === 'development' && credentials.accessKeyId && credentials.secretAccessKey) { // Use explicit credentials only in development if provided clientConfig.credentials = credentials; console.log(""Using explicit AWS credentials from .env.local for development.""); } else if (process.env.NODE_ENV === 'development') { console.warn(""AWS credentials not found in .env.local for development. SDK will fallback to shared config/credentials or environment variables.""); } // In production or if keys are missing in dev, rely on SDK's default credential chain (IAM roles, env vars, etc.) // No explicit 'credentials' object is passed in this case. if (!region) { throw new Error(""AWS_REGION environment variable is not set.""); } export const snsClient = new SNSClient(clientConfig); const ddbClient = new DynamoDBClient(clientConfig); // Helper for simplified DynamoDB operations const marshallOptions = { convertEmptyValues: false, // Default false removeUndefinedValues: true, // Recommended practice convertClassInstanceToMap: false, // Default false }; const unmarshallOptions = { wrapNumbers: false, // Default false }; const translateConfig = { marshallOptions, unmarshallOptions }; export const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig);- Why this utility? Centralizes client creation, making it easier to manage configuration and apply consistent settings. Reads credentials and region from environment variables. Uses
DynamoDBDocumentClientfor easier interaction with DynamoDB using plain JavaScript objects. - Production Credentials: The code prioritizes IAM roles or standard environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN) in production by not passing thecredentialsobject unless specifically configured for development in.env.local. The SDK automatically detects IAM roles in environments like Lambda, EC2, ECS, or Vercel/Amplify if configured correctly.
- Why this utility? Centralizes client creation, making it easier to manage configuration and apply consistent settings. Reads credentials and region from environment variables. Uses
2. Implementing Core Functionality (Sending SMS)
Let's create the frontend form and the API route to send the SMS.
-
Frontend Component: Create
src/components/SmsForm.tsx:typescript// src/components/SmsForm.tsx import React, { useState } from 'react'; export default function SmsForm() { const [phoneNumber, setPhoneNumber] = useState(''); const [message, setMessage] = useState(''); const [status, setStatus] = useState(''); // To display feedback const [loading, setLoading] = useState(false); const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); setLoading(true); setStatus('Sending...'); try { const response = await fetch('/api/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ phoneNumber, message }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `HTTP error! status: ${response.status}`); } setStatus(`Message submitted! Internal ID: ${data.internalMessageId}. Status is PENDING. Check logs/DB for updates.`); // Optionally clear form: // setPhoneNumber(''); // setMessage(''); } catch (error: any) { console.error(""Failed to send SMS:"", error); setStatus(`Error: ${error.message}`); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit}> <h2>Send SMS</h2> <div> <label htmlFor=""phoneNumber"">Phone Number (E.164 format, e.g., +14155552671):</label> <input type=""tel"" id=""phoneNumber"" value={phoneNumber} onChange={(e) => setPhoneNumber(e.target.value)} required pattern=""\+[1-9]\d{1,14}"" // Basic E.164 pattern /> </div> <div> <label htmlFor=""message"">Message:</label> <textarea id=""message"" value={message} onChange={(e) => setMessage(e.target.value)} required maxLength={160} // SMS length limit reminder /> </div> <button type=""submit"" disabled={loading}> {loading ? 'Sending...' : 'Send Message'} </button> {status && <p>Status: {status}</p>} </form> ); } -
Add Component to Page: Modify
src/pages/index.tsx:typescript// src/pages/index.tsx import Head from 'next/head'; import SmsForm from '@/components/SmsForm'; // Adjust path if needed export default function Home() { return ( <div> <Head> <title>SNS SMS Status Tracker</title> <meta name=""description"" content=""Send SMS via SNS and track status"" /> <link rel=""icon"" href=""/favicon.ico"" /> </Head> <main> <h1>SNS SMS Status Tracker</h1> <SmsForm /> </main> </div> ); }
3. Building the API Layer (Send SMS)
Create the Next.js API route that handles sending the message via SNS and creating the initial status record in DynamoDB.
- Create API Route:
Create
src/pages/api/send-sms.ts:typescript// src/pages/api/send-sms.ts import type { NextApiRequest, NextApiResponse } from 'next'; import { PublishCommand, PublishCommandInput } from '@aws-sdk/client-sns'; import { PutCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb'; // Import UpdateCommand import { snsClient, ddbDocClient } from '@/lib/awsClients'; // Adjust path if needed import { randomUUID } from 'crypto'; // Use crypto for unique IDs type RequestBody = { phoneNumber: string; message: string; }; type ResponseData = { message?: string; error?: string; internalMessageId?: string; // Our internal tracking ID snsMessageId?: string; // ID returned by SNS }; // Basic E.164 format validation (improve as needed) function isValidE164(phoneNumber: string): boolean { return /^\+[1-9]\d{1,14}$/.test(phoneNumber); } export default async function handler( req: NextApiRequest, res: NextApiResponse<ResponseData> ) { if (req.method !== 'POST') { res.setHeader('Allow', ['POST']); return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); } const { phoneNumber, message }: RequestBody = req.body; // --- Input Validation --- if (!phoneNumber || !message) { return res.status(400).json({ error: 'phoneNumber and message are required.' }); } if (!isValidE164(phoneNumber)) { return res.status(400).json({ error: 'Invalid phone number format. Use E.164 (e.g., +14155552671).' }); } if (message.length > 160) { // Basic length check return res.status(400).json({ error: 'Message exceeds 160 characters.' }); } const tableName = process.env.DYNAMODB_TABLE_NAME; if (!tableName) { console.error(""DYNAMODB_TABLE_NAME not set in environment variables.""); return res.status(500).json({ error: 'Server configuration error.' }); } const internalMessageId = randomUUID(); // Generate a unique ID for tracking const now = new Date().toISOString(); try { // --- 1. Create initial record in DynamoDB --- const initialRecord = { TableName: tableName, Item: { internalMessageId: internalMessageId, // Partition Key phoneNumber: phoneNumber, messageBody: message, // Store the message content if needed status: 'PENDING', // Initial status snsMessageId: null, // Will be updated after successful publish providerResponse: null, // Store SNS delivery status response later createdAt: now, updatedAt: now, }, // Optionally add ConditionExpression: ""attribute_not_exists(internalMessageId)"" // to prevent accidental overwrites if UUID collision occurred (highly unlikely) }; await ddbDocClient.send(new PutCommand(initialRecord)); console.log(`Initial record created in DynamoDB: ${internalMessageId}`); // --- 2. Publish message to SNS --- // Note: For SMS, MessageAttributes can configure SenderID, SMSType etc. // See: https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html#sms_publish_worldwide const params: PublishCommandInput = { PhoneNumber: phoneNumber, // Directly publish to a phone number for SMS Message: message, // Example MessageAttributes for SMS (Optional) // MessageAttributes: { // 'AWS.SNS.SMS.SenderID': { DataType: 'String', StringValue: 'MySenderID' }, // 'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' } // or 'Promotional' // } }; console.log(`Publishing SMS via SNS to: ${phoneNumber}`); const command = new PublishCommand(params); const snsResponse = await snsClient.send(command); console.log(`SNS Publish Response:`, snsResponse); if (!snsResponse.MessageId) { // This is unusual but handle it console.error(""SNS did not return a MessageId.""); // Attempt to update the status to FAILED_TO_SEND try { await ddbDocClient.send(new UpdateCommand({ TableName: tableName, Key: { internalMessageId: internalMessageId }, UpdateExpression: ""set #st = :s, providerResponse = :pr, updatedAt = :ua"", ExpressionAttributeNames: { ""#st"": ""status"" }, ExpressionAttributeValues: { "":s"": ""FAILED_TO_SEND"", "":pr"": ""SNS did not return a MessageId."", "":ua"": new Date().toISOString() } })); } catch (dbUpdateError) { console.error(""Failed to update status to FAILED_TO_SEND after missing SNS MessageId:"", dbUpdateError); } return res.status(500).json({ error: 'Failed to send message via SNS (no MessageId returned).', internalMessageId }); } // --- 3. Update DynamoDB record with SNS Message ID using UpdateCommand --- const updateParams = { TableName: tableName, Key: { internalMessageId: internalMessageId }, UpdateExpression: ""set snsMessageId = :sid, updatedAt = :ua"", ExpressionAttributeValues: { "":sid"": snsResponse.MessageId, "":ua"": new Date().toISOString() }, ReturnValues: ""UPDATED_NEW"", // Optional: Get updated attributes back }; await ddbDocClient.send(new UpdateCommand(updateParams)); // Use UpdateCommand console.log(`DynamoDB record updated with SNS Message ID: ${snsResponse.MessageId}`); // --- Success Response --- return res.status(200).json({ message: 'Message submitted successfully.', internalMessageId: internalMessageId, snsMessageId: snsResponse.MessageId, }); } catch (error: any) { console.error(""Error in /api/send-sms:"", error); // Attempt to update status to FAILED_TO_SEND if an error occurred try { await ddbDocClient.send(new UpdateCommand({ TableName: tableName, Key: { internalMessageId: internalMessageId }, // Use the ID generated earlier UpdateExpression: ""set #st = :s, providerResponse = :pr, updatedAt = :ua"", ExpressionAttributeNames: { ""#st"": ""status"" }, ExpressionAttributeValues: { "":s"": ""FAILED_TO_SEND"", "":pr"": `Error during send/update: ${error.message}`, "":ua"": new Date().toISOString() }, // ConditionExpression: ""attribute_exists(internalMessageId)"" // Optional: only update if record exists })); console.log(`Updated DynamoDB status to FAILED_TO_SEND for ${internalMessageId}`); } catch (dbError) { console.error(`Failed to update DynamoDB status to FAILED_TO_SEND for ${internalMessageId}:`, dbError); // Log this failure, but usually proceed to return the original error to the client } return res.status(500).json({ error: `Failed to process message: ${error.message}`, internalMessageId }); } }- Validation: Includes basic checks for required fields and phone number format.
- Unique ID: Generates a
randomUUID(internalMessageId) to track the message within our system before getting thesnsMessageId. - DynamoDB Interaction:
- Creates an initial record with
status: 'PENDING'usingPutCommand. - Correctly updates the record with the
snsMessageIdusingUpdateCommandupon successful SNS publication.
- Creates an initial record with
- SNS Publish: Uses
PublishCommandto send the SMS directly to the phone number. - Error Handling: Includes
try...catchblocks and attempts to update the DynamoDB status toFAILED_TO_SENDusingUpdateCommandif errors occur during SNS publishing or database updates.
4. Integrating with AWS Services (SNS, DynamoDB, IAM)
Now, let's create the necessary AWS resources. We'll primarily use the AWS Management Console for clarity, but provide equivalent AWS CLI commands where feasible. Remember to replace placeholders like YOUR_REGION and YOUR_ACCOUNT_ID with your actual values.
-
Create DynamoDB Table and GSI:
- Console:
- Navigate to DynamoDB in the AWS Console.
- Click ""Create table"".
- Table name:
SnsMessageStatus(or your chosen name from.env.local). - Partition key:
internalMessageId, Type:String. - Leave other settings as default (e.g., Provisioned capacity mode, or choose On-Demand).
- Click ""Create table"".
- Once the table is
Active, select it. - Go to the ""Indexes"" tab.
- Click ""Create index"".
- Partition key:
snsMessageId, Type:String. - Index name:
SnsMessageIdIndex(this name will be used in the Lambda code). - Projected attributes: Select ""Include"" and add
internalMessageIdto the projected attributes. This makes the lookup efficient. - Leave capacity settings as default or adjust as needed (match table mode if possible).
- Click ""Create index"". The index will take some time to build.
- CLI:
bash
# Create Table (On-Demand is often recommended) aws dynamodb create-table \ --table-name SnsMessageStatus \ --attribute-definitions AttributeName=internalMessageId,AttributeType=S \ --key-schema AttributeName=internalMessageId,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region YOUR_REGION # Add GSI (Wait for table to be ACTIVE before running this) aws dynamodb update-table \ --table-name SnsMessageStatus \ --attribute-definitions AttributeName=snsMessageId,AttributeType=S \ --global-secondary-index-updates \ ""[{\""Create\"":{\""IndexName\"": \""SnsMessageIdIndex\"",\""KeySchema\"":[{\""AttributeName\"":\""snsMessageId\"",\""KeyType\"":\""HASH\""}], \ \""Projection\"":{\""ProjectionType\"":\""INCLUDE\"", \""NonKeyAttributes\"":[\""internalMessageId\""]}}}]"" \ --region YOUR_REGION # Note: If using Provisioned capacity for the table, you must specify it for the GSI too: # Add '\""ProvisionedThroughput\"": {\""ReadCapacityUnits\"": 1, \""WriteCapacityUnits\"": 1}' inside the Create object for the GSI. - Update
.env.local: EnsureDYNAMODB_TABLE_NAMEmatches the exact name you used (SnsMessageStatus).
- Console:
-
Configure SNS SMS Delivery Status Logging: This tells SNS to log delivery attempts to CloudWatch Logs.
-
Console:
- Navigate to SNS in the AWS Console.
- In the left navigation pane, click ""Text messaging (SMS)"".
- Scroll down to ""Delivery status logging"". Click ""Edit"".
- Success sample rate (%): Enter
100(to log all successes for debugging; reduce in production if cost is a concern). - IAM role for successes: Click ""Create new service role"" -> ""Create role"". This opens IAM. Accept defaults and create the role (e.g.,
SNSSuccessFeedback). AWS automatically creates a role (SNSSuccessFeedback) with a policy allowinglogs:CreateLogStreamandlogs:PutLogEventsto CloudWatch Logs. - Select the newly created role (you might need to refresh).
- IAM role for failures: Click ""Create new service role"" -> ""Create role"". Create another role (e.g.,
SNSFailureFeedback) similarly. - Select the newly created failure role.
- Click ""Save changes"".
-
What this does: SNS will now attempt to write log entries for SMS delivery attempts to a CloudWatch Log Group, typically named
sns/YOUR_REGION/YOUR_ACCOUNT_ID/DirectPublishToPhoneNumber. Verify this exact name after sending your first message, as you'll need it for the Lambda trigger.
-
-
Create IAM Role for Lambda: Our Lambda function needs permission to be invoked by CloudWatch Logs, read those logs, query the DynamoDB GSI, and update the DynamoDB table.
- Console:
- Navigate to IAM in the AWS Console.
- Go to ""Roles"" -> ""Create role"".
- Trusted entity type: Select ""AWS service"".
- Use case: Select ""Lambda"". Click ""Next"".
- Add permissions: Search for and select the managed policy
AWSLambdaBasicExecutionRole(for basic Lambda logging). - Click ""Next"".
- Role name:
SnsStatusUpdaterRole(or your choice). - Click ""Create role"".
- Add Inline Policy for Specific Permissions: Find the created role, go to the ""Permissions"" tab, click ""Add permissions"" -> ""Create inline policy"".
- Select the ""JSON"" tab and paste the following policy, replacing placeholders:
json
{ ""Version"": ""2012-10-17"", ""Statement"": [ { ""Sid"": ""DynamoDBPermissions"", ""Effect"": ""Allow"", ""Action"": [ ""dynamodb:Query"", // To query the GSI ""dynamodb:UpdateItem"" // To update the status ], ""Resource"": [ ""arn:aws:dynamodb:YOUR_REGION:YOUR_ACCOUNT_ID:table/SnsMessageStatus"", // Allow UpdateItem on table ""arn:aws:dynamodb:YOUR_REGION:YOUR_ACCOUNT_ID:table/SnsMessageStatus/index/SnsMessageIdIndex"" // Allow Query on GSI ] }, { ""Sid"": ""CloudWatchLogsReadAccessSNS"", ""Effect"": ""Allow"", ""Action"": [ ""logs:GetLogEvents"", ""logs:FilterLogEvents"" // Needed for CloudWatch Logs trigger processing ], ""Resource"": ""arn:aws:logs:YOUR_REGION:YOUR_ACCOUNT_ID:log-group:sns/YOUR_REGION/YOUR_ACCOUNT_ID/DirectPublishToPhoneNumber:*"" // Verify exact log group name pattern } ] } - Click ""Review policy"".
- Name:
SnsStatusUpdaterServicePermissions. - Click ""Create policy"".
- Console:
5. Implementing the Lambda Function (with GSI Lookup)
This function is triggered by CloudWatch Logs events from SNS delivery status logging. It parses the event, uses the snsMessageId to query the GSI for the internalMessageId, and updates the correct DynamoDB record.
-
Create Lambda Function Directory: In your project root, create
lambda/sns-status-updater/. -
Lambda Handler Code (with GSI Lookup): Create
lambda/sns-status-updater/index.mjs(using.mjsfor native ES Modules support in Node.js Lambda runtime):javascript// lambda/sns-status-updater/index.mjs import { DynamoDBClient } from ""@aws-sdk/client-dynamodb""; import { DynamoDBDocumentClient, UpdateCommand, QueryCommand } from ""@aws-sdk/lib-dynamodb""; import { gunzipSync } from 'zlib'; const region = process.env.AWS_REGION; const tableName = process.env.DYNAMODB_TABLE_NAME; const gsiName = ""SnsMessageIdIndex""; // Name of the GSI created earlier if (!region || !tableName) { console.error(""Missing required environment variables: AWS_REGION, DYNAMODB_TABLE_NAME""); // Throwing error might cause retries; consider logging and exiting gracefully depending on retry strategy throw new Error(""Missing required environment variables.""); } const ddbClient = new DynamoDBClient({ region }); const ddbDocClient = DynamoDBDocumentClient.from(ddbClient); export const handler = async (event) => { console.log(""Received CloudWatch Logs event:"", JSON.stringify(event, null, 2)); // CloudWatch Logs data is base64 encoded and gzipped const payload = Buffer.from(event.awslogs.data, 'base64'); let logData; try { const decompressed = gunzipSync(payload); logData = JSON.parse(decompressed.toString('utf8')); } catch (err) { console.error(""Error decompressing or parsing CloudWatch log data:"", err); return ""Error processing logs: decompression/parsing failed.""; // Fail gracefully } console.log(""Decompressed Log Data:"", JSON.stringify(logData, null, 2)); if (logData.messageType !== ""LOG_GROUP"") { console.log(""Not a LOG_GROUP message, skipping.""); return `Skipped: Not a LOG_GROUP message`; } let updatesProcessed = 0; let errorsEncountered = 0; // Process each log event within the payload for (const logEvent of logData.logEvents) { try { const message = JSON.parse(logEvent.message); console.log(""Parsed Log Event Message:"", JSON.stringify(message, null, 2)); // --- Extract relevant fields --- // Adjust these fields based on the actual structure logged by SNS. Inspect your logs! const snsMessageId = message.notification?.messageId; // The ID returned by SNS when published const deliveryStatus = message.status; // e.g., SUCCESS, FAILURE const providerResponse = message.providerResponse || ""N/A""; // Reason for failure, etc. // Use log event timestamp as a fallback for updatedAt const timestamp = message.notification?.timestamp || new Date(logEvent.timestamp).toISOString(); if (!snsMessageId || !deliveryStatus) { console.warn(""Skipping log event - missing snsMessageId or status:"", logEvent.message); continue; // Skip if essential info is missing } // --- Find internalMessageId using snsMessageId via GSI --- // This is the crucial step: SNS logs don't contain our internal ID. // We query the index we created (SnsMessageIdIndex) which maps snsMessageId back to internalMessageId.
Frequently Asked Questions
How to track AWS SNS SMS delivery status in Next.js?
Implement a system using CloudWatch Logs and Lambda. SNS logs delivery attempts to CloudWatch, which triggers a Lambda function. This function parses the logs, queries a DynamoDB table using a Global Secondary Index (GSI) to find the message's internal ID, and updates its status.
What is the purpose of a Global Secondary Index (GSI) in this architecture?
The GSI allows efficient lookup of the internal message ID using the SNS message ID. Since SNS logs don't contain our internal ID, the GSI bridges this gap by indexing `snsMessageId` and projecting `internalMessageId`, enabling quick retrieval of the corresponding record in DynamoDB.
Why does AWS SNS not provide direct delivery status callbacks for SMS?
AWS SNS doesn't natively offer HTTP callbacks for SMS delivery status. This guide's architecture provides a workaround using CloudWatch Logs and Lambda to indirectly capture and process status updates.
When should I use IAM roles instead of access keys?
Always prefer IAM roles for production deployments. For local development, you can use access keys stored in `.env.local`. However, in production environments like Vercel, EC2, or ECS, IAM roles provide a more secure and manageable way to grant permissions to your application without exposing credentials.
How to send SMS messages with Next.js and AWS SNS?
Create a Next.js API route that uses the AWS SDK for JavaScript v3 to publish messages to an SNS topic. This API route should handle input validation, generate a unique internal message ID, store the initial message status in DynamoDB, and publish the SMS message via SNS.
What AWS services are used for the SNS delivery status tracking system?
The system uses Next.js for the frontend and API routes, AWS SNS for sending SMS, AWS CloudWatch Logs for capturing delivery status, AWS Lambda for processing logs, AWS DynamoDB for storing message status, AWS IAM for permissions, and the AWS SDK for JavaScript v3 for interacting with AWS services.
Can I customize the sender ID for SMS messages sent via SNS?
Yes, you can customize the sender ID by using `MessageAttributes` in the `PublishCommandInput` when sending the SMS message. Set `'AWS.SNS.SMS.SenderID'` to your desired alphanumeric sender ID (up to 11 characters).
What is the role of AWS Lambda in this SNS to DynamoDB flow?
Lambda acts as the processing engine triggered by CloudWatch Logs. It parses SNS delivery status logs, queries the DynamoDB GSI using the SNS message ID, retrieves the internal message ID, and updates the message status in DynamoDB.
How to set up the DynamoDB table for storing SMS message status?
Create a DynamoDB table with `internalMessageId` as the primary key. Add a Global Secondary Index (GSI) with `snsMessageId` as the partition key and project the `internalMessageId` attribute. This allows efficient lookup of the internal ID based on the SNS message ID logged by CloudWatch.
How do I create a new Next.js project for this tutorial?
Use the command `npx create-next-app@latest sns-status-tracker --typescript` in your terminal. This creates a new Next.js project with TypeScript. Then, navigate into the project directory using `cd sns-status-tracker`.
How do I install the required AWS SDK packages?
Run `npm install @aws-sdk/client-sns @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb` to install the necessary AWS SDK clients for interacting with SNS and DynamoDB.
Where do I configure AWS credentials for my Next.js application?
For local development, create a `.env.local` file in your project root. Store AWS credentials there. For production, use IAM roles or platform-specific secret management systems, keeping credentials out of your codebase.
What are the prerequisites for implementing this AWS SNS delivery status tracking system?
You'll need Node.js and npm/yarn, an AWS account, AWS CLI configured, and a basic understanding of Next.js, React, and AWS concepts. Also, ensure you have appropriate AWS permissions to create the required resources.
What data is logged by SNS to CloudWatch for delivery status?
SNS logs a JSON object containing details like message ID (`messageId`), delivery status (`status`), provider response (`providerResponse`), and timestamp. The Lambda function parses this data to update the message status in DynamoDB.