Developer Guide: Building an SMS Marketing Campaign App with Next.js and MessageBird
This guide provides a step-by-step walkthrough for building the core components of an SMS marketing campaign application using Next.js and the MessageBird SMS API. You'll learn how to handle SMS subscriptions (opt-in/opt-out) via keywords and enable an administrator to broadcast messages to subscribers. While this guide covers the fundamental logic, critical features like robust authentication (discussed in Section 7) must be implemented for a truly production-ready deployment.
Project Goal: Create a web application where:
- Users can text
SUBSCRIBE
to a dedicated virtual mobile number (VMN) to opt into receiving SMS marketing messages. - Subscribers receive an SMS confirmation upon successful subscription.
- Subscribers can text
STOP
to the same VMN to opt out. - Opt-outs are confirmed via SMS.
- An administrator can use a simple web interface to send custom SMS messages to all currently subscribed users.
Target Audience: Developers familiar with JavaScript, Node.js, and Next.js basics, looking to integrate SMS functionality into their applications using MessageBird.
Technologies Used:
- Next.js: A React framework for building server-side rendered and static web applications. Chosen for its robust routing (including API routes via the App Router), developer experience, and performance features.
- MessageBird SMS API: Used for sending and receiving SMS messages programmatically. Chosen for its reliability, global reach, and developer-friendly API/SDK.
- Prisma: A modern Node.js and TypeScript ORM for database access. Chosen for its type safety, auto-completion, and migration capabilities.
- PostgreSQL (or SQLite/MySQL): The database to store subscriber information. (This guide uses PostgreSQL, but Prisma supports others).
- Localtunnel/ngrok: For exposing the local development server to the internet to receive MessageBird webhooks.
System Architecture:
graph LR
User -- SMS (SUBSCRIBE/STOP) --> MessageBirdVMN[MessageBird VMN]
MessageBirdVMN -- Webhook --> NextAppWebhook[/api/messagebird/webhook]
NextAppWebhook -- Read/Write --> DB[(Database)]
NextAppWebhook -- Send Confirmation SMS --> MessageBirdAPI[MessageBird API]
MessageBirdAPI -- SMS --> User
Admin -- HTTP Request (Send Message) --> NextAppUI[Next.js UI (Admin Form)]
NextAppUI -- API Call --> NextAppSendAPI[/api/messagebird/send]
NextAppSendAPI -- Read Subscribers --> DB
NextAppSendAPI -- Send Broadcast SMS --> MessageBirdAPI
MessageBirdAPI -- SMS --> User
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- A MessageBird account.
- A text editor or IDE (e.g., VS Code).
- Access to a terminal or command prompt.
- (Optional but Recommended) Git for version control.
- (Optional but Recommended) Docker for running a local PostgreSQL database easily.
Final Outcome: A functional Next.js application capable of managing SMS subscriptions and broadcasting messages via MessageBird, forming a strong base for a production system once security and other considerations are fully implemented.
1. Setting up the Project
This section covers initializing the Next.js project, installing dependencies, setting up the database connection with Prisma, and configuring environment variables.
1.1 Initialize Next.js Project:
Open your terminal and run the following command to create a new Next.js application. Use the App Router when prompted (recommended for this guide).
npx create-next-app@latest messagebird-sms-campaigns
cd messagebird-sms-campaigns
1.2 Install Dependencies:
Install the necessary libraries: MessageBird SDK and Prisma Client.
npm install @messagebird/api prisma
npm install --save-dev prisma
@messagebird/api
: The official MessageBird Node.js SDK.prisma
: The Prisma CLI (dev dependency) and Prisma Client.
Note: Next.js automatically loads variables from .env
files, so explicit use of the dotenv
package is often unnecessary within the Next.js application code itself.
1.3 Set up Prisma:
Initialize Prisma in your project. This creates a prisma
directory with a schema.prisma
file and updates your .gitignore
. It also prompts for creating a .env
file if one doesn't exist.
npx prisma init --datasource-provider postgresql
(Note: If you prefer SQLite or MySQL, replace postgresql
accordingly).
1.4 Configure Database Connection:
Open the .env
file (create it if it doesn't exist). Add your DATABASE_URL
variable.
- Using Docker for Local PostgreSQL:
If you have Docker installed, you can easily spin up a PostgreSQL container:
Your
docker run --name messagebird-db -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
.env
file would look like this:# .env DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public" # Add MessageBird variables later MESSAGEBIRD_API_KEY= MESSAGEBIRD_ORIGINATOR=
- Using other database providers (e.g., Supabase, Neon, Railway): Obtain the connection string from your provider and paste it here.
1.5 Define Prisma Schema:
Open prisma/schema.prisma
and define the model for storing subscriber information.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // Or your chosen provider
url = env("DATABASE_URL")
}
model Subscriber {
id String @id @default(cuid())
phoneNumber String @unique // E.164 format recommended (e.g., +1xxxxxxxxxx)
subscribed Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([subscribed]) // Add index for filtering by subscription status
}
phoneNumber
: Stores the subscriber's phone number (ensure uniqueness). Storing in E.164 format is recommended for consistency.subscribed
: A boolean flag indicating the current subscription status. An index is added for efficient querying.createdAt
,updatedAt
: Timestamps managed automatically by Prisma.
1.6 Apply Database Migrations:
Run the Prisma migrate command to create the Subscriber
table in your database based on the schema. Prisma will prompt you to create a name for the migration (e.g., init
).
npx prisma migrate dev --name init
This command:
- Creates an SQL migration file in
prisma/migrations
. - Applies the migration to your database, creating the
Subscriber
table and indexes. - Generates the Prisma Client based on your schema.
Your basic project structure and database setup are now complete.
2. Implementing Core Functionality: Receiving SMS
This section focuses on handling incoming SMS messages (SUBSCRIBE/STOP) via a Next.js API route acting as a webhook for MessageBird.
2.1 Create Prisma Client Utility:
To avoid creating multiple Prisma Client instances (which is inefficient), set up a singleton instance.
Create lib/prisma.ts
:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
// Prevent multiple instances of Prisma Client in development
const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV === 'development') global.prisma = prisma;
export default prisma;
2.2 Create MessageBird Client Utility:
Similarly, initialize the MessageBird client once.
Create lib/messagebird.ts
:
// lib/messagebird.ts
import MessageBird from '@messagebird/api';
// Next.js automatically loads environment variables from .env files
const apiKey = process.env.MESSAGEBIRD_API_KEY;
if (!apiKey) {
// Throw error during initialization if key is missing
throw new Error(""MESSAGEBIRD_API_KEY environment variable not set."");
}
const messagebird = MessageBird(apiKey);
export default messagebird;
Make sure to add MESSAGEBIRD_API_KEY
to your .env
file later (see Section 4).
2.3 Create the Webhook API Route:
This API route will receive POST requests from MessageBird whenever an SMS is sent to your virtual number.
Create app/api/messagebird/webhook/route.ts
(using App Router structure):
// app/api/messagebird/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma'; // Adjust path based on your structure
import messagebird from '@/lib/messagebird'; // Adjust path based on your structure
import { MessageBird } from '@messagebird/api'; // Import type for callback
// Define types matching those used in the send route for consistency
interface MessageBirdResponse {
id: string;
// Add other relevant fields based on MessageBird's actual webhook response structure if needed
}
interface MessageBirdError {
errors: Array<{
code: number;
description: string;
parameter: string | null;
}>;
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const originator = body.originator; // Sender's phone number
const payload = body.payload?.trim().toUpperCase(); // Message content
if (!originator || !payload) {
console.warn('Webhook received invalid data:', body);
return NextResponse.json({ message: 'Missing originator or payload' }, { status: 400 });
}
console.log(`Webhook received: From ${originator}, Payload: ${payload}`);
// Ensure E.164 format for storage consistency. Check if '+' is missing.
// Adjust this logic based on the actual format MessageBird sends `originator`.
// Storing consistently (e.g., with '+') is recommended.
const originatorFormatted = originator.startsWith('+') ? originator : `+${originator}`;
let confirmationMessage = '';
let subscriber = await prisma.subscriber.findUnique({
where: { phoneNumber: originatorFormatted },
});
if (payload === 'SUBSCRIBE') {
if (!subscriber) {
// New subscriber
subscriber = await prisma.subscriber.create({
data: { phoneNumber: originatorFormatted, subscribed: true },
});
console.log(`New subscriber added: ${originatorFormatted}`);
confirmationMessage = 'Thanks for subscribing! Text STOP anytime to opt-out.';
} else if (!subscriber.subscribed) {
// Re-subscribing
subscriber = await prisma.subscriber.update({
where: { phoneNumber: originatorFormatted },
data: { subscribed: true },
});
console.log(`Subscriber re-subscribed: ${originatorFormatted}`);
confirmationMessage = 'Welcome back! You are now subscribed again.';
} else {
// Already subscribed
console.log(`Subscriber already subscribed: ${originatorFormatted}`);
confirmationMessage = 'You are already subscribed.';
}
} else if (payload === 'STOP') {
if (subscriber && subscriber.subscribed) {
// Opting out
subscriber = await prisma.subscriber.update({
where: { phoneNumber: originatorFormatted },
data: { subscribed: false },
});
console.log(`Subscriber opted-out: ${originatorFormatted}`);
confirmationMessage = 'You have successfully unsubscribed. Text SUBSCRIBE to join again.';
} else {
// Not subscribed or already opted out
console.log(`Received STOP from non-subscriber or already opted-out: ${originatorFormatted}`);
// Optionally send a message, or do nothing
// confirmationMessage = 'You were not subscribed to this list.';
}
} else {
// Handle unrecognized commands (optional)
console.log(`Received unrecognized command from ${originatorFormatted}: ${payload}`);
// confirmationMessage = 'Unrecognized command. Text SUBSCRIBE to join or STOP to leave.';
}
// Send confirmation SMS if needed
if (confirmationMessage && process.env.MESSAGEBIRD_ORIGINATOR) {
try {
await new Promise<void>((resolve, reject) => {
messagebird.messages.create({
originator: process.env.MESSAGEBIRD_ORIGINATOR!,
// Use the original number format received from webhook for the recipient field here,
// as MessageBird expects it for replying.
recipients: [originator],
body: confirmationMessage,
}, (err: MessageBirdError | null, response: MessageBirdResponse | any) => { // Use defined types
if (err) {
console.error(""Error sending confirmation SMS:"", err);
// Log error but don't fail the webhook response if SMS fails
// Consider adding more robust error handling/retries if confirmation is critical
return reject(err); // Reject promise on error
}
console.log(""Confirmation SMS sent:"", response?.id);
resolve(); // Resolve promise on success
});
});
} catch (smsError) {
// Log error from the promise rejection or other issues
console.error('Failed to send confirmation SMS:', smsError);
}
}
// Respond to MessageBird promptly to acknowledge receipt
return NextResponse.json({ message: 'Webhook processed successfully' }, { status: 200 });
} catch (error) {
console.error('Webhook processing error:', error);
// Return a 500 error but still acknowledge receipt if possible
// Avoid sending detailed errors back which might expose info
return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}
// Basic GET handler for testing reachability (optional)
export async function GET() {
return NextResponse.json({ message: 'Webhook endpoint is active. Use POST for events.' });
}
Explanation:
- Import Dependencies: Import
NextRequest
,NextResponse
,prisma
,messagebird
, and types. - POST Handler: Defines an
async
function to handle POST requests. - Parse Request: Reads the JSON body sent by MessageBird (
originator
,payload
). - Input Validation: Basic check if essential fields are present.
- Logging: Logs incoming requests for debugging.
- Format Number: Ensures the phone number (
originatorFormatted
) is consistently formatted (e.g., E.164+1xxxxxxxxxx
) for database storage. Adjust based on observed MessageBird format if needed. - Database Lookup: Checks if the formatted
originatorFormatted
exists in theSubscriber
table. - Command Logic (
SUBSCRIBE
/STOP
): Handles new subscriptions, re-subscriptions, and opt-outs by creating or updating the database record. Sets appropriateconfirmationMessage
text. - Send Confirmation SMS: If a message is set and
MESSAGEBIRD_ORIGINATOR
is configured, it usesmessagebird.messages.create
to send an SMS back. Note the use ofnew Promise
to handle the SDK's callback within anasync
function. Uses the originaloriginator
format for the recipient field when replying. - Error Handling: Includes
try...catch
blocks for overall processing and specifically for SMS sending. Logs errors. - Acknowledge Receipt: Returns a
200 OK
JSON response to MessageBird. This is crucial for preventing retries.
3. Implementing Core Functionality: Sending Broadcasts
This section covers creating a simple admin interface to send messages and the corresponding API route to handle the broadcast logic.
3.1 Create the Admin UI:
Create a simple page with a form for the administrator to type and send messages.
Create app/admin/page.tsx
(App Router):
// app/admin/page.tsx
'use client'; // This component needs client-side interactivity
import { useState } from 'react';
export default function AdminPage() {
const [message, setMessage] = useState('');
const [status, setStatus] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!message.trim()) {
setStatus('Please enter a message.');
return;
}
setLoading(true);
setStatus('Sending...');
try {
const response = await fetch('/api/messagebird/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
const result = await response.json();
if (response.ok) {
setStatus(`Message sent successfully to ${result.sentCount} subscribers.`);
setMessage(''); // Clear textarea on success
} else if (response.status === 207) { // Handle partial success
setStatus(`Message sent, but some batches failed. Estimated sent count: ${result.sentCount}. Check server logs for details.`);
} else {
setStatus(`Error: ${result.message || 'Failed to send message'}`);
}
} catch (error) {
console.error('Send message error:', error);
setStatus('An unexpected error occurred. Check the console or server logs.');
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px'_ maxWidth: '600px'_ margin: 'auto' }}>
<h1>Send Broadcast SMS</h1>
<form onSubmit={handleSubmit}>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter your message here..."
rows={5}
style={{ width: '100%', marginBottom: '10px', padding: '8px', display: 'block' }}
disabled={loading}
/>
<button type="submit" disabled={loading} style={{ padding: '10px 15px' }}>
{loading ? 'Sending...' : 'Send to All Subscribers'}
</button>
</form>
{status && <p style={{ marginTop: '15px' }}>{status}</p>}
{/* Basic Security Warning */}
<p style={{ marginTop: '30px'_ color: 'red'_ border: '1px solid red'_ padding: '10px' }}>
<strong>Warning:</strong> This admin page is currently unsecured. Anyone with the URL can send messages. Implement proper authentication in a production environment (see Section 7).
</p>
</div>
);
}
Explanation:
- Uses React state (
useState
) for message input, loading state, and status feedback. handleSubmit
sends a POST request to/api/messagebird/send
.- Provides user feedback, including handling partial success (HTTP 207).
- Includes a prominent warning about the lack of security.
3.2 Create the Send API Route:
This route fetches active subscribers and sends the message using the MessageBird SDK, handling batching.
Create app/api/messagebird/send/route.ts
:
// app/api/messagebird/send/route.ts
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import messagebird from '@/lib/messagebird';
import { MessageBird } from '@messagebird/api'; // Import type for callback
// Define types for better error/response handling (optional but good practice)
interface MessageBirdResponse {
id: string;
recipients: {
totalSentCount: number;
totalDeliveredCount: number;
totalDeliveryFailedCount: number;
items: Array<{
recipient: number; // Note: MessageBird often uses numbers here
status: string;
statusDatetime: string;
}>;
};
// Add other fields as necessary based on actual API response
}
interface MessageBirdError {
errors: Array<{
code: number;
description: string;
parameter: string | null;
}>;
}
export async function POST(req: NextRequest) {
// IMPORTANT: Add Authentication/Authorization here in production!
// Example: Check session, API key, etc.
// const session = await getServerSession(authOptions); // Example using NextAuth
// if (!session || !session.user.isAdmin) { // Check for admin role
// return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
// }
try {
const body = await req.json();
const message = body.message;
if (!message || typeof message !== 'string' || message.trim().length === 0) {
return NextResponse.json({ message: 'Message content is required' }, { status: 400 });
}
if (!process.env.MESSAGEBIRD_ORIGINATOR) {
console.error("MESSAGEBIRD_ORIGINATOR is not set.");
return NextResponse.json({ message: 'Server configuration error: Originator not set' }, { status: 500 });
}
// Fetch subscribed users
const subscribers = await prisma.subscriber.findMany({
where: { subscribed: true },
select: { phoneNumber: true }, // Only fetch the phone number
});
if (subscribers.length === 0) {
console.log("Send API: No active subscribers found.");
return NextResponse.json({ message: 'No active subscribers found.', sentCount: 0 }, { status: 200 });
}
// Assuming E.164 format stored in DB (e.g., "+1...").
// Verify the exact format required by messagebird.messages.create. E.164 is usually preferred/accepted.
const recipientNumbers = subscribers.map(sub => sub.phoneNumber);
const batchSize = 50; // MessageBird API allows up to 50 recipients per request
let totalSentCount = 0;
let successfulBatches = 0;
let failedBatches = 0;
console.log(`Send API: Attempting to send message to ${recipientNumbers.length} subscribers in batches of ${batchSize}.`);
for (let i = 0; i < recipientNumbers.length; i += batchSize) {
const batch = recipientNumbers.slice(i_ i + batchSize);
try {
await new Promise<void>((resolve, reject) => {
messagebird.messages.create({
originator: process.env.MESSAGEBIRD_ORIGINATOR!,
recipients: batch, // Send the batch of numbers
body: message,
}, (err: MessageBirdError | null, response: MessageBirdResponse | any) => { // Use defined types
if (err) {
console.error(`Send API: Error sending SMS batch (start index ${i}):`, JSON.stringify(err, null, 2));
// Decide if you want to stop on error or continue with other batches
return reject(err); // Reject the promise on error
}
// Attempt to get actual count from response, fallback to batch length
const sentInBatch = response?.recipients?.totalSentCount ?? batch.length;
totalSentCount += sentInBatch;
console.log(`Send API: SMS batch sent (start index ${i}), recipients: ${batch.length}, API Response ID: ${response?.id}`);
successfulBatches++;
resolve(); // Resolve the promise on success
});
});
} catch (batchError) {
console.error(`Send API: Failed to process batch starting at index ${i}:`, batchError);
failedBatches++;
// Optional: Implement retry logic here for the failed batch, or log for manual retry
}
}
console.log(`Send API: Broadcast finished. Successful batches: ${successfulBatches}, Failed batches: ${failedBatches}, Total estimated sent: ${totalSentCount}`);
if (failedBatches > 0 && successfulBatches === 0) {
// If all batches failed
return NextResponse.json({ message: 'Failed to send message to any subscribers.' }, { status: 500 });
} else if (failedBatches > 0) {
// If some batches failed
return NextResponse.json({ message: `Message sent, but ${failedBatches} batches failed. Estimated sent count: ${totalSentCount}`, sentCount: totalSentCount }, { status: 207 }); // 207 Multi-Status
} else {
// If all batches succeeded
return NextResponse.json({ message: 'Message broadcast initiated successfully.', sentCount: totalSentCount }, { status: 200 });
}
} catch (error) {
console.error('Error in /api/messagebird/send:', error);
return NextResponse.json({ message: 'Internal server error' }, { status: 500 });
}
}
Explanation:
- Authentication Placeholder: Includes a critical comment reminder to add security checks.
- Parse Request & Validate: Gets the
message
and validates it. Checks forMESSAGEBIRD_ORIGINATOR
. - Fetch Subscribers: Queries the database for active subscribers, selecting only
phoneNumber
. - Handle No Subscribers: Returns early if the list is empty.
- Prepare Recipients: Maps the
phoneNumber
array. Crucially, confirms the required format for the SDK (E.164 with+
is assumed here, matching storage). - Batching: Sets
batchSize
to 50 and loops through recipients. - Send Batch: Calls
messagebird.messages.create
for each batch usingnew Promise
for the callback. - Logging & Error Handling: Logs success/errors per batch. Tracks counts. Handles promise rejection.
- Response: Returns appropriate status codes (200 OK, 207 Multi-Status for partial success, 500 Internal Server Error) with informative messages and the estimated sent count.
4. Integrating with MessageBird
This section details obtaining necessary credentials from MessageBird and configuring your application and the MessageBird platform.
4.1 Get MessageBird API Key:
-
Log in to your MessageBird Dashboard.
-
Navigate to Developers > API access.
-
Use an existing live API key or click Add access key.
-
Copy the Live API key. Keep it secret!
-
Add this key to your
.env
file:# .env DATABASE_URL=""postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public"" MESSAGEBIRD_API_KEY=live_YOUR_API_KEY_HERE # Paste your key here MESSAGEBIRD_ORIGINATOR=
4.2 Get a Virtual Mobile Number (Originator):
You need a number for users to text and to send messages from.
-
In the MessageBird Dashboard, go to Numbers.
-
Click Buy a number.
-
Select the Country, ensure SMS capability is checked, choose a number, and complete the purchase.
-
Copy the purchased number (in E.164 format, e.g.,
+12025550183
). -
Add this number to your
.env
file:# .env DATABASE_URL=""postgresql://postgres:mysecretpassword@localhost:5432/postgres?schema=public"" MESSAGEBIRD_API_KEY=live_YOUR_API_KEY_HERE MESSAGEBIRD_ORIGINATOR=+12025550183 # Paste your purchased number here
4.3 Expose Local Development Server:
MessageBird needs a public URL to send webhooks to your local machine. Use localtunnel
or ngrok
.
- Install localtunnel (if not already):
npm install -g localtunnel
- Run your Next.js app:
npm run dev
(usually on port 3000) - Start localtunnel: In a new terminal, run:
lt --port 3000
- Copy the public URL provided (e.g.,
https://your-subdomain.loca.lt
). Keep this terminal running. This URL is temporary.
4.4 Configure MessageBird Flow Builder:
Connect your number to your webhook API route.
- Go to Numbers in the MessageBird Dashboard.
- Find your number and click the Flow icon or navigate to its Flows tab.
- Click Create new flow > Create Custom Flow.
- Name the flow (e.g., ""SMS Subscription Handler"").
- Select SMS as the trigger. Click Next.
- Click the + below the SMS trigger step and choose Forward to URL.
- Set Method to POST.
- In URL, paste your
localtunnel
URL + webhook path:https://your-subdomain.loca.lt/api/messagebird/webhook
- Click Save.
- Click Publish changes in the top-right.
Now, SMS messages sent to your MessageBird number will trigger a POST request to your local development server's webhook endpoint.
5. Error Handling, Logging, and Retries
Robust applications need proper error handling and logging.
5.1 Error Handling Strategy:
- API Routes: Use
try...catch
around major operations. - Log Errors: Log detailed errors server-side (
console.error
or a logger). Return generic client errors. - Specific Error Codes: Use appropriate HTTP status codes (400, 401, 500, 207).
- MessageBird Errors: Log the
err
object from the SDK callback to understand API issues (e.g., auth errors, invalid numbers).
5.2 Logging:
- Development:
console.log
/error
is okay. Log key events (webhook receipt, DB actions, send attempts, errors). - Production: Use structured logging (e.g.,
pino
) for better analysis and integration with log management services.- Install:
npm install pino pino-pretty
- Setup (
lib/logger.ts
):Replace// lib/logger.ts import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined, }); export default logger;
console.*
calls withlogger.info()
,logger.error()
, etc.
- Install:
5.3 Retry Mechanisms:
- Webhook Receiving: MessageBird retries if your endpoint fails or times out. Ensure your webhook responds quickly (within a few seconds) with a 2xx code. Acknowledge receipt first, then process if necessary (though current logic is fast enough).
- SMS Sending (Broadcasts): If
messagebird.messages.create
fails for a batch (e.g., temporary network issue, MessageBird 5xx error), consider:- Logging Failures: The current code logs failed batches. An admin could potentially retry manually.
- Simple Retry: Add a small delay and retry the failed batch once or twice within the
catch
block. - Exponential Backoff: Use libraries like
async-retry
for more robust retries with increasing delays. This adds complexity. - Recommendation: For this app, logging failures is often sufficient for broadcasts, allowing manual investigation. Retrying individual confirmation SMS might be more practical if needed.
6. Database Schema and Data Layer
- Schema:
prisma/schema.prisma
(Section 1.5) defines theSubscriber
model. Add related models (e.g.,MessageLog
) as needed. - Data Access: Use the singleton Prisma Client (
lib/prisma.ts
) for type-safe DB operations (findUnique
,findMany
,create
,update
). - Migrations: Use
npx prisma migrate dev
locally andnpx prisma migrate deploy
in CI/CD for production schema changes. - Performance: Indexes on
phoneNumber
(@unique
) andsubscribed
(@@index
) are included. Analyze query performance (EXPLAIN
) for complex queries if needed. - Seeding: Use
prisma/seed.ts
for test data (npx prisma db seed
).
7. Adding Security Features
- Admin Authentication: CRITICAL. Protect
/admin
and/api/messagebird/send
.- Options: NextAuth.js (recommended for Next.js), Clerk, Lucia Auth. Simple HTTP Basic Auth or IP whitelisting are less secure alternatives.
- Implementation: Use Next.js Middleware (
middleware.ts
) to intercept requests to protected routes, verify session/token/API key, and redirect/block unauthorized access. Check for specific admin roles if applicable.
- Input Validation:
- Webhook: Sanitize
payload
(usingtoUpperCase
). Validateoriginator
format if strict E.164 is required (though standardization logic helps). - Send API: Ensure
message
is a non-empty string. Consider adding length validation/feedback in the UI.
- Webhook: Sanitize
- Rate Limiting: Protect API routes from abuse.
- Options:
rate-limiter-flexible
,upstash/ratelimit
, Vercel Edge Middleware rate limiting. - Implementation: Apply rate limits (e.g., per IP or user ID) to
/api/messagebird/webhook
and/api/messagebird/send
.
- Options:
- CSRF Protection: Generally handled well by Next.js for same-origin requests. Frameworks like NextAuth.js provide robust CSRF protection.
- Environment Variables: Keep secrets (
.env
) out of Git. Use platform environment variable management (Vercel, Netlify, Docker secrets).
8. Handling Special Cases
- Case Insensitivity: Handled by
.toUpperCase()
forpayload
in the webhook. - Number Formatting:
- Inconsistency: MessageBird might send
originator
without+
, while the SDK might prefer+
forrecipients
. - Standardization: The code now attempts to standardize storage to E.164 (with
+
) by checking/adding the prefix in the webhook (Section 2.3). When sending (Section 3.2), it uses the stored format. Always verify the exact format requirements of the MessageBird SDK'smessages.create
function.
- Inconsistency: MessageBird might send
- Duplicate Subscriptions: Prevented by
@unique
onphoneNumber
. The code handles re-subscribe/re-stop attempts gracefully. - Message Length: Standard SMS messages are limited to 160 GSM-7 characters or 70 UCS-2 characters. Longer messages are split into multiple segments. Be mindful of this for cost and user experience. The MessageBird API handles segmentation.