Building Production-Ready SMS Consent Management with Next.js and Sinch
Managing user consent is critical for any SMS marketing campaign, especially given regulations like TCPA in the US and GDPR in the EU. Failing to properly handle opt-ins and opt-outs can lead to significant legal and financial consequences, alongside damaging brand reputation.
This guide provides a step-by-step walkthrough for building a robust SMS consent management system using Next.js and the Sinch SMS API. We will create a Next.js application with an API endpoint that listens for incoming SMS messages sent to your Sinch number. This endpoint will process standard keywords like SUBSCRIBE
and STOP
, manage user participation in a Sinch group, and send appropriate confirmation messages back to the user.
Project Goal: To create a Next.js API endpoint that securely handles incoming Sinch SMS webhooks to manage user opt-ins (SUBSCRIBE
) and opt-outs (STOP
) for an SMS marketing group, leveraging the Sinch Node SDK for communication and group management.
Technologies Used:
- Next.js: A React framework enabling server-side rendering and API routes, perfect for handling webhooks and server-side logic within a single project.
- Sinch SMS API & Node SDK: Provides the necessary tools to send/receive SMS messages and manage contact groups programmatically.
- Node.js: The runtime environment for Next.js and the Sinch SDK.
System Architecture:
(Note: The following visualization describes a flow where User SMS -> Sinch -> Next.js API -> Sinch API -> Sinch Group/User SMS.)
Prerequisites:
- Node.js (v18 or later recommended) and npm/yarn installed.
- A Sinch account with access to the SMS API.
- A provisioned phone number within your Sinch account capable of sending and receiving SMS.
- Sinch API Credentials:
- Project ID
- API Key ID
- API Key Secret
- Service Plan ID associated with your SMS service.
- Familiarity with JavaScript/TypeScript and Next.js fundamentals.
- Crucially for US Traffic (A2P 10DLC): A registered Brand and an approved Campaign within the Sinch platform/TCR. This guide focuses on implementing consent logic, but you must complete 10DLC registration separately to legally send A2P marketing messages in the US. See the Sinch 10DLC Documentation for details.
By the end of this guide, you will have a deployable Next.js application capable of handling SMS consent keywords reliably and securely.
1. Setting up the Project
Let's start by creating a new Next.js project and installing the necessary dependencies.
-
Create a new Next.js App: Open your terminal and run:
npx create-next-app@latest sinch-consent-manager cd sinch-consent-manager
Follow the prompts (TypeScript recommended: Yes, ESLint: Yes, Tailwind CSS: No (or Yes if desired),
src/
directory: Yes, App Router: No (we'll use Pages Router for simpler API routes in this example), import alias: default). -
Install Sinch SDK: Install the core Sinch SDK and the SMS module:
npm install @sinch/sdk-core @sinch/sms
-
Set up Environment Variables: Create a file named
.env.local
in the root of your project. Never commit this file to version control. Add your Sinch credentials and configuration:# Sinch API Credentials & Configuration SINCH_KEY_ID=YOUR_SINCH_KEY_ID SINCH_KEY_SECRET=YOUR_SINCH_KEY_SECRET SINCH_PROJECT_ID=YOUR_SINCH_PROJECT_ID SINCH_SERVICE_PLAN_ID=YOUR_SINCH_SERVICE_PLAN_ID SINCH_NUMBER=YOUR_PROVISIONED_SINCH_PHONE_NUMBER # e.g., +12025550181 # Application Configuration SINCH_MARKETING_GROUP_NAME=""Marketing Subscribers"" # Name for the Sinch Group # Webhook Security (Basic Auth Example) # Generate strong random username/password WEBHOOK_USERNAME=your_secure_username WEBHOOK_PASSWORD=your_secure_password
SINCH_KEY_ID
,SINCH_KEY_SECRET
,SINCH_PROJECT_ID
: Find these in your Sinch Customer Dashboard under your Account > API Credentials or Project settings.SINCH_SERVICE_PLAN_ID
: Locate this ID associated with your SMS API service plan in the Sinch Dashboard (often under SMS > APIs).SINCH_NUMBER
: The full E.164 formatted phone number you've provisioned in Sinch for this campaign.SINCH_MARKETING_GROUP_NAME
: A descriptive name for the group we'll create in Sinch to manage subscribers.WEBHOOK_USERNAME
,WEBHOOK_PASSWORD
: Credentials for securing your webhook endpoint using Basic Authentication. Choose strong, unique values.
-
Project Structure: Your
src/pages/api/
directory is where the core webhook logic will reside. The rest follows a standard Next.js structure.
2. Implementing the API Endpoint Handler
We'll create a single API route in Next.js to handle incoming SMS messages from Sinch.
-
Create the API Route File: Create a new file:
src/pages/api/webhooks/sinch-sms.js
-
Implement the Webhook Handler: Add the following code to
src/pages/api/webhooks/sinch-sms.js
. This code initializes the Sinch client, handles Basic Auth, parses incoming messages, manages group membership, and sends replies.import { SinchClient } from '@sinch/sdk-core'; import { SmsRegion } from '@sinch/sms'; // Adjust region if needed (e.g., SmsRegion.EU) // --- Configuration --- const sinchClient = new SinchClient({ projectId: process.env.SINCH_PROJECT_ID, keyId: process.env.SINCH_KEY_ID, keySecret: process.env.SINCH_KEY_SECRET, // Optional: Specify region if not US default // smsRegion: SmsRegion.US, }); const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID; const sinchNumber = process.env.SINCH_NUMBER; const groupName = process.env.SINCH_MARKETING_GROUP_NAME || 'Default Marketing Group'; const webhookUser = process.env.WEBHOOK_USERNAME; const webhookPass = process.env.WEBHOOK_PASSWORD; // --- Helper Functions --- /** * Finds or creates a Sinch group by name. * Returns the Group ID. */ async function findOrCreateGroup(name) { try { const { groups } = await sinchClient.sms.groups.list(); let group = groups.find((g) => g.name === name); if (!group) { console.log(`Group ""${name}"" not found, creating...`); const newGroup = await sinchClient.sms.groups.create({ createGroupRequestBody: { name: name }, }); console.log(`Group ""${name}"" created with ID: ${newGroup.id}`); return newGroup.id; } else { console.log(`Found group ""${name}"" with ID: ${group.id}`); return group.id; } } catch (error) { console.error('Error finding or creating Sinch group:', error?.response?.data || error.message); throw new Error('Could not find or create Sinch group.'); } } /** * Sends an SMS message using the Sinch SDK. */ async function sendSms(to, message) { try { console.log(`Sending SMS to ${to}: ""${message}""`); // Note: Verify the method name and parameters below against the current Sinch SDK documentation. await sinchClient.sms.batches.send({ sendBatchSmsRequestBody: { to: [to], from: sinchNumber, body: message, delivery_report: 'none', // Adjust if delivery reports are needed }, // Pass servicePlanId if not using default project settings // servicePlanId: servicePlanId }); console.log(`SMS successfully sent to ${to}`); } catch (error) { console.error(`Error sending SMS to ${to}:`, error?.response?.data || error.message); // Implement retry logic here if needed (see Section 4 discussion) } } /** * Basic Authentication Middleware */ function handleBasicAuth(req, res) { if (!webhookUser || !webhookPass) { console.warn('Webhook basic auth credentials not set. Endpoint is unsecured.'); return true; // Skip auth if not configured (not recommended for production) } const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Basic ')) { console.warn('Missing or invalid Authorization header'); res.setHeader('WWW-Authenticate', 'Basic realm=""Secure Area""'); res.status(401).json({ error: 'Authorization Required' }); return false; } const base64Credentials = authHeader.split(' ')[1]; const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii'); const [username, password] = credentials.split(':'); if (username === webhookUser && password === webhookPass) { return true; // Authenticated } else { console.warn('Invalid Basic Auth credentials provided'); res.setHeader('WWW-Authenticate', 'Basic realm=""Secure Area""'); res.status(401).json({ error: 'Invalid Credentials' }); return false; } } // --- API Handler --- export default async function handler(req, res) { // 1. Only accept POST requests if (req.method !== 'POST') { console.log(`Received non-POST request: ${req.method}`); res.setHeader('Allow', ['POST']); return res.status(405).json({ error: `Method ${req.method} Not Allowed` }); } // 2. Authenticate the request (Basic Auth) if (!handleBasicAuth(req, res)) { return; // Response already sent by handleBasicAuth } // 3. Parse the incoming webhook payload const incomingSms = req.body; console.log('Received Sinch Webhook:', JSON.stringify(incomingSms, null, 2)); // Basic validation of expected payload structure if (!incomingSms || typeof incomingSms !== 'object' || !incomingSms.from || !incomingSms.body) { console.error('Invalid or missing webhook payload structure:', incomingSms); return res.status(400).json({ error: 'Invalid payload structure' }); } const fromNumber = incomingSms.from; // Sender's phone number const messageBody = incomingSms.body.trim().toUpperCase(); // Normalize keyword try { // 4. Find or Create the Target Group const groupId = await findOrCreateGroup(groupName); // 5. Process Keywords if (messageBody === 'SUBSCRIBE' || messageBody === 'JOIN' || messageBody === 'START') { console.log(`Processing SUBSCRIBE request from ${fromNumber} for group ${groupId}`); // Add number to the group // Note: Verify the method name and parameters below against the current Sinch SDK documentation. await sinchClient.sms.groups.replaceMembers({ groupId: groupId, addPhoneNumbersRequest: { add: [fromNumber] }, // Use 'add' to avoid removing others }); console.log(`Added ${fromNumber} to group ${groupId}`); // Send confirmation message const reply = `Thanks for subscribing to ${groupName}! Msg frequency varies. Msg&Data rates may apply. Reply HELP for help, STOP to cancel.`; await sendSms(fromNumber, reply); } else if (messageBody === 'STOP' || messageBody === 'UNSUBSCRIBE' || messageBody === 'CANCEL' || messageBody === 'END' || messageBody === 'QUIT') { console.log(`Processing STOP request from ${fromNumber} for group ${groupId}`); // Remove number from the group // Note: Verify the method name and parameters below against the current Sinch SDK documentation. await sinchClient.sms.groups.removeMember({ groupId: groupId, phoneNumber: fromNumber, }); console.log(`Removed ${fromNumber} from group ${groupId}`); // Send confirmation message const reply = `You have unsubscribed from ${groupName} and will no longer receive messages. Reply SUBSCRIBE to rejoin.`; await sendSms(fromNumber, reply); } else if (messageBody === 'HELP') { console.log(`Processing HELP request from ${fromNumber}`); // Send help message const reply = `${groupName}: For help, contact support@example.com or reply STOP to unsubscribe. Msg&Data rates may apply.`; // Customize help info await sendSms(fromNumber, reply); } else { // Optional: Handle unknown keywords or general messages console.log(`Received unknown keyword ""${messageBody}"" from ${fromNumber}`); // Decide if you want to reply to unknown messages. Sometimes it's better not to. // Example reply: // const reply = `Sorry, I didn't understand ""${incomingSms.body}"". Reply SUBSCRIBE to join or STOP to unsubscribe.`; // await sendSms(fromNumber, reply); } // 6. Respond to Sinch Platform // Sinch expects a 2xx status code to acknowledge receipt of the webhook. // No specific response body is usually required unless defined by Sinch for specific features. return res.status(200).json({ status: 'ok', message: 'Webhook processed' }); } catch (error) { console.error('Error processing webhook:', error); // Respond with an error status code to indicate failure. // Avoid sending detailed internal errors back in the response for security. return res.status(500).json({ error: 'Internal Server Error' }); } }
Code Explanation:
- Initialization: Sets up the
SinchClient
using credentials from environment variables. Defines constants for configuration. findOrCreateGroup
: Checks if the group defined bySINCH_MARKETING_GROUP_NAME
exists. If not, it creates it usingsinchClient.sms.groups.create
. It returns the group ID. Error Handling: Includes basic error logging. Production systems might require more sophisticated handling (e.g., specific error types, retries), but logging is sufficient for this guide.sendSms
: A utility to send an SMS usingsinchClient.sms.batches.send
. It takes the recipient number and message body. Error Handling: Logs errors during sending.handleBasicAuth
: Implements Basic Authentication checking based onWEBHOOK_USERNAME
andWEBHOOK_PASSWORD
. It sends appropriate 401 responses if auth fails or is missing. Returnstrue
on success,false
on failure (response sent).handler
(Main Logic):- Checks for
POST
method. - Calls
handleBasicAuth
to secure the endpoint. - Parses the
req.body
(Sinch sends JSON). Includes basic payload validation. While this basic check is useful, production applications might benefit from more rigorous validation using libraries likezod
orjoi
(as discussed in the Security section) to strictly enforce the expected schema. - Extracts the sender's number (
from
) and the message body (body
). Normalizes the body to uppercase for case-insensitive keyword matching. - Calls
findOrCreateGroup
to get the relevant group ID. - Uses
if/else if
blocks to check forSUBSCRIBE
/JOIN
/START
,STOP
/UNSUBSCRIBE
/etc., andHELP
keywords. - Subscribe Logic: Uses
sinchClient.sms.groups.replaceMembers
with theadd
property to add the number to the group without affecting other members. Sends a confirmation SMS. (Note: SDK method calls should be verified against current Sinch documentation.) - Stop Logic: Uses
sinchClient.sms.groups.removeMember
to remove the specific number. Sends an opt-out confirmation SMS. (Note: SDK method calls should be verified against current Sinch documentation.) - Help Logic: Sends a pre-defined help message.
- Unknown Keywords: Currently logs them. You can optionally add a reply here.
- Response: Sends a
200 OK
response back to Sinch to acknowledge successful processing. Sends500 Internal Server Error
if any part of the processing fails within thetry...catch
block.
- Checks for
- Initialization: Sets up the
3. Integrating with Sinch (Configuration)
Now that the code is ready, you need to configure your Sinch number to send incoming SMS messages to your deployed Next.js API endpoint.
-
Deploy Your Application: You need a publicly accessible URL for your API route. Deploy your Next.js application to a hosting provider like Vercel, Netlify, or AWS Amplify.
- Using Vercel (Example):
- Push your code to a Git repository (GitHub, GitLab, Bitbucket).
- Connect your repository to Vercel.
- Configure Environment Variables in the Vercel project settings (copy paste from your
.env.local
). - Deploy. Vercel will provide a production URL (e.g.,
https://your-app-name.vercel.app
).
- Your webhook URL will be:
https://your-app-name.vercel.app/api/webhooks/sinch-sms
- Using Vercel (Example):
-
Configure Sinch Webhook:
- Log in to your Sinch Customer Dashboard.
- Navigate to SMS -> APIs.
- Find your Service Plan ID (the one specified in
.env.local
) and click on it or its associated settings/edit icon. - Look for a section related to Callback URLs or Webhooks.
- In the field for Incoming Messages (MO) or a similar label, enter the full URL of your deployed API route:
https://your-app-name.vercel.app/api/webhooks/sinch-sms
- Configure Authentication: Find the settings for webhook authentication. Select Basic Authentication.
- Enter the Username defined in your
.env.local
(WEBHOOK_USERNAME
). - Enter the Password defined in your
.env.local
(WEBHOOK_PASSWORD
).
- Enter the Username defined in your
- Save the configuration.
(Dashboard navigation might vary slightly. Consult the official Sinch documentation for the most up-to-date instructions on configuring SMS webhooks.)
4. Error Handling, Logging, and Retries
- Error Handling: The provided API route includes
try...catch
blocks around the main processing logic and Sinch SDK calls. Errors are logged to the console (which Vercel or your hosting provider typically captures). A generic500 Internal Server Error
is sent back to Sinch upon failure, preventing leakage of internal details. - Logging: We use
console.log
for informational messages andconsole.error
for errors. For production, consider integrating a dedicated logging service (like Logtail, Datadog, Sentry) for better aggregation, searching, and alerting. - Retries:
- Sinch Webhook Retries: Sinch typically retries sending webhooks if it doesn't receive a
2xx
response within a timeout period. The exact policy (number of retries, intervals) can vary, so it's recommended to consult the official Sinch documentation regarding their webhook retry behavior. Ensuring your endpoint responds quickly (even with an error code) is important. - Sinch API Call Retries: The current code doesn't implement explicit retries for
sendSms
or group operations. For critical operations, you could wrap Sinch SDK calls in a retry mechanism (e.g., using libraries likeasync-retry
) with exponential backoff, especially for transient network errors or temporary Sinch API issues (like 5xx errors from Sinch). However, be cautious about retrying operations that modify state. Implementing idempotency keys or careful state checking is crucial when retrying write operations. For instance, retrying agroups.list
(read operation) is generally safe, whereas retryinggroups.addMember
(write operation) could potentially lead to duplicate actions if the initial request succeeded but the response was lost.
- Sinch Webhook Retries: Sinch typically retries sending webhooks if it doesn't receive a
5. Database Schema and Data Layer (Consideration)
While this guide uses Sinch Groups for simplicity, a production system often benefits from its own database to store subscriber information and consent status.
- Benefits: Persistence independent of Sinch, ability to store additional user data, detailed consent history (timestamps, source), easier querying and segmentation.
- Implementation (Example with Prisma):
- Install Prisma:
npm install prisma @prisma/client --save-dev
- Initialize Prisma:
npx prisma init --datasource-provider postgresql
(or your preferred DB) - Configure
prisma/schema.prisma
:datasource db { provider = ""postgresql"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } model Subscriber { id String @id @default(cuid()) phoneNumber String @unique // E.164 format isSubscribed Boolean @default(false) subscribedAt DateTime? unsubscribedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Add other fields as needed (e.g., source, name) }
- Set
DATABASE_URL
in.env.local
. - Run
npx prisma db push
to sync the schema. - Modify the API route to import
PrismaClient
and update the database record alongside the Sinch Group operations.
- Install Prisma:
Using a database adds complexity but provides greater control and data richness. For this guide's scope, we rely solely on Sinch Groups.
6. Security Features
- Webhook Authentication: Basic Authentication is implemented. This is crucial to ensure only Sinch can trigger your API route. Sinch also supports Signed Requests (HMAC), which offer stronger security as credentials aren't sent directly. Consider upgrading for higher security needs.
- Input Validation: Basic checks ensure the payload exists and has
from
andbody
. More robust validation could use libraries likezod
orjoi
to strictly enforce the expected Sinch payload schema. - HTTPS: Deploying via platforms like Vercel automatically enforces HTTPS, protecting data in transit.
- Rate Limiting: High-traffic webhooks could be rate-limited to prevent abuse or resource exhaustion. Vercel offers built-in rate limiting on certain plans. Alternatively, use middleware with libraries like
express-rate-limit
(if using a custom server) or similar concepts in Next.js API routes. - Environment Variable Security: Keep
.env.local
secure and never commit it. Use your hosting provider's secret management features (like Vercel Environment Variables). - Least Privilege (Sinch API Key): Ensure the Sinch API Key used has only the necessary permissions (SMS sending, group management) required for this application.
7. Handling Special Cases
- Keyword Case-Insensitivity: Handled by converting the incoming message body to uppercase (
.toUpperCase()
). - Whitespace: Handled using
.trim()
on the message body. - Multiple Keywords: The code handles common variations like
SUBSCRIBE
/JOIN
/START
andSTOP
/UNSUBSCRIBE
/etc. You can easily add more synonyms. - International Numbers: The code assumes E.164 format (
+
followed by country code and number) as provided by Sinch. - Character Encoding: Sinch webhooks typically use UTF-8 JSON, which Node.js handles correctly by default.
- Duplicate Messages: Sinch might occasionally send duplicate webhooks. While the group add/remove operations are somewhat idempotent (adding an existing member or removing a non-existent member might not cause errors, though check SDK specifics), consider adding logic to check the
isSubscribed
status in a database (if used) before performing the action.
8. Performance Optimizations
- Webhook Response Time: Respond to Sinch quickly (ideally under 2 seconds) to avoid timeouts and retries. The current logic is lightweight. If you add slow operations (complex DB queries, external API calls), consider deferring them using background jobs/queues (e.g., Vercel Serverless Functions triggered by your webhook, or dedicated queue services).
- Sinch Client Initialization: The
SinchClient
is initialized outside the handler function, so it's reused across requests within the same serverless function instance, improving performance slightly. - Caching Group ID: The
findOrCreateGroup
call involves an API request. If performance is critical and the group rarely changes, you could cache thegroupId
in memory (with a short TTL) or a faster cache like Redis, but this adds complexity and is likely unnecessary for typical volumes.
9. Monitoring, Observability, and Analytics
- Health Checks: Create a simple health check endpoint (e.g.,
src/pages/api/health.js
) that returns200 OK
to verify the deployment is live.export default function handler(req, res) { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }
- Logging: Centralized logging (Vercel Logs, Datadog, etc.) is crucial for monitoring webhook activity, errors, and execution flow.
- Error Tracking: Integrate services like Sentry (
npm install @sentry/nextjs
,npx @sentry/wizard@latest -i nextjs
) to capture, track, and get alerted on runtime errors in your API route. - Metrics: Track key metrics:
- Webhook request count & latency.
- Number of
SUBSCRIBE
,STOP
,HELP
events processed. - Sinch API call latency and error rates.
- (If using DB) Subscriber growth/churn rate.
- Hosting platforms often provide basic metrics. For more detail, use monitoring services.
- Dashboards: Create dashboards (e.g., in Datadog, Grafana, or your logging provider) visualizing these metrics to understand traffic patterns and system health.
- Alerting: Set up alerts (e.g., in Sentry or your monitoring tool) for:
- High error rates (>1% of requests).
- Sustained high latency (>2 seconds).
- Failures in critical operations (e.g.,
findOrCreateGroup
).
10. Troubleshooting and Caveats
- Webhook Not Firing:
- Verify the Callback URL in Sinch Dashboard is exactly correct and points to your deployed application URL.
- Check your application logs (e.g., Vercel Logs) for any incoming requests or startup errors.
- Ensure your deployment is healthy and accessible.
- Check Sinch Dashboard for any delivery errors related to webhooks.
- Ensure Basic Auth credentials match between
.env.local
/Vercel variables and Sinch configuration.
- Authentication Errors (401 Unauthorized):
- Double-check
WEBHOOK_USERNAME
andWEBHOOK_PASSWORD
match exactly between your environment variables and the Sinch webhook configuration. - Ensure the
Authorization: Basic ...
header is being sent correctly by Sinch (usually automatic if configured).
- Double-check
- Sinch API Errors (Logged in Console):
401 Unauthorized
: CheckSINCH_KEY_ID
,SINCH_KEY_SECRET
,SINCH_PROJECT_ID
. Ensure the key is active and has SMS permissions.400 Bad Request
: Often indicates an issue with the request body sent to Sinch (e.g., invalid phone number format, missing required fields). Check the error details logged. Could also be an incorrectSINCH_SERVICE_PLAN_ID
.404 Not Found
: Could mean thegroupId
is incorrect or the resource doesn't exist.5xx Server Error
: Transient issue on Sinch's side. Consider implementing retries (see Section 4).- Check
SinchClient
Configuration: Ensure the correctsmsRegion
(e.g.,SmsRegion.EU
,SmsRegion.US
) is configured when initializing theSinchClient
if your service plan is not based in the default US region.
- Consent Logic Not Working:
- Check logs to see if keywords are being recognized correctly (case/trimming).
- Verify the
groupId
being used is correct. Use the Sinch API (or dashboard, if available) to inspect the group members directly. - Ensure
fromNumber
is being parsed correctly from the webhook.
- Caveats:
- 10DLC Compliance (US): This code implements consent handling but does not fulfill 10DLC registration requirements. You must register your Brand and Campaign via Sinch for A2P SMS marketing in the US. Failure to do so will result in blocked messages and potential fines.
- Sinch Groups vs. Database: Sinch Groups are convenient but may have limitations in querying, scalability, or data richness compared to managing subscribers in your own database.
- Idempotency: While basic group operations might be somewhat idempotent, ensure your logic handles potential duplicate webhook deliveries gracefully, especially if integrating with other systems or a database.
- Rate Limits: Be aware of Sinch API rate limits. High-volume processing might require strategies to stay within limits.
- Error Handling Granularity: The current error handling logs errors and returns 500. More granular error handling could provide better insights or trigger specific recovery actions.
- SDK Verification: The Sinch SDK method names and parameters used in the code examples should be verified against the latest official Sinch Node SDK documentation to ensure they are current and accurate.
11. Deployment and CI/CD
- Deployment (Vercel Example):
- Ensure
.env.local
is in your.gitignore
. - Push code to your Git provider.
- Create a new Project on Vercel, importing your Git repository.
- Framework Preset: Should be detected as Next.js.
- Environment Variables: Copy all key-value pairs from
.env.local
into the Vercel Project Settings > Environment Variables section. Ensure they are available for the ""Production"" environment (and Preview/Development if needed). - Deploy. Vercel builds and deploys the app, providing the production URL.
- Ensure
- CI/CD:
- Vercel automatically sets up CI/CD. Pushing to the main branch triggers a production deployment. Pushing to other branches or creating Pull Requests triggers preview deployments.
- Automated Testing: Integrate testing (Unit, Integration) into your pipeline. Add an
npm run test
script to yourpackage.json
and configure Vercel (or your CI/CD tool like GitHub Actions) to run tests before deploying. Fail the build if tests fail. - Environment Configuration: Use Vercel's environment variable system to manage different configurations (e.g., separate Sinch credentials or group names for staging vs. production).
- Rollbacks: Vercel maintains previous deployments. If a deployment introduces issues, you can instantly roll back to a previous working deployment via the Vercel dashboard.
12. Verification and Testing
- Manual Verification:
- Deploy the application and configure the Sinch webhook correctly (Section 3).
- Using a test phone number (not the Sinch number itself), send SMS messages to your provisioned
SINCH_NUMBER
:- Send
SUBSCRIBE
(orJoin
,Start
). - Expected: Receive the opt-in confirmation SMS. Check application logs for successful processing. Check Sinch Group members (via API or dashboard if possible) to confirm the number was added.
- Send
STOP
(orUnsubscribe
,Quit
). - Expected: Receive the opt-out confirmation SMS. Check logs. Check Sinch Group to confirm removal.
- Send
HELP
. - Expected: Receive the help message SMS. Check logs.
- Send a random message (e.g.,
Hello
). - Expected: No reply (based on current code). Check logs for ""unknown keyword"" message.
- Send
STOP
again after unsubscribing. - Expected: Receive the opt-out confirmation SMS again (or handle gracefully, e.g., log ""already unsubscribed""). Check logs. Verify the number is still not in the group.
- Send