Sent logo
Sent TeamMar 8, 2026 / tools / MessageBird

MessageBird SMS Scheduling and Reminders with Next.js and Supabase

Build a production-ready SMS scheduling and reminder system with MessageBird, Next.js, and Supabase. Learn to schedule SMS reminders, manage automated messages, implement Supabase Cron, and handle time zones, error handling, and delivery tracking.

Schedule a message

Build a production-ready SMS scheduling and reminder system with MessageBird, Next.js, and Supabase. This comprehensive guide shows you how to schedule SMS reminders, manage automated messages through API routes, implement Supabase Cron for reliable task scheduling, and handle time zones, error handling, and delivery tracking in a scalable Node.js application.

Estimated completion time: 2–3 hours

Skill level: Intermediate (requires familiarity with Node.js, React, and REST APIs)

Version Compatibility:

  • Node.js: 18.x or higher (LTS recommended; download from nodejs.org)
  • npm: 9.x or higher (included with Node.js)
  • Next.js: 13.x or 14.x (App Router or Pages Router)
  • MessageBird SDK: messagebird@^4.0.1 (npm package, API docs)
  • Supabase JS Client: @supabase/supabase-js@^2.x (Supabase docs)
  • node-cron: ^3.0.x

Minimum System Requirements: 4GB RAM, modern multi-core processor, 1GB free disk space

By the end of this MessageBird SMS scheduling tutorial, you'll have built a production-ready Next.js application that:

  • Accepts API requests to schedule SMS messages via MessageBird API
  • Stores scheduled messages persistently in Supabase PostgreSQL database
  • Reliably sends messages at scheduled times using Supabase Cron or node-cron
  • Provides RESTful API endpoints to manage (view, cancel) scheduled messages
  • Implements comprehensive error handling, retry logic, and timezone management
  • Handles delivery receipts and message status tracking

Target Audience: Node.js developers building SMS reminder systems, appointment notification services, or marketing automation tools who need to implement reliable scheduled messaging with MessageBird and Supabase.

Technologies Used:

  • Node.js: JavaScript runtime environment.
  • Next.js: React framework for building full-stack web applications with API routes.
  • MessageBird SMS API: Cloud communications platform for sending SMS messages (pricing varies by destination; test credits available).
  • Supabase: Open-source Firebase alternative with PostgreSQL database, authentication, and real-time subscriptions.
  • Supabase Cron: Native Postgres-based cron job scheduler (pg_cron extension) for recurring tasks.
  • node-cron: Alternative task scheduler library for Node.js based on cron syntax (for development or when not using Supabase Cron).
  • dotenv: Module to load environment variables from a .env.local file.

Cost Considerations:

  • MessageBird charges per SMS sent; rates vary by destination country (see pricing)
  • MessageBird offers test credits for new accounts
  • MessageBird enforces rate limits: 500 req/s for POST (SMS sending), 50 req/s for GET operations
  • Supabase offers a free tier; paid plans start at $25/month for production workloads
  • Monitor usage to avoid unexpected charges

System Architecture:

mermaid
graph LR
    A[User / Client<br/>(Browser, API)] --> B(Next.js API Routes<br/>/api/schedule);
    B --> C{Scheduling Service<br/>(Supabase Cron or node-cron)};
    C --> D[MessageBird<br/>SMS API];
    D --> E[User Phone<br/>(SMS Delivery)];

    subgraph Next.js Application
        B --> B1(Input Validation);
        B --> B2(Error Handling);
        B --> B3(Logging);
    end

    subgraph Supabase Backend
        C --> C1[(PostgreSQL Database<br/>Scheduled Messages)];
    end

    B --> C1; // Next.js API interacts with Supabase DB
    C --> C1; // Cron job queries pending messages
    D --> F[Webhook Handler<br/>(Delivery Receipts)];
    F --> C1; // Update message status

Prerequisites:

  • Node.js and npm: Install Node.js 18+ on your system. Download from nodejs.org. Verify installation: node -v and npm -v.
  • MessageBird API Account: Sign up at MessageBird Dashboard. Get your API Key from Dashboard → Developers → API access keys. See creating access keys guide.
  • MessageBird Virtual Mobile Number (VMN): Purchase a number with SMS capabilities for your region. Navigate to Numbers → Buy numbers in the Dashboard. Note: Some countries have registration requirements.
  • Supabase Account and Project: Sign up at supabase.com and create a new project. Get your Project URL and anon/public API key from Project Settings → API.
  • Basic Terminal/Command Line Knowledge: Know how to navigate directories and run commands.
  • (Optional) curl or Postman: For testing the API endpoints.

1. Project Setup and Installation

Initialize your Next.js SMS scheduling project and install the MessageBird SDK, Supabase client, and scheduling dependencies.

