> ## Documentation Index
> Fetch the complete documentation index at: https://twigz.arqqin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Notifications

> FCM/APNs-only push notification system with advanced features

<Warning>
  **Important:** This system uses ONLY Firebase Cloud Messaging (FCM) and Apple Push Notification Service (APNs). Expo push service has been completely removed for full control and production reliability.
</Warning>

## Overview

The Notifications API provides a comprehensive push notification system built specifically for production environments. It supports direct FCM and APNs integration, scheduled notifications, bulk messaging, and advanced debugging tools.

**Location:** `convex/shared/notifications.ts` & `convex/shared/notificationActions.ts`

## Key Features

<CardGroup cols={2}>
  <Card title="FCM/APNs Direct" icon="mobile">
    Direct integration with Firebase and Apple push services - no third-party dependencies
  </Card>

  <Card title="Scheduled Notifications" icon="clock">
    Send notifications at specific future times with automatic scheduling
  </Card>

  <Card title="Bulk Messaging" icon="users">
    Efficiently send notifications to multiple users with batching and rate limiting
  </Card>

  <Card title="Advanced Debugging" icon="bug">
    Comprehensive debugging tools for troubleshooting delivery issues
  </Card>
</CardGroup>

## Record Native Device Token ⭐

**PRIMARY FUNCTION** - Records a native device token (FCM/APNs) for direct push notifications.

<ParamField path="deviceToken" type="string" required>
  Native FCM token (Android) or APNs token (iOS)
</ParamField>

<ParamField path="platform" type="'ios' | 'android'" required>
  Device platform
</ParamField>

<ParamField path="appVersion" type="string">
  App version for tracking
</ParamField>

<ParamField path="deviceInfo" type="object">
  Device information object with `deviceId`, `deviceName`, and `osVersion`
</ParamField>

<CodeGroup>
  ```typescript Expo Integration theme={null}
  import * as Notifications from 'expo-notifications';
  import { Platform } from 'react-native';

  // Get NATIVE token (not Expo token)
  const nativeDeviceToken = await Notifications.getDevicePushTokenAsync();

  // Register with backend
  await convex.mutation(api.shared.notifications.recordNativeDeviceToken, {
    deviceToken: nativeDeviceToken.data, // This is FCM/APNs token
    platform: Platform.OS === 'ios' ? 'ios' : 'android',
    appVersion: "1.0.0",
    deviceInfo: {
      deviceId: "device-123",
      deviceName: "Samsung Galaxy S21",
      osVersion: "Android 12"
    }
  });
  ```

  ```typescript React Native theme={null}
  import messaging from '@react-native-firebase/messaging';
  import { Platform } from 'react-native';

  // Get FCM token directly
  const fcmToken = await messaging().getToken();

  // Register with backend
  await convex.mutation(api.shared.notifications.recordNativeDeviceToken, {
    deviceToken: fcmToken,
    platform: Platform.OS === 'ios' ? 'ios' : 'android',
    appVersion: "1.0.0",
    deviceInfo: {
      deviceId: "device-123",
      deviceName: "iPhone 14 Pro",
      osVersion: "iOS 16.0"
    }
  });
  ```

  ```javascript JavaScript theme={null}
  const deviceToken = await getDeviceToken(); // Your implementation

  await convex.mutation("shared.notifications.recordNativeDeviceToken", {
    deviceToken: deviceToken,
    platform: "android", // or "ios"
    appVersion: "1.0.0",
    deviceInfo: {
      deviceId: "device-123",
      deviceName: "Pixel 7",
      osVersion: "Android 13"
    }
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  null
  ```
</ResponseExample>

## Send Notification to User ⭐

**PRIMARY FUNCTION** - Sends notifications to a user across all their devices via FCM/APNs.

<ParamField path="userId" type="string" required>
  User ID to send notification to
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body text
</ParamField>

<ParamField path="data" type="Record<string, string>">
  Optional data payload for deep linking and custom handling
