code examples
code examples
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.
Send MMS with Plivo, Next.js, and NextAuth
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.)
Prerequisites:
- Install Node.js (v18 or later).
- Install npm or yarn package manager.
- Create a Plivo account (Sign up here).
- 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:
bashnpx 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:
bashcd nextjs-plivo-mms -
Install Dependencies: Install
next-authfor authentication andplivofor the Plivo API interaction. Addbcryptfor password hashing if using the Credentials provider and its types.bashnpm install next-auth plivo bcrypt npm install -D @types/bcrypt(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.
bashnpm install @prisma/client @next-auth/prisma-adapter npm install -D prisma npx prisma init --datasource-provider postgresqlThis 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:
bashopenssl rand -base64 32Copy the output.
-
Edit
.env.local:plaintext# .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" -
Get Plivo Credentials:
- Log in to your Plivo Console.
- Find your
AUTH IDandAUTH TOKENdisplayed 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) intoPLIVO_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.prismato 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.
bashnpx prisma migrate dev --name init_nextauthThis 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.
typescript// 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:
typescript// 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.
typescript// 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:
typescript// 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?
- 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
SessionProviderto make session data available globally via hooks likeuseSession.typescript// 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:typescript// 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.
typescript// 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:- Ensure your database is running and accessible based on
DATABASE_URLin.env.local. - Run
npx prisma studioin your terminal. This opens a web interface to your database. - Navigate to the
Usermodel. - Click "Add record".
- Enter an email address (e.g.,
test@example.com). - For the
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:Run this script usingjavascript// 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$...). - Paste the copied hash into the
passwordfield 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. - Ensure your database is running and accessible based on
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.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:
- 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.json{ "success": false, "error": "Server configuration error." }json{ "success": false, "error": "Plivo API Error: [Specific error from Plivo]", "details": { ... } }
- Endpoint:
-
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.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_IDandPLIVO_AUTH_TOKENsecurely in.env.localand access them viaprocess.env. Obtain them directly from the Plivo Console Dashboard. - Sender Number: Store
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). - SDK Usage: Install the
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 URLs: The
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):
- Supported Formats:
- 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
getServerSessionand return a 401 if none exists. - Input Validation Errors: Check for the presence and basic type of
to,text, andmediaUrl, 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.createcall in atry...catchblock. 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, 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):
- Error Code 40: Invalid Source Number - The sender number is incorrectly formatted, not SMS/MMS-enabled, or not assigned to your account
- Error Code 50: Invalid Destination Number - The recipient number is incorrectly formatted, not SMS-enabled, or is a landline
- Error Code 120: MMS Message Payload Too Large - Total size exceeds 5MB limit
- Error Code 130: Unsupported Message Media - One or more media attachments is of an unsupported type
- Error Code 140: Message Media Processing Failed - Media URL is unreachable or file data is incorrectly formatted
- Error Code 420: Message Expired - Message remained in queue longer than 3 hours (default expiry)
- Error Code 900: Insufficient Credit - Account lacks required credits
- Error Code 1000: Unknown Error - Failure reason unknown; contact Plivo support if persistent
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:
- Unauthorized: Make a
curlrequest without theCookieheader. - Bad Request: Send a
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 Error: Temporarily change
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.
- Schema: The
prisma/schema.prismafile defines theUser,Account,Session, andVerificationTokenmodels required bynext-auth/prisma-adapter. You added an optionalpasswordfield to theUsermodel for the Credentials provider. - Data Access: Prisma Client (
@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. - Migrations: Use
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:
typescript// 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:
typescript// 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:
- 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.,+14155551234for US numbers)
-
Plivo Error Code 40 (Invalid Source Number):
- Cause:
PLIVO_SENDER_NUMBERis incorrect or doesn't have MMS capability - Solution: Verify the number in Plivo Console → Phone Numbers → Your Numbers. Ensure it has MMS capability enabled.
- Cause:
-
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.tsexists with proper module augmentation (see Section 2)
Testing Checklist:
- User can log in with valid credentials
- User cannot access
/send-mmswithout 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_URLto 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 generateduring build process - Run
npx prisma migrate deployfor production migrations (notmigrate dev)
Security Hardening:
- Enable HTTPS only (enforce in Next.js middleware or at CDN level)
- Implement rate limiting on
/api/send-mmsto prevent abuse (use libraries like@upstash/ratelimitor middleware) - Add CSRF protection if not using default NextAuth cookies
- Monitor failed login attempts and implement account lockout
- Regularly rotate
NEXTAUTH_SECRETand 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
How to send MMS with Plivo and Next.js?
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.
What is NextAuth used for in this MMS setup?
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.
Why use Plivo for sending MMS from a web app?
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.
When should I initialize the Plivo client in Next.js?
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.
What Plivo credentials are needed for sending MMS?
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.
How to handle Plivo API errors in Next.js?
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.
How to protect the send MMS API route in Next.js?
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.
What is the role of Prisma in sending MMS with Plivo?
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.
How to structure the request body for the send MMS API?
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).
What are the prerequisites for setting up MMS sending?
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.
How to get a Plivo MMS enabled number?
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.
What media formats are supported for MMS with Plivo?
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.
How to set up NextAuth for protecting the API endpoint?
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.
Can I test sending MMS locally during development?
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.