Common Setup Issues & Solutions:

  • Port already in use: Change PORT in .env.local or kill the process using the port
  • npm install fails: Clear npm cache (npm cache clean --force) and retry
  • Supabase connection errors: Verify project URL and API keys are correct
  1. Create Next.js Project:

    Open your terminal and create a new Next.js project with TypeScript support.

    bash
    npx create-next-app@latest sms-scheduler
    cd sms-scheduler

    When prompted, select:

    • TypeScript: Yes
    • ESLint: Yes
    • Tailwind CSS: Yes (optional, for UI)
    • src/ directory: Yes (recommended)
    • App Router: Yes (recommended for Next.js 13+)
    • Import alias: No (or customize as needed)
  2. Install Dependencies:

    Install MessageBird SDK, Supabase client, node-cron for scheduling, and dotenv.

    bash
    npm install messagebird @supabase/supabase-js node-cron
    npm install --save-dev @types/node-cron

    Package Versions & Security:

    • Pin major versions in package.json to avoid breaking changes: "messagebird": "^4.0.1"
    • Run npm audit regularly to check for vulnerabilities
    • Use npm audit fix to automatically patch security issues
    • Consider using npm ci in production for reproducible builds
    • Review MessageBird SDK changelog for updates
    • messagebird: Official MessageBird Node.js SDK (npm, GitHub).
    • @supabase/supabase-js: Supabase JavaScript client.
    • node-cron: Task scheduler (for development; production should use Supabase Cron).
  3. Configure Environment Variables:

    Create a .env.local file in the project root (Next.js convention for local environment variables).

    env
    # .env.local
    
    # MessageBird Credentials
    MESSAGEBIRD_API_KEY=your_messagebird_api_key_here
    MESSAGEBIRD_ORIGINATOR=YourSenderID
    
    # Supabase Configuration
    NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here
    SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key_here
    
    # Application Configuration
    CRON_SCHEDULE="*/30 * * * * *"
    APP_TIMEZONE=UTC
    NODE_ENV=development

    Environment Variable Security & Best Practices:

    • Never commit .env.local to version control (included in .gitignore by default)
    • Use NEXT_PUBLIC_ prefix only for client-accessible variables
    • Service role key grants admin access—use only in server-side code (API routes, not client)
    • Rotate API keys every 90 days or when team members leave
    • Use environment-specific files: .env.development, .env.production
    • In production, use secure secrets management:
      • Vercel: Environment Variables in project settings
      • AWS: Secrets Manager or Parameter Store
      • Azure: Key Vault
      • GCP: Secret Manager
    • Validate required environment variables at startup (see Section 5)
    • Document all environment variables in .env.example:
    env
    # .env.example (commit this file)
    MESSAGEBIRD_API_KEY=
    MESSAGEBIRD_ORIGINATOR=
    NEXT_PUBLIC_SUPABASE_URL=
    NEXT_PUBLIC_SUPABASE_ANON_KEY=
    SUPABASE_SERVICE_ROLE_KEY=
    • MESSAGEBIRD_API_KEY: Find in MessageBird Dashboard → Developers → API access keys (guide).
    • MESSAGEBIRD_ORIGINATOR: The sender ID shown to recipients (alphanumeric, max 11 chars, or a phone number). Note: Some countries require registered sender IDs—see country restrictions.
    • NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY: Find in Supabase Project Settings → API.
    • SUPABASE_SERVICE_ROLE_KEY: Admin-level key, found in same location. Keep this secret!
    • CRON_SCHEDULE: How often to check for due messages. */30 * * * * * = every 30 seconds (development). Use * * * * * (every minute) for production. See crontab.guru.
    • APP_TIMEZONE: Use UTC for consistency. Supabase stores timestamps in UTC by default.
  4. Project Structure:

    Next.js uses file-based routing. Create this structure:

    sms-scheduler/ ├── src/ │ ├── app/ # Next.js App Router (Next.js 13+) │ │ ├── api/ │ │ │ └── schedule/ │ │ │ ├── route.ts # POST /api/schedule (create) │ │ │ └── [id]/ │ │ │ └── route.ts # GET/DELETE /api/schedule/[id] │ │ ├── layout.tsx │ │ └── page.tsx │ ├── lib/ │ │ ├── supabase.ts # Supabase client initialization │ │ ├── messagebird.ts # MessageBird client initialization │ │ └── scheduler.ts # Scheduling logic (node-cron) │ └── types/ │ └── index.ts # TypeScript types ├── .env.local ├── .gitignore ├── package.json ├── tsconfig.json └── next.config.js

    Why this structure?

    • App Router (src/app/): Next.js 13+ recommended approach with React Server Components
    • API Routes (src/app/api/): Server-side endpoints for scheduling operations
    • lib/: Reusable utility modules (Supabase, MessageBird clients, scheduler)
    • types/: Centralized TypeScript type definitions
    • Separation of concerns: database, SMS provider, and scheduling logic isolated in lib/

2. MessageBird API Configuration

Configure your MessageBird account, obtain API credentials, and set up SMS sender IDs for scheduled message delivery.