</ParamField>

<ParamField path="priority" type="'high' | 'normal'">
  Notification priority (default: "normal")
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.shared.notifications.sendNotificationToUser, {
    userId: "user-123",
    title: "🍕 Order Ready!",
    body: "Your order #12345 is ready for pickup",
    data: {
      type: "order_status",
      orderId: "order-12345",
      deepLink: "twigz://orders/order-12345"
    },
    priority: "high"
  });
  ```

  ```javascript JavaScript theme={null}
  const result = await convex.action("shared.notifications.sendNotificationToUser", {
    userId: "user-123",
    title: "🍕 Order Ready!",
    body: "Your order #12345 is ready for pickup",
    data: {
      type: "order_status",
      orderId: "order-12345",
      deepLink: "twigz://orders/order-12345"
    },
    priority: "high"
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"{CONVEX_URL}/api/action",
      json={
          "path": "shared.notifications.sendNotificationToUser",
          "args": {
              "userId": "user-123",
              "title": "🍕 Order Ready!",
              "body": "Your order #12345 is ready for pickup",
              "data": {
                  "type": "order_status",
                  "orderId": "order-12345",
                  "deepLink": "twigz://orders/order-12345"
              },
              "priority": "high"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

  result = response.json()
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true,
    "message": "Sent to 2/2 devices via FCM/APNs"
  }
  ```
</ResponseExample>

## Schedule Notification

Schedule a notification to be sent at a specific future time.

<ParamField path="userId" type="string" required>
  User ID to send to
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body
</ParamField>

<ParamField path="scheduledTime" type="number" required>
  Unix timestamp for when to send
</ParamField>

<ParamField path="data" type="Record<string, string>">
  Optional data payload
</ParamField>

<ParamField path="priority" type="'high' | 'normal'">
  Notification priority
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const notificationId = await convex.mutation(api.shared.notifications.scheduleNotification, {
    userId: "user-123",
    title: "⏰ Appointment Reminder",
    body: "Don't forget your appointment tomorrow at 3 PM",
    scheduledTime: Date.now() + (24 * 60 * 60 * 1000), // 24 hours from now
    data: {
      type: "reminder",
      appointmentId: "apt-456"
    },
    priority: "normal"
  });
  ```

  ```javascript JavaScript theme={null}
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  tomorrow.setHours(15, 0, 0, 0); // 3 PM tomorrow

  const notificationId = await convex.mutation("shared.notifications.scheduleNotification", {
    userId: "user-123",
    title: "⏰ Appointment Reminder",
    body: "Don't forget your appointment tomorrow at 3 PM",
    scheduledTime: tomorrow.getTime(),
    data: {
      type: "reminder",
      appointmentId: "apt-456"
    },
    priority: "normal"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  "sn123456789"
  ```
</ResponseExample>

## Cancel Scheduled Notification

Cancel a pending scheduled notification.

<ParamField path="notificationId" type="Id<'scheduledNotifications'>" required>
  Scheduled notification ID to cancel
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const cancelled = await convex.mutation(api.shared.notifications.cancelScheduledNotification, {
    notificationId: "sn123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  const cancelled = await convex.mutation("shared.notifications.cancelScheduledNotification", {
    notificationId: "sn123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  true
  ```
</ResponseExample>

## Send Bulk Notification

Send notifications to multiple users efficiently with batching.

<ParamField path="userIds" type="Array<string>" required>
  Array of user IDs to send to
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body
</ParamField>

<ParamField path="data" type="Record<string, string>">
  Optional data payload
</ParamField>

<ParamField path="batchSize" type="number">
  Batch size for processing (default: 50)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.shared.notifications.sendBulkNotification, {
    userIds: ["user-1", "user-2", "user-3"],
    title: "🎉 System Update",
    body: "New features are now available in the app!",
    data: {
      type: "announcement",
      updateVersion: "2.1.0"
    },
    batchSize: 100
  });
  ```

  ```javascript JavaScript theme={null}
  const userIds = ["user-1", "user-2", "user-3"]; // Users with registered FCM/APNs tokens

  const result = await convex.action("shared.notifications.sendBulkNotification", {
    userIds,
    title: "🎉 Weekend Special",
    body: "Get 25% off all orders this weekend! Use code WEEKEND25",
    data: {
      type: "promotion", 
      code: "WEEKEND25",
      discount: "25",
      validUntil: "2024-01-07"
    }
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "totalUsers": 3,
    "successfulUsers": 3,
    "failedUsers": 0,
    "batchResults": [
      {
        "userId": "user-1",
        "success": true
      },
      {
        "userId": "user-2", 
        "success": true
      },
      {
        "userId": "user-3",
        "success": true
      }
    ]
  }
  ```
</ResponseExample>

## Get User Device Tokens

Retrieve all active device tokens for a user.

<ParamField path="userId" type="string">
  User ID (defaults to authenticated user)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const tokens = await convex.query(api.shared.notifications.getUserDeviceTokens, {
    userId: "user-123"
  });
  ```

  ```javascript JavaScript theme={null}
  const tokens = await convex.query("shared.notifications.getUserDeviceTokens", {
    userId: "user-123"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  [
    {
      "_id": "dt123456789",
      "deviceToken": "fGHI...xyz",
      "platform": "android",
      "appVersion": "1.0.0",
      "isActive": true,
      "lastUpdated": 1640995200000
    },
    {
      "_id": "dt987654321",
      "deviceToken": "aBcD...123",
      "platform": "ios",
      "appVersion": "1.0.0",
      "isActive": true,
      "lastUpdated": 1640995800000
    }
  ]
  ```
</ResponseExample>

## Send Test Notification

Send a test notification with debugging information.

<ParamField path="userId" type="string" required>
  User ID to send test to
</ParamField>

<ParamField path="title" type="string">
  Custom title (default: "🧪 Test Notification")
</ParamField>

<ParamField path="body" type="string">
  Custom body (default: timestamp)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.shared.notifications.sendTestNotification, {
    userId: "user-123",
    title: "🔥 FCM/APNs Test",
    body: "Testing direct Firebase/Apple notifications!"
  });
  ```

  ```javascript JavaScript theme={null}
  const result = await convex.action("shared.notifications.sendTestNotification", {
    userId: "user-123"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true,
    "message": "Test notification sent successfully",
    "debugInfo": {
      "userHasTokens": true,
      "tokenCount": 2,
      "deliveryMethod": "FCM/APNs",
      "details": {
        "fcmTokens": 1,
        "apnsTokens": 1,
        "sentToFCM": 1,
        "sentToAPNs": 1
      }
    }
  }
  ```
</ResponseExample>

## Debug User Notifications

Debug notification setup for troubleshooting.

<ParamField path="userId" type="string" required>
  User ID to debug
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const debug = await convex.query(api.shared.notifications.debugUserNotifications, {
    userId: "user-123"
  });
  ```

  ```javascript JavaScript theme={null}
  const debug = await convex.query("shared.notifications.debugUserNotifications", {
    userId: "user-123"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "userId": "user-123",
    "hasExpoPushToken": false,
    "hasNativeTokens": true,
    "deviceTokens": [
      {
        "_id": "dt123456789",
        "platform": "android",
        "isActive": true,
        "lastUpdated": 1640995200000
      }
    ],
    "recentNotifications": [
      {
        "_id": "n123456789",
        "title": "Test Notification",
        "sentAt": 1640995200000,
        "status": "delivered"
      }
    ],
    "troubleshooting": [
      "✅ User has active FCM/APNs tokens",
      "✅ Recent notifications delivered successfully",
      "💡 All systems operational"
    ]
  }
  ```
</ResponseExample>

## Get Notification Statistics

Retrieve comprehensive notification statistics.

<ParamField path="startDate" type="number">
  Filter start date (Unix timestamp)
</ParamField>

<ParamField path="endDate" type="number">
  Filter end date (Unix timestamp)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const stats = await convex.query(api.shared.notifications.getNotificationStats, {
    startDate: Date.now() - (7 * 24 * 60 * 60 * 1000), // Last 7 days
    endDate: Date.now()
  });
  ```

  ```javascript JavaScript theme={null}
  const stats = await convex.query("shared.notifications.getNotificationStats", {
    startDate: Date.now() - (7 * 24 * 60 * 60 * 1000), // Last 7 days
    endDate: Date.now()
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "totalScheduled": 150,
    "totalSent": 142,
    "totalFailed": 8,
    "totalPending": 0,
    "deviceTokenCount": {
      "ios": 45,
      "android": 67,
      "total": 112
    }
  }
  ```
</ResponseExample>

## Notification Types

The system supports various notification types:

<CardGroup cols={2}>
  <Card title="Order Status" icon="shopping-cart">
    Order updates, confirmations, and delivery notifications
  </Card>

  <Card title="Payment" icon="credit-card">
    Payment confirmations, failures, and refund notifications
  </Card>

  <Card title="Delivery" icon="truck">
    Delivery tracking, ETA updates, and completion notifications
  </Card>

  <Card title="Promotions" icon="tag">
    Marketing campaigns, discounts, and special offers
  </Card>

  <Card title="Announcements" icon="bullhorn">
    System updates, maintenance notices, and feature releases
  </Card>

  <Card title="Store Status" icon="store">
    Store approval, rejection, and operational status changes
  </Card>
</CardGroup>

## Complete Integration Example

Here's a complete notification integration:

<CodeGroup>
  ```typescript React Hook theme={null}
  import { useEffect } from 'react';
  import * as Notifications from 'expo-notifications';
  import { Platform } from 'react-native';
  import { useMutation, useAction } from 'convex/react';
  import { api } from './convex/_generated/api';

  function useNotifications(userId: string) {
    const recordToken = useMutation(api.shared.notifications.recordNativeDeviceToken);
    const sendTest = useAction(api.shared.notifications.sendTestNotification);

    useEffect(() => {
      setupNotifications();
    }, [userId]);

    const setupNotifications = async () => {
      try {
        // Request permissions
        const { status } = await Notifications.requestPermissionsAsync();
        if (status !== 'granted') {
          console.warn('Notification permissions not granted');
          return;
        }

        // Get native device token (FCM/APNs)
        const tokenData = await Notifications.getDevicePushTokenAsync();
        
        // Register with backend
        await recordToken({
          deviceToken: tokenData.data,
          platform: Platform.OS === 'ios' ? 'ios' : 'android',
          appVersion: '1.0.0',
          deviceInfo: {
            deviceId: 'unique-device-id',
            deviceName: Platform.OS === 'ios' ? 'iPhone' : 'Android Device',
            osVersion: Platform.Version.toString()
          }
        });

        console.log('✅ Notification token registered successfully');

      } catch (error) {
        console.error('❌ Failed to setup notifications:', error);
      }
    };

    const testNotification = async () => {
      try {
        const result = await sendTest({
          userId,
          title: '🧪 Test from App',
          body: 'This is a test notification!'
        });
        console.log('Test result:', result);
      } catch (error) {
        console.error('Test failed:', error);
      }
    };

    // Handle received notifications
    useEffect(() => {
      const subscription = Notifications.addNotificationReceivedListener(notification => {
        console.log('📱 Notification received:', notification);
        
        // Handle notification data
        const data = notification.request.content.data;
        if (data?.type === 'order_status' && data?.orderId) {
          // Navigate to order details
          // navigation.navigate('Order', { orderId: data.orderId });
        }
      });

      return () => subscription.remove();
    }, []);

    return { testNotification };
  }

  export default useNotifications;
  ```

  ```typescript Vue Composition API theme={null}
  import { ref, onMounted } from 'vue';
  import { useConvexMutation, useConvexAction } from '@convex-vue/core';

  export function useNotifications(userId: string) {
    const isSetup = ref(false);
    const recordToken = useConvexMutation('shared.notifications.recordNativeDeviceToken');
    const sendTest = useConvexAction('shared.notifications.sendTestNotification');

    onMounted(() => {
      setupNotifications();
    });

    const setupNotifications = async () => {
      try {
        // Your platform-specific token retrieval logic
        const deviceToken = await getDeviceToken();
        
        await recordToken({
          deviceToken,
          platform: 'android', // or 'ios'
          appVersion: '1.0.0',
          deviceInfo: {
            deviceId: 'unique-device-id',
            deviceName: 'Android Device',
            osVersion: 'Android 13'
          }
        });

        isSetup.value = true;
        console.log('✅ Notification setup complete');

      } catch (error) {
        console.error('❌ Notification setup failed:', error);
      }
    };

    const testNotification = async () => {
      const result = await sendTest({
        userId,
        title: '🧪 Vue Test',
        body: 'Testing from Vue app!'
      });
      return result;
    };

    return {
      isSetup,
      testNotification
    };
  }
  ```
</CodeGroup>

## Environment Setup

To use FCM/APNs notifications, configure these environment variables:

<AccordionGroup>
  <Accordion title="Firebase (FCM) Setup">
    Required environment variables:

    ```bash theme={null}
    FCM_SERVICE_ACCOUNT={"type":"service_account",...} # Firebase service account JSON
    FCM_PROJECT_ID=your-firebase-project-id
    ```

    Setup steps:

    1. Go to Firebase Console → Project Settings
    2. Generate a new private key for service account
    3. Copy the JSON content to `FCM_SERVICE_ACCOUNT`
    4. Set your project ID in `FCM_PROJECT_ID`
  </Accordion>

  <Accordion title="Apple (APNs) Setup">
    Required environment variables:

    ```bash theme={null}
    APNS_KEY_ID=ABC123DEF4 # APNs key ID
    APNS_TEAM_ID=DEF456GHI7 # Apple Team ID  
    APNS_PRIVATE_KEY=-----BEGIN PRIVATE KEY----- # APNs private key (.p8 file content)
    IOS_BUNDLE_ID=com.yourapp.bundleid # iOS app bundle identifier
    APNS_ENVIRONMENT=development # or "production"
    ```

    Setup steps:

    1. Go to Apple Developer → Keys
    2. Create a new APNs key
    3. Download the .p8 file and copy content to `APNS_PRIVATE_KEY`
    4. Set the key ID, team ID, and bundle ID
  </Accordion>
</AccordionGroup>

## Error Handling

<AccordionGroup>
  <Accordion title="Token registration failed">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Invalid device token format",
      "platform": "android",
      "tokenLength": 152
    }
    ```
  </Accordion>

  <Accordion title="User has no tokens">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "No active device tokens found for user",
      "userId": "user-123"
    }
    ```
  </Accordion>

  <Accordion title="FCM/APNs delivery failed">
    **Status Code**: `500`

    ```json theme={null}
    {
      "error": "Notification delivery failed",
      "details": {
        "fcmErrors": ["Invalid token"],
        "apnsErrors": ["Device token expired"]
      }
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  This notification system is production-ready and handles millions of notifications daily. It includes automatic retry logic, token validation, and comprehensive error handling.
</Note>

<Tip>
  Always test notifications in both development and production environments, as token formats and delivery behavior can differ between environments.
</Tip>

***

## Debug & Testing

These functions live in `convex/shared/notificationDebug.ts` and `convex/shared/notificationActions.ts`. They provide low-level debugging, direct platform testing, and internal send capabilities for the notification system.

### debugNotificationSystem

**Type:** `query`

Comprehensive health check for a user's notification setup. Returns device tokens, recent notification history, environment configuration status, and actionable recommendations.

<ParamField path="userId" type="string" required>
  The user ID to inspect
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const debug = await convex.query(api.shared.notificationDebug.debugNotificationSystem, {
    userId: "user-123"
  });

  console.log("Tokens:", debug.deviceTokenCount);
  console.log("FCM configured:", debug.environmentCheck.hasFCMConfig);
  console.log("APNs configured:", debug.environmentCheck.hasAPNSConfig);
  console.log("Recommendations:", debug.recommendations);
  ```

  ```javascript JavaScript theme={null}
  const debug = await convex.query("shared.notificationDebug.debugNotificationSystem", {
    userId: "user-123"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "userId": "user-123",
    "hasDeviceTokens": true,
    "deviceTokenCount": 2,
    "tokens": [
      {
        "deviceToken": "fGHIjklmnop1234567890abcde...",
        "platform": "android",
        "isActive": true,
        "lastUpdated": 1640995200000,
        "deviceId": "device-abc",
        "failedDeliveryCount": 0
      }
    ],
    "recentNotifications": [
      {
        "title": "Order Ready!",
        "status": "sent",
        "createdAt": 1640995200000
      }
    ],
    "environmentCheck": {
      "hasFCMConfig": true,
      "hasAPNSConfig": true
    },
    "recommendations": [
      "Found 2 active device token(s)"
    ]
  }
  ```
</ResponseExample>

### testSendNotification

**Type:** `action` (requires authentication)

Sends a test notification to the currently authenticated user across all their registered devices. Useful for verifying end-to-end notification delivery without needing to specify a user ID.

<Note>
  This function requires authentication. It automatically sends to the calling user's devices.
</Note>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.shared.notificationDebug.testSendNotification, {});

  if (result.success) {
    console.log("Test notification sent!", result.message);
  } else {
    console.error("Failed:", result.message, result.details);
  }
  ```

  ```javascript JavaScript theme={null}
  const result = await convex.action("shared.notificationDebug.testSendNotification", {});
  ```
</CodeGroup>

<ResponseExample>
  ```json Success Response theme={null}
  {
    "success": true,
    "message": "Sent to 2/2 devices via FCM/APNs",
    "details": {
      "sentCount": 2,
      "failedCount": 0,
      "platforms": [
        { "platform": "android", "success": true },
        { "platform": "ios", "success": true }
      ]
    }
  }
  ```

  ```json No Tokens Response theme={null}
  {
    "success": false,
    "message": "No device tokens registered. Please register a device token first using recordNativeDeviceToken()",
    "details": {
      "hasAnyTokens": false,
      "hasExpoPushToken": false,
      "hasNativeTokens": false,
      "tokenCount": 0,
      "deliveryMethod": "none",
      "platforms": []
    }
  }
  ```
</ResponseExample>

### testFCMDirect

**Type:** `action` (requires authentication)

Manually test FCM delivery by sending a notification to a specific FCM device token. Bypasses user lookup and sends directly to the provided token. Useful for isolating whether an FCM token is valid and reachable.

<ParamField path="fcmToken" type="string" required>
  The FCM device token to send to
</ParamField>

<ParamField path="title" type="string">
  Custom notification title (default: "Direct FCM Test")
</ParamField>

<ParamField path="body" type="string">
  Custom notification body (default: "This is a direct FCM test notification")
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await convex.action(api.shared.notificationDebug.testFCMDirect, {
    fcmToken: "dGVzdC10b2tlbi0xMjM0NTY3ODkw...",
    title: "Direct Token Test",
    body: "Testing this specific FCM token"
  });

  console.log(result.success ? "Token is valid!" : "Token failed:", result.message);
  ```

  ```javascript JavaScript theme={null}
  const result = await convex.action("shared.notificationDebug.testFCMDirect", {
    fcmToken: "dGVzdC10b2tlbi0xMjM0NTY3ODkw..."
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Success Response theme={null}
  {
    "success": true,
    "message": "FCM notification sent successfully",
    "details": {
      "success": true,
      "results": [
        {
          "deviceToken": "dGVzdC10b2tlbi0xMjM0NTY3ODkw...",
          "success": true
        }
      ]
    }
  }
  ```

  ```json Failure Response theme={null}
  {
    "success": false,
    "message": "FCM notification failed",
    "details": {
      "success": false,
      "results": [
        {
          "deviceToken": "dGVzdC10b2tlbi0xMjM0NTY3ODkw...",
          "success": false,
          "error": "Requested entity was not found."
        }
      ]
    }
  }
  ```
</ResponseExample>

### debugNotificationSystemInternal

**Type:** `query` (internal)

<Warning>
  This is an internal function intended for use by other server-side actions. It is not directly callable from client code.
</Warning>

Lightweight internal version of `debugNotificationSystem`. Returns a minimal summary of a user's device token status for use in server-side logic and other actions.

<ParamField path="userId" type="string" required>
  The user ID to check
</ParamField>

<ResponseExample>
  ```json Response theme={null}
  {
    "userId": "user-123",
    "hasDeviceTokens": true,
    "deviceTokenCount": 2
  }
  ```
</ResponseExample>

### sendFCMNotificationDirect

**Type:** `internalAction` (Node.js runtime)

<Warning>
  This is an internal action that runs in the Node.js runtime. It is not directly callable from client code. It is used internally by `testFCMDirect` and the main notification dispatch system.
</Warning>

Sends push notifications to one or more Android devices via the Firebase Cloud Messaging (FCM) v1 API. Handles OAuth 2.0 token exchange using the configured service account and sends to each device token individually.

**Requires environment variables:** `FCM_SERVICE_ACCOUNT`, `FCM_PROJECT_ID`

<ParamField path="deviceTokens" type="Array<string>" required>
  Array of FCM device tokens to send to
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body text
</ParamField>

<ParamField path="data" type="Record<string, string>">
  Optional key-value data payload
</ParamField>

<ParamField path="priority" type="'high' | 'normal'">
  FCM delivery priority
</ParamField>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true,
    "results": [
      {
        "deviceToken": "fGHI...xyz",
        "success": true
      },
      {
        "deviceToken": "aBcD...123",
        "success": false,
        "error": "Requested entity was not found."
      }
    ]
  }
  ```
</ResponseExample>

### sendAPNsNotificationDirect

**Type:** `internalAction` (Node.js runtime)

<Warning>
  This is an internal action that runs in the Node.js runtime. It is not directly callable from client code. It is used internally by the main notification dispatch system for iOS delivery.
</Warning>

Sends push notifications to one or more iOS devices via the Apple Push Notification service (APNs). Generates a JWT for APNs authentication and sends to each device token individually. Automatically selects the sandbox or production APNs endpoint based on the `APNS_ENVIRONMENT` variable.

**Requires environment variables:** `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_PRIVATE_KEY`, `IOS_BUNDLE_ID`, `APNS_ENVIRONMENT`

<ParamField path="deviceTokens" type="Array<string>" required>
  Array of APNs device tokens to send to
</ParamField>

<ParamField path="title" type="string" required>
  Notification title
</ParamField>

<ParamField path="body" type="string" required>
  Notification body text
</ParamField>

<ParamField path="data" type="Record<string, string>">
  Optional key-value data payload (merged into the APNs payload alongside `aps`)
</ParamField>

<ParamField path="badge" type="number">
  App badge count to display
</ParamField>

<ParamField path="sound" type="string">
  Notification sound name (default: "default")
</ParamField>

<ResponseExample>
  ```json Response theme={null}
  {
    "success": true,
    "results": [
      {
        "deviceToken": "a1b2c3d4e5f6...",
        "success": true
      },
      {
        "deviceToken": "f6e5d4c3b2a1...",
        "success": false,
        "error": "BadDeviceToken"
      }
    ]
  }
  ```
</ResponseExample>
