messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / Node.js

Send MMS messages with Node.js, Next.js, Supabase, and Sinch

Learn how to build a production-ready Node.js application using Next.js API routes and Supabase to send Multimedia Messaging Service (MMS) messages programmatically via the Sinch MMS API.

Send MMS messages with Node.js, Next.js, Supabase, and Sinch

Learn how to build a production-ready Node.js application using Next.js API routes and Supabase to send Multimedia Messaging Service (MMS) messages programmatically via the Sinch MMS API. This comprehensive guide covers project setup, authentication, database integration, webhook handling, and deployment to Vercel.

Expected Outcomes: Build a production-ready Next.js application with:

  • Next.js API routes for sending MMS messages via Sinch
  • Supabase integration for storing message logs and tracking delivery status
  • Authentication and authorization using Supabase Auth
  • Error handling, validation, and security best practices

Time Estimate: 2–3 hours for basic implementation; additional time for optional enhancements

Prerequisites:

  • Intermediate JavaScript/TypeScript knowledge
  • Basic understanding of React and Next.js concepts
  • Familiarity with REST APIs and async/await patterns

What is MMS messaging and why use it in your application?

MMS (Multimedia Messaging Service) allows you to send rich media content—images, videos, audio files, and documents—directly to mobile phones through standard messaging channels. Unlike SMS, which is limited to text, MMS enables visual communication that can significantly improve user engagement and conversion rates.

Problem Solved: Integrate MMS capabilities into web applications programmatically, enabling automated notifications, user engagement campaigns, and multimedia content delivery via standard mobile messaging channels. Supabase integration adds message tracking, delivery history, and user management capabilities.

Real-World Use Cases:

  • Marketing campaigns with multimedia content (promotional images, video clips)
  • Appointment reminders with location maps or confirmation documents
  • Two-factor authentication with QR codes
  • Customer support with troubleshooting images or instruction videos
  • Event invitations with digital tickets and venue information

Technology stack overview

Technologies:

  • Node.js: JavaScript runtime environment for building server-side applications. Chosen for its large ecosystem (npm), asynchronous nature, and suitability for I/O-bound tasks like API interactions. Recommended: Node.js 18.x or later (LTS versions 18, 20, or 22 with npm 8+).
  • Next.js: React framework with hybrid static & server rendering, file-based routing, and API routes. Chosen for its developer experience, built-in API routes eliminating the need for a separate Express server, and excellent deployment options (Vercel, Netlify). Version 13+ recommended for App Router support.
  • Supabase: Open-source Firebase alternative providing PostgreSQL database, authentication, real-time subscriptions, and storage. Chosen for rapid development with built-in auth, Row Level Security (RLS), and generous free tier.
  • Sinch MMS API: Service providing programmatic access to send and manage MMS messages via the dedicated MMS JSON API. The API uses a sendmms action with slide-based content structure supporting images, videos, audio, and documents.
  • Axios: Promise-based HTTP client for Node.js. Chosen for making requests to the Sinch API easily.

System Architecture:

Simplified system architecture:

+-------------+ +---------------------+ +---------------------+ +-----------------+ +-------------+ | |------>| |------>| |------>| |------>| | | User/Client | HTTP | Next.js API Route | HTTP | Sinch MMS API | MMS | Recipient | | (Browser) | POST | (Server-side) | POST | (sendmms endpoint) | | (Mobile) | | |<------| |<------| |<------| | +-------------+ +---------------------+ +---------------------+ +-----------------+ | | | | v | | +------------------+ | | | | | +--------------->| Supabase DB |<---------------------------------------------+ (Auth/Query) | (Message Logs) | (Delivery Status via Webhooks) +------------------+
  1. A client (browser, mobile app) sends an HTTP POST request to the /api/send-mms Next.js API route.
  2. The Next.js API route validates the request, authenticates the user via Supabase Auth, and retrieves necessary credentials.
  3. The API route constructs the payload for the Sinch MMS API and sends an HTTP POST request using Axios.
  4. Sinch processes the request, fetches media from provided URLs, and queues the MMS for delivery to the recipient's mobile device.
  5. Sinch returns a response with a tracking-id to the Next.js API route.
  6. The API route stores the message log in Supabase with status pending and returns a success response to the client.
  7. (Optional) Sinch sends delivery status updates to a webhook endpoint, which updates the message status in Supabase.