MessageBird Overview:

  • MessageBird (now "Bird") is a cloud communications platform
  • Provides SMS, Voice, WhatsApp, and other messaging channels
  • REST API with simple authentication via API key
  • Pricing varies by destination; test credits available for new accounts
  • Free tier limitations: Test credits expire; sandbox mode for testing
  1. Get MessageBird API Key:

    • Log in to your MessageBird Dashboard.
    • Navigate to DevelopersAPI access keys.
    • Create a new key or use the existing Live API Key (not Test Key for production).
    • Copy the key and store it in your .env.local file as MESSAGEBIRD_API_KEY.
    • Important: Test keys only work with test numbers. Use Live keys for real SMS delivery.
  2. Buy a Virtual Mobile Number (VMN):

    • In the Dashboard, go to NumbersBuy numbers.
    • Search for numbers in your desired country with SMS capabilities.
    • Country-specific considerations:
      • USA: Requires 10DLC registration for application-to-person (A2P) messaging (10DLC guide)
      • India: Requires DLT (Distributed Ledger Technology) registration
      • China: Special restrictions and operator approvals required
      • UK/EU: Some countries require sender ID registration
      • Check country restrictions page for specifics
    • Purchase the number and note it down for MESSAGEBIRD_ORIGINATOR.
    • SMS capability verification: Ensure the number shows "SMS" in capabilities list.
  3. Webhook Setup for Delivery Receipts (Optional but Recommended):

    MessageBird can send delivery status updates (DLRs) via webhooks when messages are delivered or fail.

    • In Dashboard, go to DevelopersWebhooks (or configure per-message via reportUrl parameter).
    • Add a webhook URL pointing to your Next.js application, e.g., https://yourdomain.com/api/webhooks/messagebird.
    • MessageBird will POST delivery reports to this URL with status updates.
    • Production deployment: Use a public domain or tunneling service like ngrok for local testing.
    • Webhook security: Verify webhook signatures or use HTTPS + secret tokens to prevent spoofing.
    • Implement webhook handler in src/app/api/webhooks/messagebird/route.ts to update message status in Supabase.

MessageBird Rate Limits & Best Practices:

  • Rate limits: 500 POST requests/second for SMS sending, 50 GET requests/second
  • Implement exponential backoff for rate limit errors (HTTP 429)
  • Use reference field to correlate messages with your database records
  • Set reportUrl for per-message delivery receipt webhooks
  • Monitor your MessageBird balance to avoid service interruption
  • Use MessageBird's scheduledDatetime parameter for native API scheduling (alternative to cron-based approach)

3. Supabase Database Setup for SMS Scheduling

Create the PostgreSQL database schema in Supabase to store scheduled messages with proper indexing for performance.

Supabase Overview:

  • Open-source Firebase alternative built on PostgreSQL
  • Provides instant REST API, real-time subscriptions, authentication, and storage
  • Free tier: 500MB database, 1GB file storage, 50k monthly active users
  • Paid tiers: Start at $25/month for unlimited API requests and more resources
  1. Create Supabase Table:

    • Log in to the Supabase Dashboard.
    • Select your project.
    • Go to Table EditorNew table.
    • Create a table named scheduled_messages with these columns:
    Column NameTypeDefaultNullableNotes
    iduuidgen_random_uuid()NoPrimary key
    recipienttextNoPhone number (E.164 format preferred)
    messagetextNoSMS content
    send_attimestamptzNoScheduled send time (UTC)
    statustext'PENDING'NoPENDING, SENT, FAILED, CANCELED
    messagebird_idtextYesMessageBird message ID after sending
    created_attimestamptznow()NoRecord creation time
    updated_attimestamptznow()NoLast update time
    error_messagetextYesError details on failure

    Database Indexing for Performance:

    Add indexes to optimize queries for pending messages:

    sql
    -- Run in Supabase SQL Editor
    CREATE INDEX idx_scheduled_messages_status_send_at
    ON scheduled_messages (status, send_at)
    WHERE status = 'PENDING';
    
    CREATE INDEX idx_scheduled_messages_created_at
    ON scheduled_messages (created_at DESC);

    Why these indexes?

    • idx_scheduled_messages_status_send_at: Critical for the cron job query that finds pending messages due for sending
    • Partial index (WHERE status = 'PENDING') reduces index size and improves performance
    • idx_scheduled_messages_created_at: Speeds up listing recent messages

    Field Constraints & Validation:

    Add constraints at database level for data integrity:

    sql
    -- Message length constraint (SMS is typically 160 chars for GSM, 70 for Unicode)
    ALTER TABLE scheduled_messages
    ADD CONSTRAINT message_length_check
    CHECK (char_length(message) <= 1600);
    
    -- Phone number format validation (basic check for E.164-like format)
    ALTER TABLE scheduled_messages
    ADD CONSTRAINT recipient_format_check
    CHECK (recipient ~ '^\+?[1-9]\d{1,14}$');
    
    -- Status enum constraint
    ALTER TABLE scheduled_messages
    ADD CONSTRAINT status_check
    CHECK (status IN ('PENDING', 'SENT', 'FAILED', 'CANCELED'));
    
    -- Ensure send_at is in the future for new records
    ALTER TABLE scheduled_messages
    ADD CONSTRAINT send_at_future_check
    CHECK (send_at > now());
  2. Enable Row Level Security (RLS):

    For production, enable RLS to control access:

    sql
    ALTER TABLE scheduled_messages ENABLE ROW LEVEL SECURITY;
    
    -- Policy: Allow service role to do anything (for API routes)
    CREATE POLICY "Service role can manage all scheduled messages"
    ON scheduled_messages
    FOR ALL
    TO service_role
    USING (true)
    WITH CHECK (true);
    
    -- Policy: Authenticated users can view their own messages (if you add user_id column)
    -- CREATE POLICY "Users can view own messages"
    -- ON scheduled_messages
    -- FOR SELECT
    -- TO authenticated
    -- USING (auth.uid() = user_id);
  3. Set Up Updated_at Trigger:

    Automatically update updated_at on row changes:

    sql
    CREATE OR REPLACE FUNCTION update_updated_at_column()
    RETURNS TRIGGER AS $$
    BEGIN
        NEW.updated_at = now();
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql;
    
    CREATE TRIGGER update_scheduled_messages_updated_at
    BEFORE UPDATE ON scheduled_messages
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
  4. Migration Workflow & Rollback:

    Use Supabase migrations for version control:

    bash
    # Install Supabase CLI
    npm install -g supabase
    
    # Initialize local Supabase
    supabase init
    
    # Create migration file
    supabase migration new create_scheduled_messages_table
    
    # Apply migrations (local)
    supabase db reset
    
    # Apply migrations (remote)
    supabase db push
    
    # Rollback (if needed)
    supabase migration repair --status reverted

    Production migration strategy:

    • Test migrations on staging environment first
    • Use transactions to ensure atomic migrations
    • Keep migrations small and focused
    • Document rollback procedures for each migration
    • Never delete migration files from version control

