Send MMS with Plivo, Next.js 14, and NextAuth: Complete Tutorial
Learn how to send MMS messages using Plivo API in Next.js 14 with NextAuth authentication. Step-by-step guide with TypeScript, secure API endpoints, and error handling.
Build a feature in your Next.js application that enables authenticated users to send Multimedia Messaging Service (MMS) messages using the Plivo Communications Platform.
You'll leverage NextAuth.js for robust user authentication, ensuring only logged-in users can trigger MMS sending via a secure API endpoint. This approach solves the common need to integrate communication features directly into web applications while maintaining security and leveraging a reliable third-party service for message delivery.
Goal: Create a secure, server-side API endpoint in Next.js protected by NextAuth. This endpoint will accept recipient details and a media URL, then use the Plivo Node.js SDK to send an MMS message. Build a simple frontend interface for authenticated users to utilize this functionality.
Technologies Used:
Next.js (v14+ with App Router): A React framework for building server-rendered and static web applications. Choose this for its robust features, performance optimizations, and integrated API routes.
NextAuth.js (v5+): A complete authentication solution for Next.js applications. Use this to handle user sessions, login providers, and route protection seamlessly.
Plivo: A cloud communications platform providing APIs for SMS, MMS, Voice, and more. Select this for reliable messaging services and a developer-friendly Node.js SDK.
Node.js (v18+): The JavaScript runtime environment.
TypeScript: For type safety and improved developer experience.
Tailwind CSS: For styling the simple user interface.
(Note: This guide targets Next.js 14+, NextAuth v5+, and Node.js v18+. While accurate at the time of writing, check the official documentation for these libraries to ensure compatibility with the latest versions or confirm these remain the target versions for your project.)
System Architecture:
(Note: A visual diagram illustrating the flow would typically be placed here. The flow is: User interacts with Next.js Frontend → Frontend sends data to Next.js Server → Server calls /api/send-mms → API Route verifies NextAuth session → If valid, initializes Plivo Client → Sends MMS via Plivo API → Plivo API delivers to Recipient. Responses flow back accordingly.)
Understand React, Next.js, and asynchronous JavaScript basics.
Purchase or verify an MMS-enabled phone number within your Plivo account (for US/Canada destinations).
Obtain a publicly accessible URL for the media file you want to send (e.g., hosted on S3, Cloudinary, or a public server). Plivo also allows uploading media directly via their console or API.
MMS Geographic Limitations:
US and Canada Only: Plivo supports sending and receiving MMS messages exclusively within the United States and Canada (Plivo MMS Support). International MMS messaging outside these two countries is not supported as of November 2024.
Number Requirements: You must use an MMS-enabled Plivo long-code phone number. Only US and Canadian local long-code numbers support MMS functionality.
Carrier Support: MMS is supported by major mobile carriers in the US and Canada. Plivo phone numbers cannot receive MMS from short code numbers.
Final Outcome:
By the end of this guide, you will have:
A Next.js application with basic email/password authentication using NextAuth.js.
A secure API endpoint (/api/send-mms) that uses the Plivo SDK to send MMS messages.
A protected frontend page where authenticated users can input a recipient number, message text, and media URL to send an MMS.
Proper environment variable management for API keys and secrets.
Basic error handling for the MMS sending process.
TypeScript type augmentation for custom NextAuth session properties.
1. Set Up Your Project
Initialize your Next.js project and install the necessary dependencies.
Create Next.js App:
Open your terminal and run this command to create a new Next.js project using the App Router and TypeScript:
Follow the prompts and choose your preferred settings.
Navigate to Project Directory:
Bash
cd nextjs-plivo-mms
Install Dependencies:
Install next-auth for authentication and plivo for the Plivo API interaction. Add bcrypt for password hashing if using the Credentials provider and its types.
(Alternatively, use yarn add next-auth plivo bcrypt and yarn add -D @types/bcrypt)
Initialize Prisma (Optional but Recommended for User Management):
While not strictly required just to send an MMS via Plivo triggered by an authenticated user, integrate a database to manage user accounts if you use the Credentials provider or want to store user-specific data. Use Prisma with PostgreSQL as an example.
This creates a prisma directory with a schema.prisma file and a .env file for the database connection string.
Alternative Database Options: Besides PostgreSQL, Prisma supports MySQL, SQLite, MongoDB, SQL Server, and CockroachDB. For serverless deployments, consider Neon, PlanetScale, or Supabase PostgreSQL with connection pooling.
Configure Environment Variables:
NextAuth requires a secret key, and Plivo needs API credentials. Create a .env.local file in your project root. Never commit this file to version control. Add .env.local to your .gitignore file if it's not already there.
Generate NextAuth Secret:
Bash
openssl rand -base64 32
Copy the output.
Edit .env.local:
Text
# .env.local
# NextAuth Configuration
# Generate with: openssl rand -base64 32
NEXTAUTH_SECRET=YOUR_GENERATED_NEXTAUTH_SECRET
# Must be the canonical URL of your deployment. Crucial for redirects, callbacks, etc.
# For local development:
NEXTAUTH_URL=http://localhost:3000
# For production (replace with your actual domain):
# NEXTAUTH_URL=https://your-app-domain.com
# Plivo Credentials
# Find these in your Plivo Console: https://console.plivo.com/dashboard/
PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
# Plivo Phone Number
# An MMS-enabled number from your Plivo account (e.g., +14151234567)
# Must be in E.164 format (includes + and country code)
PLIVO_SENDER_NUMBER=YOUR_PLIVO_MMS_ENABLED_NUMBER
# Prisma Database Connection (if using Prisma)
# Example for local PostgreSQL: postgresql://user:password@localhost:5432/mydatabase
# Example for Neon/Supabase: Get from your provider dashboard
DATABASE_URL="YOUR_DATABASE_CONNECTION_STRING"
Find your AUTH ID and AUTH TOKEN displayed prominently on the main Dashboard page in the "Account Info" section. Click the eye icon to reveal the token.
Navigate to "Phone Numbers" → "Your Numbers". Ensure you have a number listed with MMS capability enabled (check the "Capabilities" column). If not, go to "Buy Numbers" to purchase one (filter by MMS capability for US/Canada). Copy the full number in E.164 format (e.g., +12025550111) into PLIVO_SENDER_NUMBER.
Plivo Pricing and Trial Accounts:
Trial Credits: New Plivo accounts receive trial credits (typically $25 USD) for testing purposes. Trial accounts can only send messages to verified phone numbers that you manually add in the Plivo Console under "Sandbox Numbers" or "Phone Numbers" → "Sandbox".
US MMS Pricing: MMS messages within the US typically cost $0.01-$0.015 per message segment (as of 2024). Phone number rental for US local long-code MMS-enabled numbers costs approximately $0.50/month (Plivo US Pricing).
Production Use: For production deployment, you'll need to add funds to your account and complete any required compliance registrations (such as 10DLC for US A2P messaging).
Configure Prisma Schema (if using Prisma):
Update prisma/schema.prisma to include the necessary models for NextAuth integration.
Prisma
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
password String? // Added for Credentials provider
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
Apply Prisma Migrations (if using Prisma):
Run the migration command to create database tables based on your schema.
Create NextAuth Type Definitions:
TypeScript module augmentation is required to add custom properties to NextAuth session and token objects. Create a type definition file:
TypeScript
// src/types/next-auth.d.tsimportNextAuth, { DefaultSession } from"next-auth";
import { JWT } from"next-auth/jwt";
declaremodule"next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/interfaceSession {
user: {
/** The user's unique ID from the database */id: string;
} & DefaultSession["user"]
}
interfaceUser {
id: string;
email?: string | null;
name?: string | null;
image?: string | null;
}
}
declaremodule"next-auth/jwt" {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */interface JWT {
/** The user's unique ID */id: string;
email?: string | null;
}
}
Ensure your tsconfig.json includes this file in its typeRoots or include paths. The Next.js App Router typically auto-discovers .d.ts files in the src directory.
Create NextAuth Options:
It's good practice to define auth options separately.
TypeScript
// src/lib/authOptions.tsimport { AuthOptions } from'next-auth';
importCredentialsProviderfrom'next-auth/providers/credentials';
import { PrismaAdapter } from'@next-auth/prisma-adapter';
import { prisma } from'@/lib/prisma'; // Adjust path if neededimport bcrypt from'bcrypt';
exportconstauthOptions: AuthOptions = {
// Use Prisma Adapter to store users, sessions, etc. in the DBadapter: PrismaAdapter(prisma),
providers: [
// Example: Credentials Provider (Email/Password)CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email', placeholder: 'user@example.com' },
password: { label: 'Password', type: 'password' },
},
asyncauthorize(credentials) {
if (!credentials?.email || !credentials?.password) {
console.error('Missing credentials');
returnnull;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.password) {
console.error('No user found or password not set for:', credentials.email);
returnnull; // Don't reveal if user exists
}
const isValidPassword = await bcrypt.compare(
credentials.password,
user.password
);
if (!isValidPassword) {
console.error('Invalid password for:', credentials.email);
returnnull;
}
console.log('User authorized:', user.email);
// Return user object without passwordreturn { id: user.id, name: user.name, email: user.email, image: user.image };
},
}),
// Add other providers like Google, GitHub, etc. here// Example: GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! })
],
// Use JWT strategy for session managementsession: {
strategy: 'jwt', // Use JWTs for session tokens
},
// Define custom pages for sign-in, sign-out, error, etc.pages: {
signIn: '/login', // Redirect users to /login page// error: '/auth/error', // Optional: Custom error page// verifyRequest: '/auth/verify-request', // Optional: For email provider
},
// Callbacks to control JWT and session contentcallbacks: {
asyncjwt({ token, user }) {
// Persist user ID and email into the JWT token after sign inif (user) {
token.id = user.id;
token.email = user.email; // Ensure email is in the token
}
return token;
},
asyncsession({ session, token }) {
// Add user ID and email from JWT token to the session objectif (token && session.user) {
session.user.id = token.idasstring;
session.user.email = token.emailasstring; // Ensure email is in the session
}
return session;
},
},
// Enable debug messages in developmentdebug: process.env.NODE_ENV === 'development',
secret: process.env.NEXTAUTH_SECRET, // Ensure this is set in .env.local
};
Prisma Adapter: Persists user data, essential for Credentials provider and managing user accounts long-term.
Credentials Provider: Allows traditional email/password login managed by your application. Requires password hashing (bcrypt).
JWT Strategy: Stateless session management using JSON Web Tokens stored in cookies. Suitable for serverless environments. Security Note: JWTs are not invalidatable server-side until expiry; for applications requiring immediate session revocation, consider database sessions (strategy: 'database').
Callbacks (jwt, session): Customize the token and session objects to include necessary user information (like user ID) accessible in your application.
Pages: Provides a better user experience by directing users to custom login pages instead of the default NextAuth pages.
Set up Session Provider:
Wrap your application layout with the NextAuth SessionProvider to make session data available globally via hooks like useSession.
Note on User Creation for Testing: Since we're using the CredentialsProvider, you need a user record in your database to log in. A production application would have a separate sign-up page and API route. For testing this guide, you can manually create a user:
Ensure your database is running and accessible based on DATABASE_URL in .env.local.
Run npx prisma studio in your terminal. This opens a web interface to your database.
Navigate to the User model.
Click "Add record".
Enter an email address (e.g., test@example.com).
For the password field, you need a bcrypt hash of the password you want to use. You cannot enter the plain password directly. Generate a hash using an online bcrypt generator or a simple Node.js script:
JavaScript
// Create a temporary file, e.g., temp_hash.jsconst bcrypt = require('bcrypt');
const saltRounds = 10;
const plainPassword = 'your_test_password'; // Replace with your desired password
bcrypt.hash(plainPassword, saltRounds, function(err, hash) {
if (err) {
console.error("Hashing error:", err);
return;
}
console.log('Copy this Hashed Password:', hash);
});
Run this script using node temp_hash.js. Copy the entire output hash string (it will look something like $2b$10$...).
Paste the copied hash into the password field in Prisma Studio.
Click "Save 1 record".
You can now use the email and the plain password (e.g., your_test_password) on the login page.
Production Sign-up Implementation: For production, create a sign-up API route (/api/auth/signup) that accepts email and password, validates input, hashes the password with bcrypt (minimum 10 salt rounds), creates the user in the database, and handles duplicate email errors appropriately.
3. Build the MMS Sending API Layer
Create the core endpoint that interacts with Plivo.
Create the API Route:
TypeScript
// src/app/api/send-mms/route.tsimport { NextRequest, NextResponse } from'next/server';
import { getServerSession } from'next-auth/next';
import { authOptions } from'@/lib/authOptions'; // Adjust pathimportPlivofrom'plivo'; // Use default import// E.164 phone number validation regexconstE164_REGEX = /^\+[1-9]\d{1,14}$/;
exportasyncfunctionPOST(req: NextRequest) {
// 1. Authentication Checkconst session = awaitgetServerSession(authOptions);
if (!session || !session.user) {
console.warn('Unauthorized attempt to send MMS');
returnNextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}
// 2. Input Validationlet body;
try {
body = await req.json();
} catch (error) {
console.error('Failed to parse request body:', error);
returnNextResponse.json({ success: false, error: 'Invalid request body' }, { status: 400 });
}
const { to, text, mediaUrl } = body;
if (!to || typeof to !== 'string' || !text || typeof text !== 'string' || !mediaUrl || typeof mediaUrl !== 'string') {
console.warn('Invalid input for send-mms:', { to, text, mediaUrl });
returnNextResponse.json({ success: false, error: 'Missing or invalid parameters: "to", "text", and "mediaUrl" are required strings.' }, { status: 400 });
}
// Validate phone number format (E.164)if (!E164_REGEX.test(to)) {
console.warn('Invalid phone number format:', to);
returnNextResponse.json({
success: false,
error: 'Invalid phone number format. Must be in E.164 format (e.g., +14155551234).'
}, { status: 400 });
}
// Validate text length (max 1,600 characters for MMS)if (text.length > 1600) {
console.warn('Text exceeds maximum length:', text.length);
returnNextResponse.json({
success: false,
error: 'Message text exceeds maximum length of 1,600 characters.'
}, { status: 400 });
}
// Basic validation for media URL formattry {
const url = newURL(mediaUrl);
// Ensure URL is HTTP/HTTPSif (url.protocol !== 'http:' && url.protocol !== 'https:') {
thrownewError('Invalid protocol');
}
} catch (error) {
console.warn('Invalid media URL format:', mediaUrl);
returnNextResponse.json({ success: false, error: 'Invalid media URL format. Must be a valid HTTP/HTTPS URL.' }, { status: 400 });
}
// 3. Plivo Client Initialization & API Callconst authId = process.env.PLIVO_AUTH_ID;
const authToken = process.env.PLIVO_AUTH_TOKEN;
const senderNumber = process.env.PLIVO_SENDER_NUMBER;
if (!authId || !authToken || !senderNumber) {
console.error('Plivo environment variables not configured');
returnNextResponse.json({ success: false, error: 'Server configuration error.' }, { status: 500 });
}
const client = newPlivo.Client(authId, authToken);
try {
console.log(`Attempting to send MMS from ${senderNumber} to ${to} by user ${session.user.email}`);
const response = await client.messages.create({
src: senderNumber, // Your Plivo MMS-enabled numberdst: to, // Recipient numbertext: text, // Message bodytype: 'mms', // Specify message type as MMSmedia_urls: [mediaUrl], // Array of public media URLs// media_ids: [] // Alternatively use media IDs if uploaded to Plivo
});
console.log('Plivo API Response:', response);
// Check Plivo response structure for success indication.// Plivo typically includes message_uuid (as an array) on success.if (response && response.messageUuid && Array.isArray(response.messageUuid) && response.messageUuid.length > 0) {
returnNextResponse.json({ success: true, message: 'MMS sent successfully!', plivoResponse: response });
} else {
// Log unexpected Plivo response structureconsole.error('Unexpected Plivo response structure:', response);
returnNextResponse.json({ success: false, error: 'Failed to send MMS due to unexpected Plivo response.', plivoResponse: response }, { status: 500 });
}
} catch (error: any) {
console.error(`Failed to send MMS via Plivo to ${to}:`, error);
// Provide more specific error if possible, otherwise genericconst errorMessage = error.message || 'An unknown error occurred while sending the MMS.';
returnNextResponse.json({ success: false, error: `Plivo API Error: ${errorMessage}`, details: error }, { status: 500 });
}
}
API Endpoint Documentation:
Endpoint:POST /api/send-mms
Authentication: Requires active NextAuth session (JWT cookie).
Request Body (JSON):
JSON
{"to":"+15551234567","text":"Check out this image!","mediaUrl":"https://your-public-domain.com/image.jpg"}
Success Response (200 OK):
JSON
{"success":true,"message":"MMS sent successfully!","plivoResponse":{"message":"message(s) queued","messageUuid":["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],"apiId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}}
Error Responses:
400 Bad Request: Invalid input parameters or JSON body.
JSON
{"success":false,"error":"Missing or invalid parameters..."}
401 Unauthorized: No active session or invalid session.
JSON
{"success":false,"error":"Unauthorized"}
500 Internal Server Error: Plivo configuration error or Plivo API failure.
{"success":false,"error":"Plivo API Error: [Specific error from Plivo]","details":{ ... }}
Testing with curl:
First, log in through the web interface to get a valid session cookie. Then, use your browser's developer tools (Network tab) to find the next-auth.session-token cookie value after logging in.
Bash
# Replace YOUR_COOKIE_VALUE and other placeholders
curl -X POST http://localhost:3000/api/send-mms \
-H "Content-Type: application/json" \
-H "Cookie: next-auth.session-token=YOUR_COOKIE_VALUE" \
-d '{
"to": "+1TARGETPHONE",
"text": "Test MMS via curl!",
"mediaUrl": "https://media.giphy.com/media/26gscSULUcfKU7dHq/source.gif"
}'
(Remember Plivo trial accounts can only send to verified numbers added in the Plivo console under "Sandbox Numbers")
4. Integrate with Plivo (Recap & Details)
You've already used the Plivo SDK in the API route. Here's a recap of the integration points with additional details:
Credentials: Store PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN securely in .env.local and access them via process.env. Obtain them directly from the Plivo Console Dashboard.
Sender Number: Store PLIVO_SENDER_NUMBER in .env.local. This must be an MMS-enabled number from your Plivo account, found under "Phone Numbers" → "Your Numbers". Use E.164 format (e.g., +14151234567).
SDK Usage: Install the plivo Node.js SDK (npm install plivo) and use it within the API route (/api/send-mms/route.ts) to instantiate the client (new Plivo.Client(...)) and send the message (client.messages.create(...)).
Media URLs: The media_urls parameter in client.messages.create expects an array of strings, where each string is a publicly accessible URL to a media file. Ensure the hosting server allows access from Plivo's servers. Alternatively, upload media to Plivo via their console ("Messaging" → "MMS Media Upload") or API and use the resulting media_id in the media_ids parameter.
Images: JPEG, PNG, GIF (optimized for device compatibility automatically)
Other Media: Audio, video, and vCard files are supported but not optimized for device compatibility
Size Limits:
Maximum 10 media attachments per MMS message
Total size of message body text (max 1,600 characters = ~4.8KB) + all media files must be under 5MB
Messages exceeding 5MB fail with Plivo error code 120
For non-image files (audio/video), 600KB per file is recommended for best carrier compatibility
Media Hosting Requirements:
Media URLs must be publicly accessible via HTTP or HTTPS
The hosting server must respond to Plivo's requests (check firewall/access controls)
CORS configuration is not required for media hosting since Plivo fetches media server-side
Use reliable CDN hosting (S3, Cloudinary, Imgur) for production to ensure availability during message delivery
5. Implement Error Handling and Logging
Your API route includes basic error handling with the following improvements:
Authentication Errors: Check for a valid session using getServerSession and return a 401 if none exists.
Input Validation Errors: Check for the presence and basic type of to, text, and mediaUrl, returning a 400 if invalid. Includes E.164 phone number format validation, text length validation, and URL format validation.
Configuration Errors: Check if Plivo environment variables are set, returning a 500 if not.
Plivo API Errors: Wrap the client.messages.create call in a try...catch block. Log the error to the console (console.error) and return a 500 response containing the error message from Plivo.
Logging: Use console.log, console.warn, and console.error for basic logging on the server side. In production, integrate a dedicated logging library (like Pino or Winston) and forward logs to a centralized logging service (like Datadog, Logtail, Sentry).
Unauthorized: Make a curl request without the Cookie header.
Bad Request: Send a curl request with missing fields (e.g., no mediaUrl) or malformed JSON. Send an invalid URL format. Send an invalid phone number format (e.g., missing country code).
Plivo Error: Temporarily change PLIVO_AUTH_TOKEN in .env.local to an invalid value and restart the server, then try sending. Send to an invalid/non-existent phone number format. Use an invalid/inaccessible mediaUrl.
6. Review Database Schema and Data Layer
You set up Prisma earlier for user authentication data persistence via the PrismaAdapter.
Schema: The prisma/schema.prisma file defines the User, Account, Session, and VerificationToken models required by next-auth/prisma-adapter. You added an optional password field to the User model for the Credentials provider.
Data Access: Prisma Client (@prisma/client) is used implicitly by the PrismaAdapter to read/write user and session data. The authorize function within the Credentials provider configuration explicitly uses prisma.user.findUnique to look up users by email.
Migrations: Use npx prisma migrate dev to manage database schema changes based on the schema.prisma file.
7. Build the Frontend MMS Sending Page
Create a protected page where authenticated users can send MMS messages.
Cause: Session cookie not being sent or invalid session
Solution: Ensure you're logged in. Check browser developer tools → Application → Cookies for next-auth.session-token. Clear cookies and log in again if needed.
"Invalid phone number format" Error:
Cause: Phone number not in E.164 format
Solution: Ensure the number includes + and country code (e.g., +14155551234 for US numbers)
Plivo Error Code 40 (Invalid Source Number):
Cause:PLIVO_SENDER_NUMBER is incorrect or doesn't have MMS capability
Solution: Verify the number in Plivo Console → Phone Numbers → Your Numbers. Ensure it has MMS capability enabled.
Plivo Error Code 120 (Payload Too Large):
Cause: Total MMS size exceeds 5MB
Solution: Reduce image file size or use fewer/smaller attachments
Plivo Error Code 140 (Media Processing Failed):
Cause: Media URL is not publicly accessible or returns an error
Solution: Test the media URL in a browser. Ensure it returns the image with proper MIME type headers. Check hosting service access permissions.
Messages Only Send to One Number (Trial Account):
Cause: Trial accounts can only send to verified sandbox numbers
Solution: Add recipient numbers in Plivo Console → Sandbox Numbers, or upgrade to a paid account
TypeScript Errors with Session User ID:
Cause: Missing type augmentation for NextAuth
Solution: Ensure src/types/next-auth.d.ts exists with proper module augmentation (see Section 2)
Testing Checklist:
User can log in with valid credentials
User cannot access /send-mms without authentication
MMS sends successfully with valid inputs
Proper error shown for invalid phone number format
Proper error shown for invalid media URL
Proper error shown when message text exceeds 1,600 characters
Loading state shows during message sending
Success message displays after successful send
User can send multiple messages in succession
9. Deployment Best Practices
Environment Variables:
Set all environment variables (NEXTAUTH_SECRET, NEXTAUTH_URL, PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SENDER_NUMBER, DATABASE_URL) in your deployment platform (Vercel, Railway, AWS, etc.)
Update NEXTAUTH_URL to your production domain (e.g., https://yourdomain.com)
Use your platform's secrets management for sensitive values
Database Considerations:
For serverless deployments (Vercel, Netlify), use a serverless-compatible database (Neon, PlanetScale, Supabase)
Enable connection pooling to prevent exhausting database connections
Run npx prisma generate during build process
Run npx prisma migrate deploy for production migrations (not migrate dev)
Security Hardening:
Enable HTTPS only (enforce in Next.js middleware or at CDN level)
Implement rate limiting on /api/send-mms to prevent abuse (use libraries like @upstash/ratelimit or middleware)
Add CSRF protection if not using default NextAuth cookies
Monitor failed login attempts and implement account lockout
Regularly rotate NEXTAUTH_SECRET and Plivo credentials
Set appropriate CORS headers if building a separate frontend
Monitoring:
Integrate application monitoring (Sentry, LogRocket) for error tracking
Set up logging aggregation (Datadog, Logtail) for production logs
Monitor Plivo usage and costs in the Plivo Console
Set up alerts for API errors, authentication failures, and failed message sends
This completes your MMS sending application with Plivo, Next.js, and NextAuth. You now have a secure, production-ready foundation for sending multimedia messages from your web application.
Frequently asked questions
You can send MMS messages by creating a secure API endpoint in your Next.js application using the Plivo Node.js SDK. This endpoint, protected by NextAuth.js, handles the interaction with the Plivo API to send multimedia messages after verifying user authentication and input validation. A simple frontend interface allows users to input recipient details, text, and media URL.
NextAuth.js is used for user authentication. It ensures only logged-in users can access and trigger the MMS sending functionality via the secure API endpoint. This is important for protecting your Plivo credentials and preventing unauthorized message sending.
Plivo is a cloud communications platform providing reliable messaging services. Its developer-friendly Node.js SDK simplifies the integration with your Next.js application, handling the complexities of sending MMS messages. Plivo offers various features and global reach for messaging.
The Plivo client should be initialized within the server-side API route handler that processes the MMS sending request. This ensures the client is created for each request and that API credentials are handled securely on the server.
You'll need your Plivo AUTH ID, AUTH TOKEN, and an MMS-enabled Plivo phone number. These are found in your Plivo Console dashboard. Store these securely as environment variables (PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SENDER_NUMBER) in a .env.local file, which should never be committed to version control.
Wrap the client.messages.create call in a try...catch block within your API route handler. Log the error using console.error and return a 500 response with a user-friendly error message. Include details about the error from Plivo for debugging when possible.
Use NextAuth's getServerSession function within the API route handler (/api/send-mms). This function checks for a valid NextAuth session based on the JWT cookie. Return a 401 Unauthorized response if no session exists.
Prisma is used for user data management, especially when using authentication providers like Credentials. It stores user data, sessions, accounts, and other authentication-related information. Prisma isn't directly involved in the MMS sending process via Plivo but manages users for NextAuth.
The request body should be JSON with three fields: to (recipient's phone number in E.164 format), text (the message body), and mediaUrl (a publicly accessible URL of the media file).
You need Node.js v18+, npm or yarn, a Plivo account with an MMS-enabled number, basic understanding of React/Next.js, and a publicly accessible media URL. You will also need the Plivo Node.js SDK.
Log in to your Plivo console and navigate to "Phone Numbers" -> "Your Numbers" to check your existing numbers. If you don't have an MMS-enabled number, buy one by going to "Buy Numbers" and filtering the search options by MMS capability.
Plivo supports common image formats like JPEG, PNG, and GIF for MMS messages. Ensure the URL you provide in the mediaUrl parameter points to a publicly accessible file of a supported format. Plivo allows media upload through their console or API.
Create a NextAuth API route handler (/api/auth/[...nextauth]/route.ts) with a configuration file specifying providers, session strategy, and database adapter if needed. Use getServerSession in the MMS API route to check for an active session.
Yes, you can test sending MMS during development using tools like curl to send requests to the POST /api/send-mms endpoint. Ensure Plivo is configured to send to sandbox or test numbers.
Build a feature in your Next.js application that enables authenticated users to send Multimedia Messaging Service (MMS) messages using the Plivo Communications Platform.
You'll leverage NextAuth.js for robust user authentication, ensuring only logged-in users can trigger MMS sending via a secure API endpoint. This approach solves the common need to integrate communication features directly into web applications while maintaining security and leveraging a reliable third-party service for message delivery.
Goal: Create a secure, server-side API endpoint in Next.js protected by NextAuth. This endpoint will accept recipient details and a media URL, then use the Plivo Node.js SDK to send an MMS message. Build a simple frontend interface for authenticated users to utilize this functionality.
Technologies Used:
(Note: This guide targets Next.js 14+, NextAuth v5+, and Node.js v18+. While accurate at the time of writing, check the official documentation for these libraries to ensure compatibility with the latest versions or confirm these remain the target versions for your project.)
System Architecture:
(Note: A visual diagram illustrating the flow would typically be placed here. The flow is: User interacts with Next.js Frontend → Frontend sends data to Next.js Server → Server calls
/api/send-mms→ API Route verifies NextAuth session → If valid, initializes Plivo Client → Sends MMS via Plivo API → Plivo API delivers to Recipient. Responses flow back accordingly.)Prerequisites:
MMS Geographic Limitations:
Final Outcome:
By the end of this guide, you will have:
/api/send-mms) that uses the Plivo SDK to send MMS messages.1. Set Up Your Project
Initialize your Next.js project and install the necessary dependencies.
Create Next.js App: Open your terminal and run this command to create a new Next.js project using the App Router and TypeScript:
npx create-next-app@latest nextjs-plivo-mms --typescript --tailwind --eslint --app --src-dir --import-alias '@/*'Follow the prompts and choose your preferred settings.
Navigate to Project Directory:
cd nextjs-plivo-mmsInstall Dependencies: Install
next-authfor authentication andplivofor the Plivo API interaction. Addbcryptfor password hashing if using the Credentials provider and its types.(Alternatively, use
yarn add next-auth plivo bcryptandyarn add -D @types/bcrypt)Initialize Prisma (Optional but Recommended for User Management): While not strictly required just to send an MMS via Plivo triggered by an authenticated user, integrate a database to manage user accounts if you use the Credentials provider or want to store user-specific data. Use Prisma with PostgreSQL as an example.
This creates a
prismadirectory with aschema.prismafile and a.envfile for the database connection string.Alternative Database Options: Besides PostgreSQL, Prisma supports MySQL, SQLite, MongoDB, SQL Server, and CockroachDB. For serverless deployments, consider Neon, PlanetScale, or Supabase PostgreSQL with connection pooling.
Configure Environment Variables: NextAuth requires a secret key, and Plivo needs API credentials. Create a
.env.localfile in your project root. Never commit this file to version control. Add.env.localto your.gitignorefile if it's not already there.Generate NextAuth Secret:
openssl rand -base64 32Copy the output.
Edit
.env.local:Get Plivo Credentials:
AUTH IDandAUTH TOKENdisplayed prominently on the main Dashboard page in the "Account Info" section. Click the eye icon to reveal the token.+12025550111) intoPLIVO_SENDER_NUMBER.Plivo Pricing and Trial Accounts:
Configure Prisma Schema (if using Prisma): Update
prisma/schema.prismato include the necessary models for NextAuth integration.Apply Prisma Migrations (if using Prisma): Run the migration command to create database tables based on your schema.
This also generates the Prisma Client.
2. Implement Core Functionality (NextAuth & Plivo Client)
Set up NextAuth for authentication and configure the Plivo client.
Configure NextAuth: Create the NextAuth API route handler.
Create Prisma Client Instance (if using Prisma): Create a utility file to instantiate Prisma Client, preventing multiple instances in development.
// src/lib/prisma.ts import { PrismaClient } from '@prisma/client'; declare global { // allow global `var` declarations // eslint-disable-next-line no-var var prisma: PrismaClient | undefined; } export const prisma = global.prisma || new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }); if (process.env.NODE_ENV !== 'production') global.prisma = prisma;Create NextAuth Type Definitions: TypeScript module augmentation is required to add custom properties to NextAuth session and token objects. Create a type definition file:
// src/types/next-auth.d.ts import NextAuth, { DefaultSession } from "next-auth"; import { JWT } from "next-auth/jwt"; declare module "next-auth" { /** * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context */ interface Session { user: { /** The user's unique ID from the database */ id: string; } & DefaultSession["user"] } interface User { id: string; email?: string | null; name?: string | null; image?: string | null; } } declare module "next-auth/jwt" { /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */ interface JWT { /** The user's unique ID */ id: string; email?: string | null; } }Ensure your
tsconfig.jsonincludes this file in itstypeRootsor include paths. The Next.js App Router typically auto-discovers.d.tsfiles in thesrcdirectory.Create NextAuth Options: It's good practice to define auth options separately.
// src/lib/authOptions.ts import { AuthOptions } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import { PrismaAdapter } from '@next-auth/prisma-adapter'; import { prisma } from '@/lib/prisma'; // Adjust path if needed import bcrypt from 'bcrypt'; export const authOptions: AuthOptions = { // Use Prisma Adapter to store users, sessions, etc. in the DB adapter: PrismaAdapter(prisma), providers: [ // Example: Credentials Provider (Email/Password) CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email', placeholder: 'user@example.com' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { console.error('Missing credentials'); return null; } const user = await prisma.user.findUnique({ where: { email: credentials.email }, }); if (!user || !user.password) { console.error('No user found or password not set for:', credentials.email); return null; // Don't reveal if user exists } const isValidPassword = await bcrypt.compare( credentials.password, user.password ); if (!isValidPassword) { console.error('Invalid password for:', credentials.email); return null; } console.log('User authorized:', user.email); // Return user object without password return { id: user.id, name: user.name, email: user.email, image: user.image }; }, }), // Add other providers like Google, GitHub, etc. here // Example: GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }) ], // Use JWT strategy for session management session: { strategy: 'jwt', // Use JWTs for session tokens }, // Define custom pages for sign-in, sign-out, error, etc. pages: { signIn: '/login', // Redirect users to /login page // error: '/auth/error', // Optional: Custom error page // verifyRequest: '/auth/verify-request', // Optional: For email provider }, // Callbacks to control JWT and session content callbacks: { async jwt({ token, user }) { // Persist user ID and email into the JWT token after sign in if (user) { token.id = user.id; token.email = user.email; // Ensure email is in the token } return token; }, async session({ session, token }) { // Add user ID and email from JWT token to the session object if (token && session.user) { session.user.id = token.id as string; session.user.email = token.email as string; // Ensure email is in the session } return session; }, }, // Enable debug messages in development debug: process.env.NODE_ENV === 'development', secret: process.env.NEXTAUTH_SECRET, // Ensure this is set in .env.local };Create API Route Handler:
// src/app/api/auth/[...nextauth]/route.ts import NextAuth from 'next-auth'; import { authOptions } from '@/lib/authOptions'; // Adjust path if needed const handler = NextAuth(authOptions); export { handler as GET, handler as POST };Why these choices?
strategy: 'database').jwt,session): Customize the token and session objects to include necessary user information (like user ID) accessible in your application.Set up Session Provider: Wrap your application layout with the NextAuth
SessionProviderto make session data available globally via hooks likeuseSession.// src/app/layout.tsx import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; import AuthProvider from '@/components/AuthProvider'; // We'll create this next const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: 'Next.js Plivo MMS Sender', description: 'Send MMS securely with NextAuth and Plivo', }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={inter.className}> <AuthProvider>{children}</AuthProvider> {/* Wrap with AuthProvider */} </body> </html> ); }Create the
AuthProvidercomponent:// src/components/AuthProvider.tsx 'use client'; // This component uses client-side context import { SessionProvider } from 'next-auth/react'; import React from 'react'; interface Props { children: React.ReactNode; } export default function AuthProvider({ children }: Props) { return <SessionProvider>{children}</SessionProvider>; }Create Login Page: A simple login page using the Credentials provider.
// src/app/login/page.tsx 'use client'; import { useState, FormEvent } from 'react'; import { signIn } from 'next-auth/react'; import { useRouter } from 'next/navigation'; // Use 'next/navigation' in App Router export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); // Add simple client-side validation if (!email || !password) { setError('Email and password are required.'); setIsLoading(false); return; } try { const result = await signIn('credentials', { redirect: false, // Prevent NextAuth from redirecting automatically email, password, }); if (result?.error) { console.error('Sign in error:', result.error); setError('Invalid credentials. Please try again.'); // Provide generic error setIsLoading(false); } else if (result?.ok) { // Sign-in successful, redirect to the MMS sending page or dashboard router.push('/send-mms'); // Or '/dashboard' or wherever appropriate } else { setError('An unexpected error occurred during login.'); setIsLoading(false); } } catch (err) { console.error('Login submission error:', err); setError('An unexpected error occurred. Please try again later.'); setIsLoading(false); } }; return ( <div className="min-h-screen flex items-center justify-center bg-gray-100"> <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md"> <h1 className="text-2xl font-bold mb-6 text-center">Login</h1> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="email" className="block text-sm font-medium text-gray-700" > Email </label> <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black" /> </div> <div> <label htmlFor="password" className="block text-sm font-medium text-gray-700" > Password </label> <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black" /> </div> {error && ( <p className="text-red-500 text-sm text-center">{error}</p> )} <button type="submit" disabled={isLoading} className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50" > {isLoading ? 'Logging in...' : 'Login'} </button> </form> <p className="mt-4 text-xs text-center text-gray-500"> Login requires an existing user account. See note below on creating a test user. </p> </div> </div> ); }Note on User Creation for Testing: Since we're using the
CredentialsProvider, you need a user record in your database to log in. A production application would have a separate sign-up page and API route. For testing this guide, you can manually create a user:DATABASE_URLin.env.local.npx prisma studioin your terminal. This opens a web interface to your database.Usermodel.test@example.com).passwordfield, you need a bcrypt hash of the password you want to use. You cannot enter the plain password directly. Generate a hash using an online bcrypt generator or a simple Node.js script:// Create a temporary file, e.g., temp_hash.js const bcrypt = require('bcrypt'); const saltRounds = 10; const plainPassword = 'your_test_password'; // Replace with your desired password bcrypt.hash(plainPassword, saltRounds, function(err, hash) { if (err) { console.error("Hashing error:", err); return; } console.log('Copy this Hashed Password:', hash); });node temp_hash.js. Copy the entire output hash string (it will look something like$2b$10$...).passwordfield in Prisma Studio.your_test_password) on the login page.Production Sign-up Implementation: For production, create a sign-up API route (
/api/auth/signup) that accepts email and password, validates input, hashes the password with bcrypt (minimum 10 salt rounds), creates the user in the database, and handles duplicate email errors appropriately.3. Build the MMS Sending API Layer
Create the core endpoint that interacts with Plivo.
Create the API Route:
// src/app/api/send-mms/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/lib/authOptions'; // Adjust path import Plivo from 'plivo'; // Use default import // E.164 phone number validation regex const E164_REGEX = /^\+[1-9]\d{1,14}$/; export async function POST(req: NextRequest) { // 1. Authentication Check const session = await getServerSession(authOptions); if (!session || !session.user) { console.warn('Unauthorized attempt to send MMS'); return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }); } // 2. Input Validation let body; try { body = await req.json(); } catch (error) { console.error('Failed to parse request body:', error); return NextResponse.json({ success: false, error: 'Invalid request body' }, { status: 400 }); } const { to, text, mediaUrl } = body; if (!to || typeof to !== 'string' || !text || typeof text !== 'string' || !mediaUrl || typeof mediaUrl !== 'string') { console.warn('Invalid input for send-mms:', { to, text, mediaUrl }); return NextResponse.json({ success: false, error: 'Missing or invalid parameters: "to", "text", and "mediaUrl" are required strings.' }, { status: 400 }); } // Validate phone number format (E.164) if (!E164_REGEX.test(to)) { console.warn('Invalid phone number format:', to); return NextResponse.json({ success: false, error: 'Invalid phone number format. Must be in E.164 format (e.g., +14155551234).' }, { status: 400 }); } // Validate text length (max 1,600 characters for MMS) if (text.length > 1600) { console.warn('Text exceeds maximum length:', text.length); return NextResponse.json({ success: false, error: 'Message text exceeds maximum length of 1,600 characters.' }, { status: 400 }); } // Basic validation for media URL format try { const url = new URL(mediaUrl); // Ensure URL is HTTP/HTTPS if (url.protocol !== 'http:' && url.protocol !== 'https:') { throw new Error('Invalid protocol'); } } catch (error) { console.warn('Invalid media URL format:', mediaUrl); return NextResponse.json({ success: false, error: 'Invalid media URL format. Must be a valid HTTP/HTTPS URL.' }, { status: 400 }); } // 3. Plivo Client Initialization & API Call const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; const senderNumber = process.env.PLIVO_SENDER_NUMBER; if (!authId || !authToken || !senderNumber) { console.error('Plivo environment variables not configured'); return NextResponse.json({ success: false, error: 'Server configuration error.' }, { status: 500 }); } const client = new Plivo.Client(authId, authToken); try { console.log(`Attempting to send MMS from ${senderNumber} to ${to} by user ${session.user.email}`); const response = await client.messages.create({ src: senderNumber, // Your Plivo MMS-enabled number dst: to, // Recipient number text: text, // Message body type: 'mms', // Specify message type as MMS media_urls: [mediaUrl], // Array of public media URLs // media_ids: [] // Alternatively use media IDs if uploaded to Plivo }); console.log('Plivo API Response:', response); // Check Plivo response structure for success indication. // Plivo typically includes message_uuid (as an array) on success. if (response && response.messageUuid && Array.isArray(response.messageUuid) && response.messageUuid.length > 0) { return NextResponse.json({ success: true, message: 'MMS sent successfully!', plivoResponse: response }); } else { // Log unexpected Plivo response structure console.error('Unexpected Plivo response structure:', response); return NextResponse.json({ success: false, error: 'Failed to send MMS due to unexpected Plivo response.', plivoResponse: response }, { status: 500 }); } } catch (error: any) { console.error(`Failed to send MMS via Plivo to ${to}:`, error); // Provide more specific error if possible, otherwise generic const errorMessage = error.message || 'An unknown error occurred while sending the MMS.'; return NextResponse.json({ success: false, error: `Plivo API Error: ${errorMessage}`, details: error }, { status: 500 }); } }API Endpoint Documentation:
POST /api/send-mms{ "to": "+15551234567", "text": "Check out this image!", "mediaUrl": "https://your-public-domain.com/image.jpg" }{ "success": true, "message": "MMS sent successfully!", "plivoResponse": { "message": "message(s) queued", "messageUuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], "apiId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }400 Bad Request: Invalid input parameters or JSON body.{ "success": false, "error": "Missing or invalid parameters..." }401 Unauthorized: No active session or invalid session.{ "success": false, "error": "Unauthorized" }500 Internal Server Error: Plivo configuration error or Plivo API failure.{ "success": false, "error": "Server configuration error." }{ "success": false, "error": "Plivo API Error: [Specific error from Plivo]", "details": { ... } }Testing with
curl: First, log in through the web interface to get a valid session cookie. Then, use your browser's developer tools (Network tab) to find thenext-auth.session-tokencookie value after logging in.# Replace YOUR_COOKIE_VALUE and other placeholders curl -X POST http://localhost:3000/api/send-mms \ -H "Content-Type: application/json" \ -H "Cookie: next-auth.session-token=YOUR_COOKIE_VALUE" \ -d '{ "to": "+1TARGETPHONE", "text": "Test MMS via curl!", "mediaUrl": "https://media.giphy.com/media/26gscSULUcfKU7dHq/source.gif" }'(Remember Plivo trial accounts can only send to verified numbers added in the Plivo console under "Sandbox Numbers")
4. Integrate with Plivo (Recap & Details)
You've already used the Plivo SDK in the API route. Here's a recap of the integration points with additional details:
PLIVO_AUTH_IDandPLIVO_AUTH_TOKENsecurely in.env.localand access them viaprocess.env. Obtain them directly from the Plivo Console Dashboard.PLIVO_SENDER_NUMBERin.env.local. This must be an MMS-enabled number from your Plivo account, found under "Phone Numbers" → "Your Numbers". Use E.164 format (e.g.,+14151234567).plivoNode.js SDK (npm install plivo) and use it within the API route (/api/send-mms/route.ts) to instantiate the client (new Plivo.Client(...)) and send the message (client.messages.create(...)).media_urlsparameter inclient.messages.createexpects an array of strings, where each string is a publicly accessible URL to a media file. Ensure the hosting server allows access from Plivo's servers. Alternatively, upload media to Plivo via their console ("Messaging" → "MMS Media Upload") or API and use the resultingmedia_idin themedia_idsparameter.MMS Media Specifications (Plivo MMS Documentation):
5. Implement Error Handling and Logging
Your API route includes basic error handling with the following improvements:
getServerSessionand return a 401 if none exists.to,text, andmediaUrl, returning a 400 if invalid. Includes E.164 phone number format validation, text length validation, and URL format validation.client.messages.createcall in atry...catchblock. Log the error to the console (console.error) and return a 500 response containing the error message from Plivo.console.log,console.warn, andconsole.errorfor basic logging on the server side. In production, integrate a dedicated logging library (like Pino or Winston) and forward logs to a centralized logging service (like Datadog, Logtail, Sentry).Common Plivo Error Codes (Plivo Error Codes Reference):
Structured Error Response Pattern:
// Example error response structure interface ErrorResponse { success: false; error: string; // Human-readable error message code?: string; // Application-specific error code plivoErrorCode?: number; // Plivo error code if applicable details?: any; // Additional error details for debugging }Test Error Scenarios:
curlrequest without theCookieheader.curlrequest with missing fields (e.g., nomediaUrl) or malformed JSON. Send an invalid URL format. Send an invalid phone number format (e.g., missing country code).PLIVO_AUTH_TOKENin.env.localto an invalid value and restart the server, then try sending. Send to an invalid/non-existent phone number format. Use an invalid/inaccessiblemediaUrl.6. Review Database Schema and Data Layer
You set up Prisma earlier for user authentication data persistence via the
PrismaAdapter.prisma/schema.prismafile defines theUser,Account,Session, andVerificationTokenmodels required bynext-auth/prisma-adapter. You added an optionalpasswordfield to theUsermodel for the Credentials provider.@prisma/client) is used implicitly by thePrismaAdapterto read/write user and session data. Theauthorizefunction within the Credentials provider configuration explicitly usesprisma.user.findUniqueto look up users by email.npx prisma migrate devto manage database schema changes based on theschema.prismafile.7. Build the Frontend MMS Sending Page
Create a protected page where authenticated users can send MMS messages.
Create the Send MMS Page:
// src/app/send-mms/page.tsx 'use client'; import { useState, FormEvent } from 'react'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; export default function SendMMSPage() { const { data: session, status } = useSession(); const router = useRouter(); const [to, setTo] = useState(''); const [text, setText] = useState(''); const [mediaUrl, setMediaUrl] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null); // Redirect to login if not authenticated if (status === 'loading') { return <div className="min-h-screen flex items-center justify-center">Loading...</div>; } if (status === 'unauthenticated') { router.push('/login'); return null; } const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setIsLoading(true); setError(null); setSuccess(null); try { const response = await fetch('/api/send-mms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ to, text, mediaUrl }), }); const data = await response.json(); if (response.ok && data.success) { setSuccess('MMS sent successfully!'); // Reset form setTo(''); setText(''); setMediaUrl(''); } else { setError(data.error || 'Failed to send MMS'); } } catch (err) { console.error('Error sending MMS:', err); setError('An unexpected error occurred. Please try again.'); } finally { setIsLoading(false); } }; return ( <div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-8"> <div className="mb-6"> <h1 className="text-2xl font-bold text-center">Send MMS</h1> <p className="text-sm text-gray-600 text-center mt-2"> Logged in as: {session?.user?.email} </p> </div> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="to" className="block text-sm font-medium text-gray-700"> Recipient Phone Number </label> <input type="tel" id="to" value={to} onChange={(e) => setTo(e.target.value)} placeholder="+15551234567" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border" /> <p className="mt-1 text-xs text-gray-500"> Must be in E.164 format (e.g., +14155551234) </p> </div> <div> <label htmlFor="text" className="block text-sm font-medium text-gray-700"> Message Text </label> <textarea id="text" value={text} onChange={(e) => setText(e.target.value)} placeholder="Your message here..." required rows={4} maxLength={1600} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border" /> <p className="mt-1 text-xs text-gray-500"> {text.length}/1600 characters </p> </div> <div> <label htmlFor="mediaUrl" className="block text-sm font-medium text-gray-700"> Media URL </label> <input type="url" id="mediaUrl" value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} placeholder="https://example.com/image.jpg" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 text-black border" /> <p className="mt-1 text-xs text-gray-500"> Publicly accessible URL (JPEG, PNG, GIF supported) </p> </div> {error && ( <div className="rounded-md bg-red-50 p-4"> <p className="text-sm text-red-800">{error}</p> </div> )} {success && ( <div className="rounded-md bg-green-50 p-4"> <p className="text-sm text-green-800">{success}</p> </div> )} <button type="submit" disabled={isLoading} className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed" > {isLoading ? 'Sending...' : 'Send MMS'} </button> </form> <div className="mt-6 text-center"> <button onClick={() => router.push('/')} className="text-sm text-indigo-600 hover:text-indigo-800" > Back to Home </button> </div> </div> </div> ); }Create a Simple Home Page:
// src/app/page.tsx 'use client'; import { useSession, signOut } from 'next-auth/react'; import { useRouter } from 'next/navigation'; export default function Home() { const { data: session, status } = useSession(); const router = useRouter(); return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full text-center"> <h1 className="text-3xl font-bold mb-6">Plivo MMS Sender</h1> {status === 'loading' && <p>Loading...</p>} {status === 'authenticated' && session && ( <div className="space-y-4"> <p className="text-gray-700"> Welcome, <span className="font-semibold">{session.user?.email}</span>! </p> <button onClick={() => router.push('/send-mms')} className="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition" > Send MMS </button> <button onClick={() => signOut()} className="w-full py-2 px-4 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition" > Sign Out </button> </div> )} {status === 'unauthenticated' && ( <div className="space-y-4"> <p className="text-gray-700 mb-4"> Please log in to send MMS messages. </p> <button onClick={() => router.push('/login')} className="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition" > Log In </button> </div> )} </div> </div> ); }8. Testing and Troubleshooting
Common Issues and Solutions:
"Unauthorized" Error When Sending MMS:
next-auth.session-token. Clear cookies and log in again if needed."Invalid phone number format" Error:
+and country code (e.g.,+14155551234for US numbers)Plivo Error Code 40 (Invalid Source Number):
PLIVO_SENDER_NUMBERis incorrect or doesn't have MMS capabilityPlivo Error Code 120 (Payload Too Large):
Plivo Error Code 140 (Media Processing Failed):
Messages Only Send to One Number (Trial Account):
TypeScript Errors with Session User ID:
src/types/next-auth.d.tsexists with proper module augmentation (see Section 2)Testing Checklist:
/send-mmswithout authentication9. Deployment Best Practices
Environment Variables:
NEXTAUTH_SECRET,NEXTAUTH_URL,PLIVO_AUTH_ID,PLIVO_AUTH_TOKEN,PLIVO_SENDER_NUMBER,DATABASE_URL) in your deployment platform (Vercel, Railway, AWS, etc.)NEXTAUTH_URLto your production domain (e.g.,https://yourdomain.com)Database Considerations:
npx prisma generateduring build processnpx prisma migrate deployfor production migrations (notmigrate dev)Security Hardening:
/api/send-mmsto prevent abuse (use libraries like@upstash/ratelimitor middleware)NEXTAUTH_SECRETand Plivo credentialsMonitoring:
This completes your MMS sending application with Plivo, Next.js, and NextAuth. You now have a secure, production-ready foundation for sending multimedia messages from your web application.