Prerequisites:

  • Node.js and npm: Version 18.x or later recommended. Node.js 22 is the latest LTS (as of 2025) with npm 10. (Download Node.js)
  • Sinch Account: Registered account with Sinch. (Sign up at Sinch)
  • Sinch MMS Credentials:
    • service-id (Campaign ID or Service ID for MMS)
    • Provisioned Sinch phone number (Short Code, Toll-Free, or 10DLC) capable of sending MMS
    • Note: Unlike SMS, Sinch MMS uses a different authentication approach. Check your dashboard for MMS-specific credentials.
  • Supabase Account: Registered account with Supabase. (Sign up at Supabase)
  • Supabase Project: Created project with URL and anon/public API key
  • Publicly Accessible Media URL: URL hosting the media file (e.g., image, video) you want to send. This server must provide Content-Length and valid Content-Type headers for the media file per Sinch requirements.

1. Setting up the project

Initialize your Next.js project with Supabase integration and install the necessary dependencies.

  1. Create Next.js Project: Use create-next-app to bootstrap a new Next.js application with TypeScript support.

    bash
    npx create-next-app@latest sinch-mms-nextjs --typescript --tailwind --app
    cd sinch-mms-nextjs

    When prompted:

    • Use App Router: Yes
    • Use TypeScript: Yes (recommended)
    • Use Tailwind CSS: Yes (optional, for styling)
    • Customize default import alias: No
  2. Install Dependencies: Install Supabase client libraries and Axios for HTTP requests.

    bash
    npm install @supabase/supabase-js @supabase/ssr axios
  3. Set Up Environment Variables (.env.local): Create a .env.local file in the project root with your credentials.

    dotenv
    # Supabase Configuration
    NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
    
    # Sinch MMS API Credentials
    SINCH_SERVICE_ID=your_sinch_service_id_or_campaign_id
    SINCH_FROM_NUMBER=your_sinch_mms_enabled_number  # e.g., 12065551212
    
    # Optional: For webhook signature verification
    SINCH_API_TOKEN=your_sinch_api_token

    Finding Your Credentials:

    • Supabase: Go to Project Settings > API. Copy the Project URL and anon public key.
    • Sinch Service ID: In your Sinch Dashboard, navigate to your MMS service/campaign. Find the Service ID or Campaign ID in the MMS configuration section.
    • Sinch From Number: Your provisioned short code, toll-free, or 10DLC number with MMS capability. Use international format without + (e.g., 12065551212).
  4. Configure .gitignore: Ensure .env*.local is in your .gitignore (Next.js includes this by default).

    text
    # .gitignore (verify these are present)
    .env*.local
    node_modules/
    .next/

2. Setting up Supabase database schema

Create a database table to store MMS message logs with delivery tracking.

  1. Navigate to SQL Editor: Open your Supabase Dashboard > SQL Editor.

  2. Create mms_messages Table: Run the following SQL to create the table with Row Level Security (RLS):

    sql
    -- Create mms_messages table
    CREATE TABLE IF NOT EXISTS public.mms_messages (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
      recipient_number TEXT NOT NULL,
      from_number TEXT NOT NULL,
      message_text TEXT,
      media_url TEXT NOT NULL,
      media_type TEXT,
      subject TEXT,
      sinch_tracking_id TEXT,
      status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'queued', 'delivered', 'failed', 'expired')),
      error_message TEXT,
      client_reference TEXT UNIQUE,
      created_at TIMESTAMPTZ DEFAULT NOW(),
      updated_at TIMESTAMPTZ DEFAULT NOW()
    );
    
    -- Create index for faster queries
    CREATE INDEX IF NOT EXISTS idx_mms_messages_user_id ON public.mms_messages(user_id);
    CREATE INDEX IF NOT EXISTS idx_mms_messages_tracking_id ON public.mms_messages(sinch_tracking_id);
    CREATE INDEX IF NOT EXISTS idx_mms_messages_status ON public.mms_messages(status);
    
    -- Enable Row Level Security
    ALTER TABLE public.mms_messages ENABLE ROW LEVEL SECURITY;
    
    -- Create RLS policies
    -- Users can read their own messages
    CREATE POLICY "Users can view own messages"
      ON public.mms_messages
      FOR SELECT
      USING (auth.uid() = user_id);
    
    -- Users can insert their own messages
    CREATE POLICY "Users can insert own messages"
      ON public.mms_messages
      FOR INSERT
      WITH CHECK (auth.uid() = user_id);
    
    -- Service role can update messages (for webhook updates)
    CREATE POLICY "Service role can update messages"
      ON public.mms_messages
      FOR UPDATE
      USING (true);
    
    -- Create updated_at trigger
    CREATE OR REPLACE FUNCTION update_updated_at_column()
    RETURNS TRIGGER AS $$
    BEGIN
      NEW.updated_at = NOW();
      RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    
    CREATE TRIGGER update_mms_messages_updated_at
      BEFORE UPDATE ON public.mms_messages
      FOR EACH ROW
      EXECUTE FUNCTION update_updated_at_column();
  3. Verify Table Creation: Open Database > Tables to confirm the mms_messages table exists.