4. Initialize Supabase and MessageBird Clients

Create reusable client initialization modules.

  1. Create Supabase Client (src/lib/supabase.ts):

    typescript
    // src/lib/supabase.ts
    import { createClient } from '@supabase/supabase-js';
    
    const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
    const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
    
    if (!supabaseUrl || !supabaseServiceKey) {
      throw new Error('Missing Supabase environment variables');
    }
    
    // Use service role key for server-side operations (bypasses RLS)
    export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
      auth: {
        autoRefreshToken: false,
        persistSession: false,
      },
    });
    
    // For client-side operations (respects RLS)
    const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
    export const supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
    
    // Database types
    export interface ScheduledMessage {
      id: string;
      recipient: string;
      message: string;
      send_at: string; // ISO 8601 timestamp
      status: 'PENDING' | 'SENT' | 'FAILED' | 'CANCELED';
      messagebird_id: string | null;
      created_at: string;
      updated_at: string;
      error_message: string | null;
    }

    Supabase Connection Pooling:

    • Supabase automatically manages connection pooling via PgBouncer
    • Default pool size: 15 connections (Free tier), up to 1000 (Enterprise)
    • For serverless environments (Next.js API routes), use Supabase client (not raw Postgres driver)
    • Monitor connection usage in Supabase Dashboard → Database → Connection Pooling
  2. Create MessageBird Client (src/lib/messagebird.ts):

    typescript
    // src/lib/messagebird.ts
    import messagebird from 'messagebird';
    
    const apiKey = process.env.MESSAGEBIRD_API_KEY;
    
    if (!apiKey) {
      throw new Error('MESSAGEBIRD_API_KEY is not set');
    }
    
    export const messagebirdClient = messagebird(apiKey);
    
    export const ORIGINATOR = process.env.MESSAGEBIRD_ORIGINATOR || 'MessageBird';
    
    // Helper function to send SMS
    export async function sendSMS(
      recipient: string,
      message: string
    ): Promise<{ id: string; status: string }> {
      return new Promise((resolve, reject) => {
        messagebirdClient.messages.create(
          {
            originator: ORIGINATOR,
            recipients: [recipient],
            body: message,
          },
          (err, response) => {
            if (err) {
              reject(err);
            } else {
              resolve({
                id: response.id,
                status: response.recipients.items[0]?.status || 'unknown',
              });
            }
          }
        );
      });
    }

Graceful Shutdown Handling:

In Next.js, API routes are stateless and don't require explicit connection cleanup. However, for long-running processes (like cron jobs), implement graceful shutdown:

typescript
// src/lib/shutdown.ts
export function setupGracefulShutdown() {
  const signals = ['SIGTERM', 'SIGINT', 'SIGUSR2'] as const;

  signals.forEach((signal) => {
    process.on(signal, async () => {
      console.log(`Received ${signal}, shutting down gracefully...`);

      // Stop accepting new cron job executions
      // Finish pending database operations
      // Close connections if needed

      process.exit(0);
    });
  });
}

5. Implement SMS Scheduling Logic with Supabase Cron

Implement production-grade SMS scheduling using Supabase's native pg_cron extension or node-cron for automated message delivery.

Distributed Scheduling Concerns:

  • Horizontal scaling: Running multiple Next.js instances can cause duplicate executions
  • Solutions:
    1. Supabase Cron (recommended): Single scheduler within Postgres, no race conditions
    2. Distributed locks: Use pg_advisory_lock for node-cron in multi-instance setups
    3. Dedicated scheduler service: Single cron instance, multiple API instances

