> ## Documentation Index
> Fetch the complete documentation index at: https://www.docs.wazap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Supabase Integration

> Complete guide for integrating Supabase storage with WhatsApp Client SDK

## Prerequisites

* Node.js 16+ installed
* Supabase account ([create one here](https://supabase.com))
* WhatsApp Business API credentials
* Basic knowledge of TypeScript/JavaScript

***

## Step 1: Create Supabase Database

### 1.1 Create Project

1. Go to [Supabase Dashboard](https://supabase.com/dashboard)
2. Click **"New Project"**
3. Enter project name and database password
4. Choose a region close to your users
5. Click **"Create new project"**

### 1.2 Run SQL Schema

1. Go to **SQL Editor** in your Supabase dashboard
2. Click **"New Query"**
3. Copy the entire content from `/sql/supabase-schema.sql` (located in the SDK repository)
4. Paste and click **"Run"**

<Accordion title="View Complete SQL Schema">
  ```sql theme={null}
  -- WhatsApp SDK Storage Schema for Supabase
  -- Version: 1.0.0

  -- WhatsApp SDK Supabase Schema
  -- This file contains the complete database schema for WhatsApp message storage in Supabase
  -- You can run this manually if you prefer not to use autoCreateTables: true

  -- Enable necessary extensions
  CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

  -- Create conversations table
  CREATE TABLE IF NOT EXISTS whatsapp_conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    phone_number VARCHAR(20) NOT NULL,
    business_phone_id VARCHAR(20) NOT NULL,
    last_message_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    message_count INTEGER DEFAULT 0,
    unread_count INTEGER DEFAULT 0,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT uk_phone_business UNIQUE(phone_number, business_phone_id)
  );

  -- Create messages table
  CREATE TABLE IF NOT EXISTS whatsapp_messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    whatsapp_message_id VARCHAR(255) UNIQUE NOT NULL,
    conversation_id UUID REFERENCES whatsapp_conversations(id) ON DELETE CASCADE,
    phone_number_id VARCHAR(20) NOT NULL,
    from_phone VARCHAR(20) NOT NULL,
    to_phone VARCHAR(20) NOT NULL,
    message_type VARCHAR(20) NOT NULL,
    content JSONB NOT NULL DEFAULT '{}',
    reply_to_message_id UUID REFERENCES whatsapp_messages(id) ON DELETE SET NULL,
    whatsapp_reply_to_id VARCHAR(255),
    timestamp TIMESTAMPTZ NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'sent',
    direction VARCHAR(10) NOT NULL CHECK (direction IN ('incoming', 'outgoing')),
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT ck_message_status CHECK (status IN ('sent', 'delivered', 'read', 'failed')),
    CONSTRAINT ck_message_type CHECK (message_type IN (
      'text', 'image', 'video', 'audio', 'document', 'location',
      'contacts', 'sticker', 'reaction', 'interactive', 'template'
    ))
  );

  -- Create indexes for better performance
  CREATE INDEX IF NOT EXISTS idx_messages_conversation ON whatsapp_messages(conversation_id);
  CREATE INDEX IF NOT EXISTS idx_messages_from_phone ON whatsapp_messages(from_phone);
  CREATE INDEX IF NOT EXISTS idx_messages_to_phone ON whatsapp_messages(to_phone);
  CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON whatsapp_messages(timestamp DESC);
  CREATE INDEX IF NOT EXISTS idx_messages_type ON whatsapp_messages(message_type);
  CREATE INDEX IF NOT EXISTS idx_messages_direction ON whatsapp_messages(direction);
  CREATE INDEX IF NOT EXISTS idx_messages_status ON whatsapp_messages(status);
  CREATE INDEX IF NOT EXISTS idx_messages_reply ON whatsapp_messages(reply_to_message_id);
  CREATE INDEX IF NOT EXISTS idx_messages_whatsapp_reply ON whatsapp_messages(whatsapp_reply_to_id);
  CREATE INDEX IF NOT EXISTS idx_messages_whatsapp_id ON whatsapp_messages(whatsapp_message_id);

  -- Full-text search index for content
  CREATE INDEX IF NOT EXISTS idx_messages_content_search ON whatsapp_messages
    USING GIN (to_tsvector('english', COALESCE(content->>'text', '')));

  -- Additional useful indexes
  CREATE INDEX IF NOT EXISTS idx_messages_phone_timestamp ON whatsapp_messages(from_phone, timestamp DESC);
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_timestamp ON whatsapp_messages(conversation_id, timestamp DESC);
  CREATE INDEX IF NOT EXISTS idx_messages_direction_timestamp ON whatsapp_messages(direction, timestamp DESC);

  -- Conversations indexes
  CREATE INDEX IF NOT EXISTS idx_conversations_phone ON whatsapp_conversations(phone_number);
  CREATE INDEX IF NOT EXISTS idx_conversations_business ON whatsapp_conversations(business_phone_id);
  CREATE INDEX IF NOT EXISTS idx_conversations_last_message ON whatsapp_conversations(last_message_at DESC);

  -- Functions for maintaining conversation metadata
  CREATE OR REPLACE FUNCTION update_conversation_on_message()
  RETURNS TRIGGER AS $$
  BEGIN
    -- Update conversation activity and message count
    INSERT INTO whatsapp_conversations (phone_number, business_phone_id, last_message_at, message_count, updated_at)
    VALUES (
      CASE
        WHEN NEW.direction = 'incoming' THEN NEW.from_phone
        ELSE NEW.to_phone
      END,
      NEW.phone_number_id,
      NEW.timestamp,
      1,
      NOW()
    )
    ON CONFLICT (phone_number, business_phone_id)
    DO UPDATE SET
      last_message_at = GREATEST(whatsapp_conversations.last_message_at, NEW.timestamp),
      message_count = whatsapp_conversations.message_count + 1,
      updated_at = NOW();

    -- Update conversation_id in the message if it was just created
    IF NEW.conversation_id IS NULL THEN
      UPDATE whatsapp_messages
      SET conversation_id = (
        SELECT id FROM whatsapp_conversations
        WHERE phone_number = CASE
          WHEN NEW.direction = 'incoming' THEN NEW.from_phone
          ELSE NEW.to_phone
        END
        AND business_phone_id = NEW.phone_number_id
      )
      WHERE id = NEW.id;
    END IF;

    -- If this is an incoming message, increment unread count
    IF NEW.direction = 'incoming' THEN
      UPDATE whatsapp_conversations
      SET unread_count = unread_count + 1
      WHERE phone_number = NEW.from_phone
      AND business_phone_id = NEW.phone_number_id;
    END IF;

    RETURN NEW;
  END;
  $$ LANGUAGE plpgsql;

  -- Trigger to automatically update conversation metadata
  DROP TRIGGER IF EXISTS update_conversation_trigger ON whatsapp_messages;
  CREATE TRIGGER update_conversation_trigger
    AFTER INSERT ON whatsapp_messages
    FOR EACH ROW
    EXECUTE FUNCTION update_conversation_on_message();

  -- Function for marking messages as read and updating unread count
  CREATE OR REPLACE FUNCTION mark_conversation_read(conversation_uuid UUID)
  RETURNS void AS $$
  BEGIN
    UPDATE whatsapp_conversations
    SET unread_count = 0, updated_at = NOW()
    WHERE id = conversation_uuid;
  END;
  $$ LANGUAGE plpgsql;

  -- Function for cleaning up old messages
  CREATE OR REPLACE FUNCTION cleanup_old_messages(retention_days INTEGER DEFAULT 90)
  RETURNS INTEGER AS $$
  DECLARE
    deleted_count INTEGER;
  BEGIN
    -- Delete messages older than retention period
    WITH deleted AS (
      DELETE FROM whatsapp_messages
      WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL
      RETURNING conversation_id
    )
    SELECT count(*) INTO deleted_count FROM deleted;

    -- Update conversation message counts
    UPDATE whatsapp_conversations
    SET message_count = (
      SELECT COUNT(*)
      FROM whatsapp_messages
      WHERE conversation_id = whatsapp_conversations.id
    );

    RETURN deleted_count;
  END;
  $$ LANGUAGE plpgsql;

  -- Function for getting conversation statistics
  CREATE OR REPLACE FUNCTION get_conversation_stats(phone_number_param VARCHAR)
  RETURNS TABLE(
    total_messages BIGINT,
    incoming_messages BIGINT,
    outgoing_messages BIGINT,
    first_message_at TIMESTAMPTZ,
    last_message_at TIMESTAMPTZ,
    avg_response_time_minutes NUMERIC
  ) AS $$
  BEGIN
    RETURN QUERY
    SELECT
      COUNT(*) as total_messages,
      COUNT(*) FILTER (WHERE direction = 'incoming') as incoming_messages,
      COUNT(*) FILTER (WHERE direction = 'outgoing') as outgoing_messages,
      MIN(timestamp) as first_message_at,
      MAX(timestamp) as last_message_at,
      AVG(EXTRACT(EPOCH FROM (
        LEAD(timestamp) OVER (ORDER BY timestamp) - timestamp
      )) / 60) as avg_response_time_minutes
    FROM whatsapp_messages m
    JOIN whatsapp_conversations c ON m.conversation_id = c.id
    WHERE c.phone_number = phone_number_param;
  END;
  $$ LANGUAGE plpgsql;

  -- Function for searching messages with full-text search
  CREATE OR REPLACE FUNCTION search_messages(
    search_text TEXT DEFAULT NULL,
    phone_number_filter VARCHAR DEFAULT NULL,
    message_type_filter VARCHAR DEFAULT NULL,
    direction_filter VARCHAR DEFAULT NULL,
    date_from_filter TIMESTAMPTZ DEFAULT NULL,
    date_to_filter TIMESTAMPTZ DEFAULT NULL,
    limit_count INTEGER DEFAULT 50,
    offset_count INTEGER DEFAULT 0
  )
  RETURNS TABLE(
    id UUID,
    whatsapp_message_id VARCHAR,
    conversation_id UUID,
    from_phone VARCHAR,
    to_phone VARCHAR,
    message_type VARCHAR,
    content JSONB,
    msg_timestamp TIMESTAMPTZ,
    direction VARCHAR,
    rank REAL
  ) AS $$
  BEGIN
    RETURN QUERY
    SELECT
      m.id,
      m.whatsapp_message_id,
      m.conversation_id,
      m.from_phone,
      m.to_phone,
      m.message_type,
      m.content,
      m.timestamp AS msg_timestamp,
      m.direction,
      CASE
        WHEN search_text IS NOT NULL THEN
          ts_rank(to_tsvector('english', COALESCE(m.content->>'text', '')), plainto_tsquery('english', search_text))
        ELSE 1.0
      END as rank
    FROM whatsapp_messages m
    LEFT JOIN whatsapp_conversations c ON m.conversation_id = c.id
    WHERE
      (search_text IS NULL OR to_tsvector('english', COALESCE(m.content->>'text', '')) @@ plainto_tsquery('english', search_text))
      AND (phone_number_filter IS NULL OR c.phone_number = phone_number_filter)
      AND (message_type_filter IS NULL OR m.message_type = message_type_filter)
      AND (direction_filter IS NULL OR m.direction = direction_filter)
      AND (date_from_filter IS NULL OR m.timestamp >= date_from_filter)
      AND (date_to_filter IS NULL OR m.timestamp <= date_to_filter)
    ORDER BY
      CASE
        WHEN search_text IS NOT NULL THEN
          ts_rank(to_tsvector('english', COALESCE(m.content->>'text', '')), plainto_tsquery('english', search_text))
        ELSE 0
      END DESC,
      m.timestamp DESC
    LIMIT limit_count
    OFFSET offset_count;
  END;
  $$ LANGUAGE plpgsql;

  -- Views for common queries
  CREATE OR REPLACE VIEW message_threads AS
  SELECT
    m.id,
    m.whatsapp_message_id,
    m.conversation_id,
    m.from_phone,
    m.content,
    m.timestamp,
    m.direction,
    m.reply_to_message_id,
    parent.content as parent_content,
    parent.timestamp as parent_timestamp,
    COALESCE(reply_count.count, 0) as reply_count
  FROM whatsapp_messages m
  LEFT JOIN whatsapp_messages parent ON m.reply_to_message_id = parent.id
  LEFT JOIN (
    SELECT reply_to_message_id, COUNT(*) as count
    FROM whatsapp_messages
    WHERE reply_to_message_id IS NOT NULL
    GROUP BY reply_to_message_id
  ) reply_count ON m.id = reply_count.reply_to_message_id
  ORDER BY m.timestamp DESC;

  CREATE OR REPLACE VIEW conversation_summary AS
  SELECT
    c.*,
    COALESCE(m.content->>'text',
      CASE
        WHEN m.message_type = 'image' THEN '[Image]'
        WHEN m.message_type = 'video' THEN '[Video]'
        WHEN m.message_type = 'audio' THEN '[Audio]'
        WHEN m.message_type = 'document' THEN '[Document]'
        WHEN m.message_type = 'location' THEN '[Location]'
        WHEN m.message_type = 'sticker' THEN '[Sticker]'
        WHEN m.message_type = 'reaction' THEN '[Reaction]'
        ELSE '[' || m.message_type || ']'
      END
    ) as last_message_preview,
    m.message_type as last_message_type,
    m.direction as last_message_direction,
    m.timestamp as last_message_timestamp
  FROM whatsapp_conversations c
  LEFT JOIN whatsapp_messages m ON m.conversation_id = c.id
    AND m.timestamp = c.last_message_at
  ORDER BY c.last_message_at DESC;

  -- Analytics view
  CREATE OR REPLACE VIEW message_analytics AS
  SELECT
    DATE_TRUNC('day', timestamp) as date,
    COUNT(*) as total_messages,
    COUNT(*) FILTER (WHERE direction = 'incoming') as incoming_messages,
    COUNT(*) FILTER (WHERE direction = 'outgoing') as outgoing_messages,
    COUNT(DISTINCT conversation_id) as active_conversations,
    COUNT(DISTINCT CASE WHEN direction = 'incoming' THEN from_phone END) as unique_customers
  FROM whatsapp_messages
  WHERE timestamp >= CURRENT_DATE - INTERVAL '30 days'
  GROUP BY DATE_TRUNC('day', timestamp)
  ORDER BY date DESC;

  -- Optional: Row Level Security (RLS) setup
  -- Uncomment these if you want to enable RLS

  /*
  -- Enable RLS on both tables
  ALTER TABLE whatsapp_messages ENABLE ROW LEVEL SECURITY;
  ALTER TABLE whatsapp_conversations ENABLE ROW LEVEL SECURITY;

  -- RLS Policies for messages table
  -- These assume you have a user_id field in metadata
  CREATE POLICY "Users can view their own messages" ON whatsapp_messages
    FOR SELECT USING (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );

  CREATE POLICY "Users can insert their own messages" ON whatsapp_messages
    FOR INSERT WITH CHECK (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );

  CREATE POLICY "Users can update their own messages" ON whatsapp_messages
    FOR UPDATE USING (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );

  -- RLS Policies for conversations table
  CREATE POLICY "Users can view their own conversations" ON whatsapp_conversations
    FOR SELECT USING (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );

  CREATE POLICY "Users can insert their own conversations" ON whatsapp_conversations
    FOR INSERT WITH CHECK (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );

  CREATE POLICY "Users can update their own conversations" ON whatsapp_conversations
    FOR UPDATE USING (
      auth.uid()::text = metadata->>'user_id' OR
      auth.uid()::text = metadata->>'business_owner_id'
    );
  */

  -- Grant permissions to authenticated users
  -- Adjust these based on your security requirements
  GRANT SELECT, INSERT, UPDATE, DELETE ON whatsapp_messages TO authenticated;
  GRANT SELECT, INSERT, UPDATE, DELETE ON whatsapp_conversations TO authenticated;
  GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO authenticated;

  -- Grant permissions for the views and functions
  GRANT SELECT ON message_threads TO authenticated;
  GRANT SELECT ON conversation_summary TO authenticated;
  GRANT SELECT ON message_analytics TO authenticated;

  GRANT EXECUTE ON FUNCTION update_conversation_on_message() TO authenticated;
  GRANT EXECUTE ON FUNCTION mark_conversation_read(UUID) TO authenticated;
  GRANT EXECUTE ON FUNCTION cleanup_old_messages(INTEGER) TO authenticated;
  GRANT EXECUTE ON FUNCTION get_conversation_stats(VARCHAR) TO authenticated;
  GRANT EXECUTE ON FUNCTION search_messages(TEXT, VARCHAR, VARCHAR, VARCHAR, TIMESTAMPTZ, TIMESTAMPTZ, INTEGER, INTEGER) TO authenticated;

  -- Comments for documentation
  COMMENT ON TABLE whatsapp_conversations IS 'Stores WhatsApp conversation metadata';
  COMMENT ON TABLE whatsapp_messages IS 'Stores individual WhatsApp messages with full content and metadata';

  COMMENT ON COLUMN whatsapp_messages.whatsapp_message_id IS 'Original message ID from WhatsApp API';
  COMMENT ON COLUMN whatsapp_messages.reply_to_message_id IS 'References the parent message for threaded conversations';
  COMMENT ON COLUMN whatsapp_messages.whatsapp_reply_to_id IS 'Original WhatsApp message ID being replied to';
  COMMENT ON COLUMN whatsapp_messages.content IS 'Message content in JSON format - text, media info, location, etc.';
  COMMENT ON COLUMN whatsapp_messages.metadata IS 'Additional metadata like contact info, business context, etc.';

  COMMENT ON FUNCTION update_conversation_on_message() IS 'Automatically maintains conversation metadata when messages are inserted';
  COMMENT ON FUNCTION cleanup_old_messages(INTEGER) IS 'Removes messages older than specified days and updates conversation counts';
  COMMENT ON FUNCTION get_conversation_stats(VARCHAR) IS 'Returns comprehensive statistics for a specific conversation';
  COMMENT ON FUNCTION search_messages IS 'Full-text search across messages with multiple filter options';

  -- Success message
  DO $$
  BEGIN
    RAISE NOTICE 'WhatsApp SDK schema created successfully!';
    RAISE NOTICE 'Tables: whatsapp_conversations, whatsapp_messages';
    RAISE NOTICE 'Functions: update_conversation_on_message, mark_conversation_read, cleanup_old_messages, get_conversation_stats, search_messages';
    RAISE NOTICE 'Views: message_threads, conversation_summary, message_analytics';
    RAISE NOTICE 'Remember to uncomment RLS policies if you need row-level security!';
  END $$;
  ```
</Accordion>

You should see:

```
✅ WhatsApp SDK schema created successfully!
```

### 1.3 Verify Tables Created

Run this query to verify:

```sql theme={null}
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name LIKE 'whatsapp_%';
```

Expected output:

* `whatsapp_messages`
* `whatsapp_conversations`

### 1.4 Get API Credentials

1. Go to **Settings → API**
2. Copy these values:
   * **Project URL**: `https://xxxxx.supabase.co`
   * **Service Role Key** (⚠️ **NOT** the anon/public key)

<Warning>
  **IMPORTANT**: Use the **Service Role Key**, not the anon/public key. The service role key bypasses Row Level Security (RLS) and is only safe for backend/API routes.
</Warning>

***

## Step 2: Install Dependencies

```bash theme={null}
npm install whatsapp-client-sdk@latest @supabase/supabase-js
```

Or with yarn:

```bash theme={null}
yarn add whatsapp-client-sdk@latest @supabase/supabase-js
```

***

## Step 3: Configure Environment Variables

Create a `.env.local` file (or `.env` for non-Next.js projects):

```bash theme={null}
# WhatsApp Credentials
WHATSAPP_ACCESS_TOKEN=your_whatsapp_access_token
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_WEBHOOK_TOKEN=your_webhook_verify_token

# Supabase Credentials
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...  # ⚠️ Use Service Role Key
```

***

## Step 4: Initialize SDK with Storage

### Basic Setup

```typescript theme={null}
import { WhatsAppClient } from 'whatsapp-client-sdk';

const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,

  // Storage configuration
  storage: {
    enabled: true,
    provider: 'supabase',
    options: {
      url: process.env.SUPABASE_URL!,
      apiKey: process.env.SUPABASE_KEY!,
      schema: 'public',        // Optional, default: 'public'
      tablePrefix: 'whatsapp_' // Optional, default: 'whatsapp_'
    },
    features: {
      persistIncoming: true,   // Save incoming messages
      persistOutgoing: true,   // Save outgoing messages
      persistStatus: true,     // Update message status (sent, delivered, read)
      autoConversations: true, // Auto-create conversations
      createThreads: true,     // Track message threads/replies
      enableSearch: true,      // Enable full-text search
      retentionDays: 90       // Auto-delete messages after 90 days
    }
  }
});

// Initialize storage
await client.initializeStorage();
```

### Verify Storage is Enabled

```typescript theme={null}
if (client.isStorageEnabled()) {
  console.log('✅ Storage is enabled and ready');
} else {
  console.error('❌ Storage failed to initialize');
}
```

***

## Step 5: Setup Webhooks with Storage

### Next.js App Router Example

Create `app/api/webhook/route.ts`:

```typescript theme={null}
import { WhatsAppClient } from 'whatsapp-client-sdk';
import { NextRequest, NextResponse } from 'next/server';

const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,
  storage: {
    enabled: true,
    provider: 'supabase',
    options: {
      url: process.env.SUPABASE_URL!,
      apiKey: process.env.SUPABASE_KEY!
    },
    features: {
      persistIncoming: true,
      persistOutgoing: true,
      persistStatus: true,
      autoConversations: true,
      createThreads: true,
      enableSearch: true
    }
  }
});

// Initialize storage once
let initialized = false;
const init = async () => {
  if (!initialized) {
    await client.initializeStorage();
    initialized = true;
  }
};

// Create webhook processor
const webhookProcessor = client.createWebhookProcessor({
  enableBuffer: true,
  bufferTimeMs: 5000,

  onTextMessage: async (messages) => {
    const messagesArray = Array.isArray(messages) ? messages : [messages];

    for (const message of messagesArray) {
      // Messages are automatically saved to Supabase
      // Just handle your business logic here

      const text = message.text?.toLowerCase() || '';

      if (text.includes('hello') || text.includes('hola')) {
        await client.sendText(message.from, '👋 Hi! How can I help you?');
      } else {
        await client.sendText(message.from, `You said: "${message.text}"`);
      }

      await client.markMessageAsRead(message.id);
    }
  },

  onMessageStatusUpdate: async (status) => {
    // Status updates are automatically saved to Supabase
    console.log(`Message ${status.id} is now ${status.status}`);
  },

  onError: async (error) => {
    console.error('Webhook error:', error);
  }
});

// GET endpoint for webhook verification
export async function GET(request: NextRequest) {
  try {
    await init();

    const searchParams = request.nextUrl.searchParams;
    const result = await webhookProcessor.processWebhook(
      {} as any,
      Object.fromEntries(searchParams)
    );

    return new NextResponse(String(result.response), {
      status: result.status
    });
  } catch (error) {
    console.error('GET webhook error:', error);
    return new NextResponse('Internal Server Error', { status: 500 });
  }
}

// POST endpoint for receiving messages
export async function POST(request: NextRequest) {
  try {
    await init();

    const body = await request.json();
    const result = await webhookProcessor.processWebhook(body, {});

    return new NextResponse('OK', { status: result.status });
  } catch (error) {
    console.error('POST webhook error:', error);
    return new NextResponse('Internal Server Error', { status: 500 });
  }
}
```

### Express.js Example

```typescript theme={null}
import express from 'express';
import { WhatsAppClient } from 'whatsapp-client-sdk';

const app = express();
app.use(express.json());

const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
  webhookVerifyToken: process.env.WHATSAPP_WEBHOOK_TOKEN!,
  storage: {
    enabled: true,
    provider: 'supabase',
    options: {
      url: process.env.SUPABASE_URL!,
      apiKey: process.env.SUPABASE_KEY!
    },
    features: {
      persistIncoming: true,
      persistOutgoing: true,
      persistStatus: true,
      autoConversations: true,
      createThreads: true,
      enableSearch: true
    }
  }
});

// Initialize storage
await client.initializeStorage();

const webhookProcessor = client.createWebhookProcessor({
  enableBuffer: true,
  onTextMessage: async (message) => {
    await client.sendText(message.from, `You said: "${message.text}"`);
  }
});

// Webhook verification
app.get('/webhook', (req, res) => {
  const mode = req.query['hub.mode'] as string;
  const token = req.query['hub.verify_token'] as string;
  const challenge = req.query['hub.challenge'] as string;

  const result = client.verifyWebhook(mode, token, challenge);
  if (result !== null) {
    res.status(200).send(result);
  } else {
    res.status(403).send('Forbidden');
  }
});

// Receive messages
app.post('/webhook', async (req, res) => {
  try {
    const result = await webhookProcessor.processWebhook(req.body, req.query);
    res.status(result.status).send(result.response);
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Error');
  }
});

app.listen(3000, () => {
  console.log('✅ Server running on port 3000');
});
```

***

## Querying Stored Messages

See the [Querying Data](/storage/querying) guide for detailed examples of:

* Getting conversation history
* Searching messages
* Tracking message threads
* Analyzing conversations
* Exporting data

Quick example:

```typescript theme={null}
// Get conversation history
const conversation = await client.getConversation('+1234567890', {
  limit: 50,
  offset: 0
});

console.log(`Total messages: ${conversation.totalMessages}`);
conversation.messages.forEach(msg => {
  console.log(`[${msg.direction}] ${msg.content.text || 'Media'}`);
});
```

***

## Troubleshooting

### Storage Not Enabling

**Problem**: `isStorageEnabled()` returns `false`

**Solutions**:

1. **Verify you're using Service Role Key, not Anon Key**
   ```bash theme={null}
   # Check your .env file
   # Service Role Key contains "service_role" when decoded
   echo $SUPABASE_KEY | cut -d. -f2 | base64 -d
   # Should show: "role":"service_role"
   ```

2. **Check tables exist**
   ```sql theme={null}
   SELECT table_name FROM information_schema.tables
   WHERE table_name LIKE 'whatsapp_%';
   ```

3. **Check initialization logs**
   ```typescript theme={null}
   try {
     await client.initializeStorage();
     console.log('Storage enabled:', client.isStorageEnabled());
   } catch (error) {
     console.error('Initialization error:', error);
   }
   ```

### Permission Errors

**Problem**: `insert or update violates foreign key constraint`

**Solution**: Make sure `autoConversations: true` is enabled. This automatically creates conversations before saving messages.

### Connection Errors

**Problem**: `Failed to connect to Supabase`

**Solutions**:

1. Verify URL is correct (should start with `https://`)
2. Check API key is valid
3. Test connection manually:
   ```typescript theme={null}
   import { createClient } from '@supabase/supabase-js';

   const supabase = createClient(
     process.env.SUPABASE_URL!,
     process.env.SUPABASE_KEY!
   );

   const { data, error } = await supabase
     .from('whatsapp_messages')
     .select('id')
     .limit(1);

   if (error) console.error('Connection error:', error);
   else console.log('✅ Connected successfully');
   ```

### RLS (Row Level Security) Issues

If you get permission denied errors with Service Role Key:

```sql theme={null}
-- Disable RLS (for testing only)
ALTER TABLE whatsapp_messages DISABLE ROW LEVEL SECURITY;
ALTER TABLE whatsapp_conversations DISABLE ROW LEVEL SECURITY;
```

<Warning>
  For production, keep RLS enabled and use Service Role Key only in backend.
</Warning>

***

## Best Practices

### 1. Security

* ✅ Use Service Role Key only in backend/API routes
* ✅ Never expose Service Role Key in frontend code
* ✅ Use environment variables for all credentials
* ✅ Add `.env` to `.gitignore`

### 2. Performance

* ✅ Enable `enableBuffer: true` in webhook processor
* ✅ Set appropriate `bufferTimeMs` (default: 5000ms)
* ✅ Use `retentionDays` to auto-cleanup old data
* ✅ Create indexes on frequently queried fields

### 3. Error Handling

```typescript theme={null}
const webhookProcessor = client.createWebhookProcessor({
  onTextMessage: async (message) => {
    try {
      // Your logic here
    } catch (error) {
      console.error('Message handling error:', error);
      // Don't throw - let other messages process
    }
  },

  onError: async (error, message) => {
    // Log critical errors
    console.error('Critical webhook error:', error);
    // Send alert to monitoring service
  }
});
```

### 4. Cleanup Resources

```typescript theme={null}
// When shutting down your application
process.on('SIGTERM', async () => {
  webhookProcessor.destroy(); // Clean up timers
  await client.disconnectStorage(); // Close Supabase connection
  process.exit(0);
});
```

***

## Schema Overview

### whatsapp\_messages Table

| Column                 | Type        | Description                         |
| ---------------------- | ----------- | ----------------------------------- |
| id                     | UUID        | Primary key                         |
| whatsapp\_message\_id  | VARCHAR     | WhatsApp's message ID               |
| conversation\_id       | UUID        | Foreign key to conversations        |
| phone\_number\_id      | VARCHAR     | Your business phone ID              |
| from\_phone            | VARCHAR     | Sender phone number                 |
| to\_phone              | VARCHAR     | Recipient phone number              |
| message\_type          | VARCHAR     | text, image, video, etc.            |
| content                | JSONB       | Message content (text, media, etc.) |
| reply\_to\_message\_id | UUID        | Parent message for threads          |
| timestamp              | TIMESTAMPTZ | Message timestamp                   |
| status                 | VARCHAR     | sent, delivered, read, failed       |
| direction              | VARCHAR     | incoming or outgoing                |
| metadata               | JSONB       | Additional metadata                 |

### whatsapp\_conversations Table

| Column              | Type        | Description            |
| ------------------- | ----------- | ---------------------- |
| id                  | UUID        | Primary key            |
| phone\_number       | VARCHAR     | Customer phone number  |
| business\_phone\_id | VARCHAR     | Your business phone ID |
| last\_message\_at   | TIMESTAMPTZ | Last message timestamp |
| message\_count      | INTEGER     | Total message count    |
| unread\_count       | INTEGER     | Unread message count   |
| metadata            | JSONB       | Additional metadata    |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Query Messages" icon="search" href="/storage/querying">
    Learn how to query and search stored messages
  </Card>

  <Card title="Best Practices" icon="shield-check" href="/storage/best-practices">
    Security, performance, and production tips
  </Card>

  <Card title="Custom Adapters" icon="code" href="/storage/custom-adapters">
    Build custom storage adapters for other databases
  </Card>

  <Card title="Webhook System" icon="webhook" href="/webhooks/overview">
    Learn more about webhook processing
  </Card>
</CardGroup>