3. Setting up Supabase utilities

Create utility functions for Supabase client initialization in both client and server contexts.

  1. Create lib/supabase/client.ts: For client-side Supabase access (browser).

    typescript
    import { createBrowserClient } from '@supabase/ssr'
    
    export function createClient() {
      return createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
      )
    }
  2. Create lib/supabase/server.ts: For server-side Supabase access (API routes, Server Components).

    typescript
    import { createServerClient, type CookieOptions } from '@supabase/ssr'
    import { cookies } from 'next/headers'
    
    export async function createClient() {
      const cookieStore = await cookies()
    
      return createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            get(name: string) {
              return cookieStore.get(name)?.value
            },
            set(name: string, value: string, options: CookieOptions) {
              try {
                cookieStore.set({ name, value, ...options })
              } catch (error) {
                // Handle cookie setting errors (e.g., in middleware)
              }
            },
            remove(name: string, options: CookieOptions) {
              try {
                cookieStore.set({ name, value: '', ...options })
              } catch (error) {
                // Handle cookie removal errors
              }
            },
          },
        }
      )
    }

4. Implementing core functionality: Sending MMS via Next.js API Route

Create the Next.js API route for sending MMS messages using the official Sinch MMS API structure.

Critical: Sinch MMS API Endpoint and Payload Structure

According to the official Sinch MMS API documentation, the MMS API uses:

  • Endpoint: The MMS-specific endpoint (confirm with your Sinch account manager or documentation)
  • Payload Structure: JSON with action: "sendmms", service-id, to, from, and slide array containing content
  • Authentication: Check your Sinch dashboard for authentication method (may differ from SMS API)
  • Key Requirements:
    • Media URLs must return Content-Length and valid Content-Type headers
    • Use international number format (e.g., 17745559144 for US)
    • Each message must contain at least one slide with media or text
    • Maximum 8 slides per message
  1. Create app/api/send-mms/route.ts:

    typescript
    import { NextRequest, NextResponse } from 'next/server'
    import { createClient } from '@/lib/supabase/server'
    import axios from 'axios'
    import { v4 as uuidv4 } from 'uuid'
    
    // Install uuid: npm install uuid
    
    export async function POST(request: NextRequest) {
      try {
        // 1. Authenticate user
        const supabase = await createClient()
        const { data: { user }, error: authError } = await supabase.auth.getUser()
    
        if (authError || !user) {
          return NextResponse.json(
            { error: 'Unauthorized' },
            { status: 401 }
          )
        }
    
        // 2. Parse and validate request body
        const body = await request.json()
        const { to, text, mediaUrl, subject, mediaType } = body
    
        if (!to || !mediaUrl) {
          return NextResponse.json(
            { error: 'Missing required fields: to and mediaUrl are required' },
            { status: 400 }
          )
        }
    
        // 3. Validate phone number format (basic E.164 check)
        const phoneRegex = /^\+?[1-9]\d{10,14}$/
        if (!phoneRegex.test(to)) {
          return NextResponse.json(
            { error: 'Invalid phone number format. Use E.164 format (e.g., +12065551212)' },
            { status: 400 }
          )
        }
    
        // 4. Generate client reference for idempotency
        const clientReference = uuidv4()
    
        // 5. Prepare Sinch MMS API request
        const fromNumber = process.env.SINCH_FROM_NUMBER!
        const serviceId = process.env.SINCH_SERVICE_ID!
    
        // Format numbers for Sinch (international format without +)
        const formattedTo = to.replace(/^\+/, '')
        const formattedFrom = fromNumber.replace(/^\+/, '')
    
        // Construct payload per official Sinch MMS API documentation
        const sinchPayload = {
          "action": "sendmms",
          "service-id": serviceId,
          "to": formattedTo,
          "from": formattedFrom,
          "message-subject": subject || "MMS Message",
          "fallback-sms-text": text || "You have received a multimedia message",
          "disable-fallback-sms": false,
          "client-reference": clientReference,
          "slide": [
            {
              "image": { "url": mediaUrl },
              "message-text": text || ""
            }
          ]
        }
    
        // 6. Store message log in Supabase (status: pending)
        const { data: messageLog, error: dbError } = await supabase
          .from('mms_messages')
          .insert({
            user_id: user.id,
            recipient_number: formattedTo,
            from_number: formattedFrom,
            message_text: text,
            media_url: mediaUrl,
            media_type: mediaType || 'image/jpeg',
            subject: subject,
            client_reference: clientReference,
            status: 'pending'
          })
          .select()
          .single()
    
        if (dbError) {
          console.error('Database error:', dbError)
          return NextResponse.json(
            { error: 'Failed to create message log' },
            { status: 500 }
          )
        }
    
        // 7. Send request to Sinch MMS API
        // Note: Verify the exact endpoint URL with your Sinch account setup
        const sinchEndpoint = 'https://api.sinch.com/mms/v1/send'  // Update with actual endpoint
    
        try {
          const sinchResponse = await axios.post(sinchEndpoint, sinchPayload, {
            headers: {
              'Content-Type': 'application/json',
              // Add authentication headers as required by your Sinch MMS setup
              // Example: 'Authorization': `Bearer ${process.env.SINCH_API_TOKEN}`
            },
            timeout: 30000
          })
    
          // 8. Update message log with Sinch tracking ID
          const trackingId = sinchResponse.data['tracking-id']
    
          await supabase
            .from('mms_messages')
            .update({
              sinch_tracking_id: trackingId,
              status: 'queued'
            })
            .eq('id', messageLog.id)
    
          // 9. Return success response
          return NextResponse.json({
            success: true,
            message: 'MMS queued for delivery',
            trackingId: trackingId,
            messageId: messageLog.id
          })
    
        } catch (sinchError: any) {
          console.error('Sinch API error:', sinchError.response?.data || sinchError.message)
    
          // Update message log with error
          await supabase
            .from('mms_messages')
            .update({
              status: 'failed',
              error_message: sinchError.response?.data?.['error-info'] || sinchError.message
            })
            .eq('id', messageLog.id)
    
          return NextResponse.json(
            {
              error: 'Failed to send MMS via Sinch',
              details: sinchError.response?.data
            },
            { status: sinchError.response?.status || 500 }
          )
        }
    
      } catch (error: any) {
        console.error('Unexpected error:', error)
        return NextResponse.json(
          { error: 'Internal server error', details: error.message },
          { status: 500 }
        )
      }
    }
  2. Install UUID package:

    bash
    npm install uuid
    npm install --save-dev @types/uuid