Using Supabase Cron (Recommended for Production):

  1. Enable pg_cron Extension:

    In Supabase Dashboard, go to DatabaseExtensions, search for pg_cron, and enable it.

  2. Create Cron Job to Process Pending Messages:

    In Supabase SQL Editor, run:

    sql
    -- Create function to process pending messages
    CREATE OR REPLACE FUNCTION process_pending_messages()
    RETURNS void AS $$
    DECLARE
      msg RECORD;
      response TEXT;
    BEGIN
      -- Find messages due for sending
      FOR msg IN
        SELECT id, recipient, message, send_at
        FROM scheduled_messages
        WHERE status = 'PENDING' AND send_at <= NOW()
        FOR UPDATE SKIP LOCKED -- Prevent concurrent processing
      LOOP
        BEGIN
          -- Call Next.js API route to send SMS
          SELECT content INTO response
          FROM http((
            'POST',
            'https://your-domain.com/api/send-sms-internal', -- Replace with your domain
            ARRAY[http_header('Content-Type', 'application/json')],
            'application/json',
            json_build_object(
              'id', msg.id,
              'recipient', msg.recipient,
              'message', msg.message
            )::text
          ));
    
          -- Log success
          RAISE NOTICE 'Processed message %', msg.id;
        EXCEPTION WHEN OTHERS THEN
          -- Update message status to FAILED on error
          UPDATE scheduled_messages
          SET status = 'FAILED', error_message = SQLERRM, updated_at = NOW()
          WHERE id = msg.id;
    
          RAISE WARNING 'Failed to process message %: %', msg.id, SQLERRM;
        END;
      END LOOP;
    END;
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    -- Schedule the function to run every minute
    SELECT cron.schedule(
      'process-pending-sms', -- Job name
      '* * * * *',           -- Every minute
      $$SELECT process_pending_messages();$$
    );

    Note: Install the pg_net or http extension to make HTTP requests from Postgres. Alternatively, process messages directly in Postgres using a MessageBird SDK wrapper (advanced).

Alternative: Using node-cron in Next.js (Development/Simple Deployments):

If not using Supabase Cron, implement scheduling in Node.js:

  1. Create Scheduler Service (src/lib/scheduler.ts):

    typescript
    // src/lib/scheduler.ts
    import cron from 'node-cron';
    import { supabaseAdmin } from './supabase';
    import { sendSMS } from './messagebird';
    
    let cronJob: cron.ScheduledTask | null = null;
    
    export function startScheduler() {
      if (cronJob) {
        console.log('Scheduler already running');
        return;
      }
    
      const schedule = process.env.CRON_SCHEDULE || '* * * * *'; // Every minute
    
      if (!cron.validate(schedule)) {
        console.error(`Invalid CRON_SCHEDULE: ${schedule}, defaulting to every minute`);
      }
    
      cronJob = cron.schedule(
        cron.validate(schedule) ? schedule : '* * * * *',
        async () => {
          console.log(`[${new Date().toISOString()}] Checking for due messages...`);
          await checkAndSendDueMessages();
        },
        {
          scheduled: true,
          timezone: process.env.APP_TIMEZONE || 'UTC',
        }
      );
    
      console.log(`Scheduler started with pattern: ${schedule}`);
    }
    
    export function stopScheduler() {
      if (cronJob) {
        cronJob.stop();
        cronJob = null;
        console.log('Scheduler stopped');
      }
    }
    
    async function checkAndSendDueMessages() {
      try {
        const now = new Date();
    
        // Query pending messages due for sending
        const { data: dueMessages, error } = await supabaseAdmin
          .from('scheduled_messages')
          .select('*')
          .eq('status', 'PENDING')
          .lte('send_at', now.toISOString())
          .order('send_at', { ascending: true });
    
        if (error) throw error;
    
        if (!dueMessages || dueMessages.length === 0) {
          return; // No messages to send
        }
    
        console.log(`Found ${dueMessages.length} due messages. Processing...`);
    
        for (const msg of dueMessages) {
          try {
            console.log(`Sending message ID: ${msg.id} to ${msg.recipient}`);
    
            const result = await sendSMS(msg.recipient, msg.message);
    
            // Update status to SENT
            await supabaseAdmin
              .from('scheduled_messages')
              .update({
                status: 'SENT',
                messagebird_id: result.id,
                updated_at: new Date().toISOString(),
              })
              .eq('id', msg.id);
    
            console.log(`Message ${msg.id} sent successfully. MessageBird ID: ${result.id}`);
          } catch (err: any) {
            console.error(`Failed to send message ${msg.id}:`, err);
    
            // Update status to FAILED
            await supabaseAdmin
              .from('scheduled_messages')
              .update({
                status: 'FAILED',
                error_message: err.message || 'Unknown error',
                updated_at: new Date().toISOString(),
              })
              .eq('id', msg.id);
          }
        }
      } catch (error) {
        console.error('Error during message check/send process:', error);
      }
    }
  2. Start Scheduler in API Route (src/app/api/cron/start/route.ts):

    typescript
    // src/app/api/cron/start/route.ts
    import { NextResponse } from 'next/server';
    import { startScheduler } from '@/lib/scheduler';
    
    export async function POST() {
      try {
        startScheduler();
        return NextResponse.json({ message: 'Scheduler started' });
      } catch (error: any) {
        return NextResponse.json(
          { error: error.message },
          { status: 500 }
        );
      }
    }

    Important: Call this endpoint once when you deploy your app, or invoke startScheduler() in a global initialization script.

Retry Logic for Failed Messages:

Implement exponential backoff retry for transient failures:

typescript
// Add to scheduler.ts
const MAX_RETRIES = 3;
const RETRY_DELAYS = [60000, 300000, 900000]; // 1min, 5min, 15min

