This guide provides a comprehensive walkthrough for building a feature within a Next.js application that enables authenticated users to send Multimedia Messaging Service (MMS) messages using the Plivo Communications Platform.
We'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. We will also 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. Chosen for its robust features, performance optimizations, and integrated API routes.
- NextAuth.js (v5+): A complete authentication solution for Next.js applications. Handles user sessions, login providers, and route protection seamlessly.
- Plivo: A cloud communications platform providing APIs for SMS, MMS, Voice, and more. Selected for its reliable messaging services and 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 specifies target versions like Next.js 14+, NextAuth v5+, and Node.js v18+. While accurate at the time of writing, please 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:
- Node.js (v18 or later) installed.
- npm or yarn package manager.
- A Plivo account (Sign up here).
- Basic understanding of React, Next.js, and asynchronous JavaScript.
- An MMS-enabled phone number purchased or verified within your Plivo account (for US/Canada destinations).
- 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.
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.
1. Setting Up the Project
Let's initialize our Next.js project and install the necessary dependencies.
-
Create Next.js App: Open your terminal and run the following 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, choosing your preferred settings.
-
Navigate to Project Directory:
cd nextjs-plivo-mms
-
Install Dependencies: We need
next-auth
for authentication andplivo
for the Plivo API interaction. We'll also addbcrypt
for password hashing if using the Credentials provider and its types.npm install next-auth plivo bcrypt npm install -D @types/bcrypt
(Alternatively, use
yarn add next-auth plivo bcrypt
andyarn 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, integrating a database is essential for managing user accounts if you use the Credentials provider or want to store user-specific data. We'll use Prisma with PostgreSQL as an example.
npm install @prisma/client @next-auth/prisma-adapter npm install -D prisma npx prisma init --datasource-provider postgresql
This creates a
prisma
directory with aschema.prisma
file and a.env
file for the database connection string. -
Configure Environment Variables: NextAuth requires a secret key, and Plivo needs API credentials. Create a
.env.local
file in the root of your project. Never commit this file to version control. Add your.env.local
to your.gitignore
file if it's not already there.-
Generate NextAuth Secret:
openssl rand -base64 32
Copy the output.
-
Edit
.env.local
:# .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""
-
How to get Plivo Credentials:
- Log in to your Plivo Console.
- Your
AUTH ID
andAUTH TOKEN
are 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 here that has 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
.
-
-
Configure Prisma Schema (if using Prisma): Update
prisma/schema.prisma
to include the necessary models for NextAuth integration.// 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 the database tables based on your schema.
npx prisma migrate dev --name init_nextauth
This will also generate the Prisma Client.
2. Implementing Core Functionality (NextAuth & Plivo Client)
Now, let's 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 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?
- 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.
- 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 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
AuthProvider
component:// 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:- 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:Run this script using// 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
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.
- Ensure your database is running and accessible based on
3. Building the MMS Sending API Layer
This is 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 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 }); } // Basic validation for media URL format (more robust validation recommended) try { new URL(mediaUrl); } catch (error) { console.warn('Invalid media URL format:', mediaUrl); return NextResponse.json({ success: false, error: 'Invalid media URL format.' }, { 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. // Consulting Plivo's API documentation for the most reliable success indicators is recommended. 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):
{ ""to"": ""+15551234567"", ""text"": ""Check out this image!"", ""mediaUrl"": ""https://your-public-domain.com/image.jpg"" }
- Success Response (200 OK):
{ ""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.{ ""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"": { ... } }
- 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-token
cookie 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. Integrating with Plivo (Recap & Details)
We've already used the Plivo SDK in the API route. Here's a recap of the integration points:
- Credentials:
PLIVO_AUTH_ID
andPLIVO_AUTH_TOKEN
are securely stored in.env.local
and accessed viaprocess.env
. They are obtained directly from the Plivo Console Dashboard. - Sender Number:
PLIVO_SENDER_NUMBER
is also in.env.local
. This must be an MMS-enabled number from your Plivo account, found under ""Phone Numbers"" -> ""Your Numbers"". It must be in E.164 format (e.g.,+14151234567
). - SDK Usage: The
plivo
Node.js SDK is installed (npm install plivo
) and used 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 inclient.messages.create
expects an array of strings, where each string is a publicly accessible URL to a media file (JPEG, PNG, GIF supported). 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_id
in themedia_ids
parameter.
5. Implementing Error Handling and Logging
Our API route includes basic error handling:
- Authentication Errors: Checks for a valid session using
getServerSession
and returns a 401 if none exists. - Input Validation Errors: Checks for the presence and basic type of
to
,text
, andmediaUrl
, returning a 400 if invalid. Includes basic URL format validation. - Configuration Errors: Checks if Plivo environment variables are set, returning a 500 if not.
- Plivo API Errors: Wraps the
client.messages.create
call in atry...catch
block. Logs the error to the console (console.error
) and returns a 500 response containing the error message from Plivo. - Logging: Uses
console.log
,console.warn
, andconsole.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).
Testing Error Scenarios:
- Unauthorized: Make a
curl
request without theCookie
header. - Bad Request: Send a
curl
request with missing fields (e.g., nomediaUrl
) or malformed JSON. Send an invalid URL format. - 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/inaccessiblemediaUrl
.
6. Creating a Database Schema and Data Layer (Recap)
We set up Prisma earlier for user authentication data persistence via the PrismaAdapter
.
- Schema: The
prisma/schema.prisma
file defines theUser
,Account
,Session
, andVerificationToken
models required bynext-auth/prisma-adapter
. We added an optionalpassword
field to theUser
model for the Credentials provider. - Data Access: Prisma Client (
@prisma/client
) is used implicitly by thePrismaAdapter
to read/write user and session data. Theauthorize
function within the Credentials provider configuration explicitly usesprisma.user.findUnique
to look up users by email. - Migrations:
npx prisma migrate dev
manages database schema changes based on theschema.prisma
file.