Key Implementation Notes:

  • The API route uses Next.js 13+ App Router conventions with route.ts files
  • Authentication is handled via Supabase Auth cookies
  • Phone numbers are formatted to international format without + prefix as required by Sinch
  • The payload follows the official Sinch MMS JSON API structure
  • Message logs are created before sending to track all attempts
  • Client reference ensures idempotency for retry scenarios

5. Creating a webhook handler for delivery receipts

To track message delivery status, create a webhook endpoint that Sinch will call.

Create app/api/webhooks/sinch/route.ts:

typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

// Use service role key for webhook updates (bypasses RLS)
const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // Add this to .env.local
)

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()

    // Sinch webhook payload typically includes:
    // - tracking-id
    // - status (DELIVERED, FAILED, etc.)
    // - error-code and error-info (if failed)

    const trackingId = body['tracking-id']
    const status = body.status?.toLowerCase()
    const errorInfo = body['error-info']

    if (!trackingId) {
      return NextResponse.json(
        { error: 'Missing tracking-id' },
        { status: 400 }
      )
    }

    // Map Sinch status to our database status
    const dbStatus = mapSinchStatus(status)

    // Update message in database
    const { error } = await supabaseAdmin
      .from('mms_messages')
      .update({
        status: dbStatus,
        error_message: errorInfo || null,
        updated_at: new Date().toISOString()
      })
      .eq('sinch_tracking_id', trackingId)

    if (error) {
      console.error('Failed to update message status:', error)
      return NextResponse.json(
        { error: 'Database update failed' },
        { status: 500 }
      )
    }

    return NextResponse.json({ success: true })

  } catch (error: any) {
    console.error('Webhook error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

function mapSinchStatus(sinchStatus: string): string {
  const statusMap: Record<string, string> = {
    'delivered': 'delivered',
    'failed': 'failed',
    'expired': 'expired',
    'queued': 'queued'
  }
  return statusMap[sinchStatus] || 'pending'
}

Configure Webhook in Sinch Dashboard:

  1. Open your Sinch Dashboard > MMS Settings
  2. Add webhook URL: https://yourdomain.com/api/webhooks/sinch
  3. Select events: Delivery Receipts
  4. Save configuration

6. Building a complete API layer with validation

Enhance the API route with robust validation using Zod.

  1. Install Zod:

    bash
    npm install zod
  2. Update app/api/send-mms/route.ts with validation:

    typescript
    import { z } from 'zod'
    
    // Define validation schema
    const SendMmsSchema = z.object({
      to: z.string()
        .regex(/^\+?[1-9]\d{10,14}$/, 'Invalid E.164 phone number format'),
      text: z.string()
        .max(5000, 'Text exceeds maximum length of 5000 characters')
        .optional(),
      mediaUrl: z.string()
        .url('Invalid media URL format'),
      subject: z.string()
        .max(80, 'Subject exceeds maximum length of 80 characters')
        .optional(),
      mediaType: z.enum(['image/jpeg', 'image/png', 'image/gif', 'video/mp4'])
        .optional()
    })
    
    export async function POST(request: NextRequest) {
      try {
        // ... authentication code ...
    
        // Validate request body
        const body = await request.json()
        const validationResult = SendMmsSchema.safeParse(body)
    
        if (!validationResult.success) {
          return NextResponse.json(
            {
              error: 'Validation failed',
              details: validationResult.error.errors
            },
            { status: 400 }
          )
        }
    
        const { to, text, mediaUrl, subject, mediaType } = validationResult.data
    
        // ... rest of the implementation ...
      } catch (error) {
        // ... error handling ...
      }
    }

7. Adding security features

Implement rate limiting, CORS, and security headers.

  1. Install rate limiting package:

    bash
    npm install @upstash/ratelimit @upstash/redis
  2. Configure rate limiting: Create lib/rate-limit.ts:

    typescript
    import { Ratelimit } from '@upstash/ratelimit'
    import { Redis } from '@upstash/redis'
    
    // Requires Upstash Redis (free tier available)
    export const ratelimit = new Ratelimit({
      redis: Redis.fromEnv(),  // Uses UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN
      limiter: Ratelimit.slidingWindow(10, '1 m'),  // 10 requests per minute
      analytics: true,
    })
  3. Apply rate limiting in API route:

    typescript
    import { ratelimit } from '@/lib/rate-limit'
    
    export async function POST(request: NextRequest) {
      try {
        // Rate limiting by IP
        const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
        const { success, limit, remaining } = await ratelimit.limit(ip)
    
        if (!success) {
          return NextResponse.json(
            { error: 'Rate limit exceeded' },
            {
              status: 429,
              headers: {
                'X-RateLimit-Limit': limit.toString(),
                'X-RateLimit-Remaining': remaining.toString()
              }
            }
          )
        }
    
        // ... rest of implementation ...
      } catch (error) {
        // ... error handling ...
      }
    }

8. Handling special cases relevant to MMS

Media URL Requirements (Critical):

Per Sinch MMS documentation:

  • Must be publicly accessible (no authentication required)
  • Must provide Content-Length header
  • Must provide valid Content-Type header (e.g., image/jpeg, video/mp4)
  • Avoid chunked transfer encoding (Transfer-Encoding: chunked will cause rejection)
  • Maximum file size: typically ~1 MB (varies by carrier)

Example: Validating Media URL Headers

typescript
async function validateMediaUrl(url: string): Promise<boolean> {
  try {
    const response = await axios.head(url, { timeout: 5000 })
    const contentLength = response.headers['content-length']
    const contentType = response.headers['content-type']

    if (!contentLength || !contentType) {
      throw new Error('Missing required headers: Content-Length or Content-Type')
    }

    // Check file size (1 MB = 1048576 bytes)
    if (parseInt(contentLength) > 1048576) {
      throw new Error('Media file exceeds 1 MB limit')
    }

    return true
  } catch (error) {
    console.error('Media URL validation failed:', error)
    return false
  }
}

Using Supabase Storage for Media Hosting:

Instead of external URLs, host media in Supabase Storage:

typescript
import { createClient } from '@/lib/supabase/server'

async function uploadMediaToSupabase(file: File) {
  const supabase = await createClient()
  const fileName = `${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('mms-media')  // Create this bucket in Supabase
    .upload(fileName, file, {
      cacheControl: '3600',
      upsert: false
    })

  if (error) throw error

  // Get public URL
  const { data: { publicUrl } } = supabase.storage
    .from('mms-media')
    .getPublicUrl(fileName)

  return publicUrl
}

Character and Size Limits:

  • Subject: 40 characters recommended, 80 maximum
  • Text per slide: 5000 characters maximum
  • Media file size: ~1 MB recommended (varies by carrier and device)
  • Number of slides: Maximum 8 slides per MMS
  • Fallback SMS text: 110 characters for best deliverability

Number Formatting:

Always use E.164 format: +[country code][subscriber number]

  • US Example: +12065551212
  • UK Example: +447911123456
  • Remove formatting: no spaces, dashes, or parentheses

9. Implementing performance optimizations

Caching Strategy:

typescript
// Cache Sinch credentials in memory
let cachedCredentials: { serviceId: string; fromNumber: string } | null = null

function getCredentials() {
  if (!cachedCredentials) {
    cachedCredentials = {
      serviceId: process.env.SINCH_SERVICE_ID!,
      fromNumber: process.env.SINCH_FROM_NUMBER!
    }
  }
  return cachedCredentials
}

Asynchronous Logging:

typescript
// Log asynchronously without blocking response
async function logMessageAsync(data: any) {
  setImmediate(async () => {
    try {
      await supabase.from('mms_logs').insert(data)
    } catch (error) {
      console.error('Async logging failed:', error)
    }
  })
}

Connection Pooling:

typescript
// Create reusable axios instance with keep-alive
import axios from 'axios'
import http from 'http'
import https from 'https'

const axiosInstance = axios.create({
  timeout: 30000,
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true })
})

10. Adding monitoring, observability, and analytics

Integrating Sentry for Error Tracking:

bash
npm install @sentry/nextjs
typescript
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
})

Supabase Analytics Queries:

sql
-- Message delivery success rate
SELECT
  status,
  COUNT(*) as count,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM mms_messages
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY status;

-- Average delivery time
SELECT
  AVG(EXTRACT(EPOCH FROM (updated_at - created_at))) as avg_seconds
FROM mms_messages
WHERE status = 'delivered'
  AND created_at > NOW() - INTERVAL '7 days';

-- Messages per user
SELECT
  user_id,
  COUNT(*) as message_count
FROM mms_messages
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY user_id
ORDER BY message_count DESC
LIMIT 10;

11. Troubleshooting and Caveats

Common Issues:

IssueCauseSolution
400 Bad RequestInvalid payload structureVerify payload matches Sinch MMS API docs
401 UnauthorizedMissing/invalid credentialsCheck SINCH_SERVICE_ID and authentication headers
Media fetch failedMissing Content-Length headerEnsure media server sends required headers
Fallback to SMSMMS too large or unsupportedReduce media file size (<1 MB) or check carrier support
Delivery failedInvalid number formatUse E.164 format: +[country][number]

Testing Media URLs:

bash
# Verify media URL has required headers
curl -I https://example.com/image.jpg

# Expected response should include:
# Content-Length: 245678
# Content-Type: image/jpeg

Webhook Testing:

Use tools like webhook.site or ngrok to test webhook delivery during development:

bash
# Expose local server for webhook testing
npx ngrok http 3000
# Use the provided URL in Sinch webhook configuration

12. Deployment with Vercel

  1. Push to GitHub:

    bash
    git init
    git add .
    git commit -m "Initial commit"
    git remote add origin your-repo-url
    git push -u origin main
  2. Deploy to Vercel:

    • Open vercel.com and import your repository
    • Configure environment variables:
      • NEXT_PUBLIC_SUPABASE_URL
      • NEXT_PUBLIC_SUPABASE_ANON_KEY
      • SUPABASE_SERVICE_ROLE_KEY
      • SINCH_SERVICE_ID
      • SINCH_FROM_NUMBER
      • SINCH_API_TOKEN (if required)
    • Deploy
  3. Update Supabase Redirect URLs: In Supabase Dashboard > Authentication > URL Configuration, add:

    • https://your-vercel-app.vercel.app/auth/callback
  4. Update Sinch Webhook: Update webhook URL to: https://your-vercel-app.vercel.app/api/webhooks/sinch

Additional Resources

Conclusion

You've built a fully functional Next.js application with:

  • ✅ Sinch MMS API integration using official payload structure
  • ✅ Supabase authentication and database storage
  • ✅ Message logging and delivery tracking
  • ✅ Webhook handling for status updates
  • ✅ Input validation and security features
  • ✅ Production-ready error handling and monitoring

Your application is ready for production deployment on Vercel with automatic scaling and edge function support.