async function sendWithRetry(msg: ScheduledMessage, attempt = 0): Promise<void> {
  try {
    const result = await sendSMS(msg.recipient, msg.message);

    await supabaseAdmin
      .from('scheduled_messages')
      .update({
        status: 'SENT',
        messagebird_id: result.id,
        updated_at: new Date().toISOString(),
      })
      .eq('id', msg.id);
  } catch (err: any) {
    if (attempt < MAX_RETRIES && isRetriableError(err)) {
      const delay = RETRY_DELAYS[attempt];
      console.log(`Retrying message ${msg.id} in ${delay}ms (attempt ${attempt + 1})`);

      setTimeout(() => sendWithRetry(msg, attempt + 1), delay);
    } else {
      // Max retries exceeded or non-retriable error
      await supabaseAdmin
        .from('scheduled_messages')
        .update({
          status: 'FAILED',
          error_message: `${err.message} (after ${attempt + 1} attempts)`,
          updated_at: new Date().toISOString(),
        })
        .eq('id', msg.id);
    }
  }
}

function isRetriableError(err: any): boolean {
  // Retry on rate limits (429) or server errors (5xx)
  return err.statusCode === 429 || (err.statusCode >= 500 && err.statusCode < 600);
}

Rate Limiting to Prevent API Throttling:

Implement rate limiting to respect MessageBird's 500 req/s POST limit:

typescript
// src/lib/rate-limiter.ts
class RateLimiter {
  private tokens: number;
  private lastRefill: number;
  private readonly maxTokens: number;
  private readonly refillRate: number; // tokens per second

  constructor(maxTokens: number, refillRate: number) {
    this.maxTokens = maxTokens;
    this.refillRate = refillRate;
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
  }

  async waitForToken(): Promise<void> {
    this.refill();

    if (this.tokens < 1) {
      const waitTime = (1 / this.refillRate) * 1000;
      await new Promise(resolve => setTimeout(resolve, waitTime));
      this.refill();
    }

    this.tokens -= 1;
  }

  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
    this.lastRefill = now;
  }
}

// Limit to 400 req/s (80% of MessageBird's 500 req/s limit for safety)
export const messagebirdRateLimiter = new RateLimiter(400, 400);

Use in scheduler:

typescript
await messagebirdRateLimiter.waitForToken();
const result = await sendSMS(msg.recipient, msg.message);

6. Build Next.js API Routes for SMS Management

Create RESTful API endpoints to schedule SMS messages, retrieve message status, and cancel pending reminders.

API Authentication & Authorization:

For production, implement authentication:

typescript
// src/lib/auth.ts
import { NextRequest } from 'next/server';

export function validateApiKey(request: NextRequest): boolean {
  const apiKey = request.headers.get('x-api-key');
  const validKey = process.env.API_SECRET_KEY;

  if (!validKey) {
    throw new Error('API_SECRET_KEY not configured');
  }

  return apiKey === validKey;
}

// Middleware for protected routes
export function requireAuth(handler: Function) {
  return async (request: NextRequest, context: any) => {
    if (!validateApiKey(request)) {
      return new Response(JSON.stringify({ error: 'Unauthorized' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' },
      });
    }
    return handler(request, context);
  };
}

Request Validation with Zod:

Use schema validators for robust input validation:

bash
npm install zod
typescript
// src/lib/validation.ts
import { z } from 'zod';

export const createScheduleSchema = z.object({
  recipient: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid E.164 phone number format'),
  message: z.string().min(1).max(1600, 'Message too long'),
  sendAt: z.string().datetime('Invalid ISO 8601 datetime'),
});

export type CreateScheduleInput = z.infer<typeof createScheduleSchema>;
  1. Create Schedule Endpoint (src/app/api/schedule/route.ts):

    typescript
    // src/app/api/schedule/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { supabaseAdmin } from '@/lib/supabase';
    import { createScheduleSchema } from '@/lib/validation';
    
    export async function POST(request: NextRequest) {
      try {
        const body = await request.json();
    
        // Validate input
        const validation = createScheduleSchema.safeParse(body);
        if (!validation.success) {
          return NextResponse.json(
            { error: 'Validation failed', details: validation.error.errors },
            { status: 400 }
          );
        }
    
        const { recipient, message, sendAt } = validation.data;
    
        // Ensure sendAt is in the future
        const sendAtDate = new Date(sendAt);
        if (sendAtDate <= new Date()) {
          return NextResponse.json(
            { error: 'Scheduled time must be in the future' },
            { status: 400 }
          );
        }
    
        // Insert into Supabase
        const { data, error } = await supabaseAdmin
          .from('scheduled_messages')
          .insert({
            recipient,
            message,
            send_at: sendAtDate.toISOString(),
            status: 'PENDING',
          })
          .select()
          .single();
    
        if (error) throw error;
    
        return NextResponse.json(
          {
            message: 'SMS scheduled successfully',
            scheduleId: data.id,
            details: data,
          },
          { status: 201 }
        );
      } catch (error: any) {
        console.error('Error in POST /api/schedule:', error);
        return NextResponse.json(
          { error: error.message || 'Internal server error' },
          { status: 500 }
        );
      }
    }
  2. Get Schedule Status Endpoint (src/app/api/schedule/[id]/route.ts):

    typescript
    // src/app/api/schedule/[id]/route.ts
    import { NextRequest, NextResponse } from 'next/server';
    import { supabaseAdmin } from '@/lib/supabase';
    
    export async function GET(
      request: NextRequest,
      { params }: { params: { id: string } }
    ) {
      try {
        const { id } = params;
    
        const { data, error } = await supabaseAdmin
          .from('scheduled_messages')
          .select('*')
          .eq('id', id)
          .single();
    
        if (error || !data) {
          return NextResponse.json(
            { error: 'Scheduled message not found' },
            { status: 404 }
          );
        }
    
        return NextResponse.json(data);
      } catch (error: any) {
        console.error(`Error in GET /api/schedule/${params.id}:`, error);
        return NextResponse.json(
          { error: error.message || 'Internal server error' },
          { status: 500 }
        );
      }
    }
    
    export async function DELETE(
      request: NextRequest,
      { params }: { params: { id: string } }
    ) {
      try {
        const { id } = params;
    
        // Check if message exists and is pending
        const { data: existing, error: fetchError } = await supabaseAdmin
          .from('scheduled_messages')
          .select('status')
          .eq('id', id)
          .single();
    
        if (fetchError || !existing) {
          return NextResponse.json(
            { error: 'Scheduled message not found' },
            { status: 404 }
          );
        }
    
        if (existing.status !== 'PENDING') {
          return NextResponse.json(
            { error: `Cannot cancel message with status: ${existing.status}` },
            { status: 400 }
          );
        }
    
        // Update status to CANCELED
        const { data, error } = await supabaseAdmin
          .from('scheduled_messages')
          .update({ status: 'CANCELED', updated_at: new Date().toISOString() })
          .eq('id', id)
          .select()
          .single();
    
        if (error) throw error;
    
        return NextResponse.json({
          message: 'Scheduled message canceled successfully',
          details: data,
        });
      } catch (error: any) {
        console.error(`Error in DELETE /api/schedule/${params.id}:`, error);
        return NextResponse.json(
          { error: error.message || 'Internal server error' },
          { status: 500 }
        );
      }
    }

API Rate Limiting (Application-Level):

Protect your API from abuse:

bash
npm install express-rate-limit
typescript
// src/middleware.ts (Next.js middleware)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const rateLimit = new Map<string, { count: number; resetTime: number }>();

export function middleware(request: NextRequest) {
  const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
  const now = Date.now();
  const limit = 100; // requests per window
  const windowMs = 15 * 60 * 1000; // 15 minutes

  const record = rateLimit.get(ip);

  if (!record || now > record.resetTime) {
    rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
  } else if (record.count >= limit) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  } else {
    record.count++;
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

CORS Configuration for Frontend Integration:

typescript
// src/app/api/schedule/route.ts (add to each API route)
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || '*',
      'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key',
    },
  });
}

Security Middleware with Helmet.js:

For production, add security headers:

bash
npm install helmet

Create custom Next.js middleware:

typescript
// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Security headers
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-XSS-Protection', '1; mode=block');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
  );

  return response;
}

Production Logging Setup:

Implement structured logging with log levels and rotation:

bash
npm install winston
typescript
// src/lib/logger.ts
import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'sms-scheduler' },
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

export default logger;

7. Testing Your SMS Scheduler

Comprehensive Testing Strategy:

  1. Unit Tests: Test individual functions (validation, SMS sending logic)
  2. Integration Tests: Test API routes end-to-end
  3. API Testing: Use curl, Postman, or automated tools

Example: Unit Test with Jest:

bash
npm install --save-dev jest @types/jest ts-jest
typescript
// __tests__/validation.test.ts
import { createScheduleSchema } from '@/lib/validation';

describe('createScheduleSchema', () => {
  it('should validate correct input', () => {
    const valid = {
      recipient: '+14155552671',
      message: 'Test message',
      sendAt: '2025-12-31T23:59:59Z',
    };

    expect(() => createScheduleSchema.parse(valid)).not.toThrow();
  });

  it('should reject invalid phone number', () => {
    const invalid = {
      recipient: 'invalid',
      message: 'Test',
      sendAt: '2025-12-31T23:59:59Z',
    };

    expect(() => createScheduleSchema.parse(invalid)).toThrow();
  });
});

API Testing with curl:

bash
# Schedule a message
curl -X POST http://localhost:3000/api/schedule \
  -H "Content-Type: application/json" \
  -H "x-api-key: your_secret_key" \
  -d '{
    "recipient": "+14155552671",
    "message": "Reminder: Your appointment is tomorrow at 3 PM",
    "sendAt": "2025-10-15T15:00:00Z"
  }'

# Get message status
curl -X GET http://localhost:3000/api/schedule/{id} \
  -H "x-api-key: your_secret_key"

# Cancel message
curl -X DELETE http://localhost:3000/api/schedule/{id} \
  -H "x-api-key: your_secret_key"

8. Deploying Your SMS Scheduling App

Environment Setup:

  • Use Vercel, Netlify, or AWS Amplify for Next.js hosting
  • Set environment variables in hosting platform dashboard
  • Use Supabase production project (not dev project)
  • Configure custom domain and SSL certificates

Database Migration Strategy:

bash
# Run migrations on production Supabase
supabase link --project-ref your-project-ref
supabase db push

Monitoring & Observability:

  • Enable Supabase Database Logs
  • Set up Vercel Analytics or Sentry for error tracking
  • Monitor MessageBird usage dashboard for rate limits and costs
  • Set up alerts for failed messages (Supabase functions can trigger emails)

Scaling Strategies:

  • Use Supabase Cron for scheduling (scales automatically)
  • Implement database read replicas for high-traffic reads
  • Use Redis or Upstash for caching frequently accessed data
  • Consider message queues (BullMQ, AWS SQS) for high-volume SMS sending

9. Troubleshooting Common SMS Scheduling Issues

Common Errors & Solutions:

ErrorCauseSolution
"Invalid phone number format"Recipient not in E.164 formatUse format +[country code][number], e.g., +14155552671
"Unauthorized" (401 from MessageBird)Invalid API keyVerify MESSAGEBIRD_API_KEY in .env.local
"Rate limit exceeded" (429)Too many requestsImplement rate limiting (see Section 5)
"Message not sent"Insufficient MessageBird balanceTop up balance in MessageBird Dashboard
Supabase connection timeoutNetwork issues or RLS blocking accessCheck RLS policies; use service role key for server-side
Cron job not runningpg_cron not enabled or schedule invalidEnable pg_cron extension; verify cron syntax
Duplicate message sendsMultiple scheduler instancesUse Supabase Cron or implement distributed locks

10. Best Practices for Production SMS Scheduling

Timezone Handling:

  • Store all timestamps in UTC (Supabase default)
  • Convert to user's local timezone only in UI
  • Clearly document timezone expectations in API
  • Use ISO 8601 format with timezone info: 2025-10-15T15:00:00Z

Scheduling Edge Cases:

  • Past timestamps: Reject at API level (validation in Section 6)
  • Far-future dates: Consider maximum scheduling window (e.g., 1 year)
  • Daylight Saving Time: Use UTC to avoid DST ambiguity
  • Leap seconds: PostgreSQL handles automatically

Delivery Receipt Handling:

Create webhook endpoint to update message status:

typescript
// src/app/api/webhooks/messagebird/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase';

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

    // MessageBird sends: { id, reference, recipient, status, statusDatetime, ... }
    const { id: messagebirdId, status, statusReason } = body;

    // Map MessageBird status to our status
    const ourStatus = status === 'delivered' ? 'SENT' : 'FAILED';

    // Update message in database
    await supabaseAdmin
      .from('scheduled_messages')
      .update({
        status: ourStatus,
        error_message: statusReason,
        updated_at: new Date().toISOString(),
      })
      .eq('messagebird_id', messagebirdId);

    return NextResponse.json({ received: true });
  } catch (error: any) {
    console.error('Webhook error:', error);
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

Monitoring & Alerting:

  • Set up Supabase Database Webhooks to trigger alerts on failed messages
  • Use Vercel Monitoring or Datadog for application performance
  • Monitor MessageBird balance to avoid service interruption
  • Track key metrics: messages sent/day, failure rate, average latency

Performance Optimization:

  • Database indexes: Already covered in Section 3
  • Caching: Cache frequently accessed data (message templates, user preferences) in Redis
  • Batch processing: Send multiple SMS in parallel (respect rate limits)
  • Query optimization: Use select() to fetch only needed columns

Bulk Scheduling Endpoint:

Add endpoint for scheduling multiple messages at once:

typescript
// src/app/api/schedule/bulk/route.ts
export async function POST(request: NextRequest) {
  const { messages } = await request.json(); // Array of { recipient, message, sendAt }

  // Validate each message
  const validatedMessages = messages.map((msg: any) =>
    createScheduleSchema.parse(msg)
  );

  // Insert in batch
  const { data, error } = await supabaseAdmin
    .from('scheduled_messages')
    .insert(validatedMessages.map(msg => ({
      recipient: msg.recipient,
      message: msg.message,
      send_at: msg.sendAt,
      status: 'PENDING',
    })))
    .select();

  if (error) throw error;

  return NextResponse.json({
    message: `${data.length} messages scheduled`,
    scheduleIds: data.map(d => d.id)
  });
}

List Schedules with Pagination:

typescript
// src/app/api/schedule/list/route.ts
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');
  const status = searchParams.get('status') || undefined;

  const offset = (page - 1) * limit;

  let query = supabaseAdmin
    .from('scheduled_messages')
    .select('*', { count: 'exact' })
    .range(offset, offset + limit - 1)
    .order('created_at', { ascending: false });

  if (status) {
    query = query.eq('status', status);
  }

  const { data, error, count } = await query;

  if (error) throw error;

  return NextResponse.json({
    data,
    pagination: {
      page,
      limit,
      total: count,
      totalPages: Math.ceil((count || 0) / limit),
    },
  });
}

Conclusion: Your MessageBird SMS Scheduler is Ready

You've successfully built a production-ready SMS scheduling system using MessageBird API, Next.js, and Supabase Cron. Your system includes:

  • Scheduled SMS delivery with precise timing control
  • Persistent storage in Supabase PostgreSQL
  • RESTful API for scheduling, viewing, and canceling messages
  • Error handling and retry logic for reliability
  • Rate limiting and security best practices
  • Scalable architecture using Supabase Cron or node-cron

Next Steps:

  1. Add user authentication with Supabase Auth
  2. Build a frontend UI with React (already set up in Next.js)
  3. Implement message templates and personalization
  4. Add recurring schedule support (daily, weekly reminders)
  5. Integrate with calendar APIs (Google Calendar, Outlook) for appointment reminders
  6. Set up comprehensive monitoring and alerting
  7. Implement A/B testing for message content effectiveness

Resources:

For production deployment, review MessageBird's country-specific regulations and ensure compliance with SMS messaging laws (TCPA in USA, GDPR in EU, etc.).