Skip to content

Device Management

Complete guide to registering and managing FCM device tokens for push notifications in the Reserva platform.


Overview

The device management system enables push notification delivery by managing FCM (Firebase Cloud Messaging) tokens across user devices.

Frontend Integration Guide

For complete Firebase setup, service worker configuration, and frontend implementation examples, see Push Notification Integration.

Key Features:

  • Device Registration - Register FCM tokens for push notification delivery
  • Multi-Device Support - Same user can have multiple registered devices
  • Automatic Deduplication - Upsert handling prevents duplicate token entries
  • Cross-Platform - Supports web, Android, and iOS devices
  • Token Refresh - Update tokens when Firebase refreshes them
  • Device Lifecycle - Activate, deactivate, or remove devices

Key Concepts:

  • FCM Token = Unique device identifier from Firebase for push delivery
  • Device = A registered FCM token with metadata (platform, name, etc.)
  • User Type = Either staff (admin portal) or customer (customer portal)
  • Active Device = Device enabled to receive push notifications

Subscription Plan Limits

No Device Limits

Device registration has no subscription plan limits. All plans (FREE, PRO, ENTERPRISE) can register unlimited devices.

Feature FREE PRO ENTERPRISE
Devices per User Unlimited Unlimited Unlimited
Push Notifications Unlimited Unlimited Unlimited

Notes:

  • Push notifications are always unlimited on all plans
  • Device registration is available for both staff and customer users
  • No monthly quota on device registrations

Device Platforms

Platform Value Description
Web web Browser-based (Chrome, Firefox, Edge, Safari)
Android android Android mobile app
iOS ios iOS mobile app
Unknown unknown Platform not specified

Register Device (Staff)

Register a new FCM device token for push notifications. Used by staff users in the admin portal.

Endpoint

POST /api/v1/devices/register

Authentication: Required (Staff JWT token)

Access: Any authenticated staff user

Request Body

{
  "fcm_token": "dGhpc19pc19hX3NhbXBsZV9mY21fdG9rZW4...",
  "platform": "web",
  "device_name": "Chrome on Windows",
  "app_version": "1.0.0",
  "device_info": {
    "browser": "Chrome",
    "os": "Windows 10",
    "screen": "1920x1080"
  }
}

Parameters:

Parameter Type Required Description
fcm_token string Yes Firebase Cloud Messaging token (100-4096 chars)
platform string No Device platform: web, android, ios (default: web)
device_name string No Human-readable device name (max 200 chars)
app_version string No Application version (max 20 chars)
device_info object No Additional device metadata (browser, OS, etc.)

Response

{
  "id": "507f1f77bcf86cd799439011",
  "is_new": true,
  "message": "Device registered successfully"
}

Response Fields:

Field Description
id Device record ID in database
is_new true if new device created, false if existing updated
message Status message

Behavior

The endpoint uses upsert logic based on the FCM token:

Scenario Behavior
Token is new Creates new device record
Token exists for same user Updates device metadata
Token exists for different user Updates ownership to current user

Important Notes:

  • FCM token must be between 100-4096 characters
  • Re-registering resets consecutive_failures counter to 0
  • Device is automatically set to is_active: true on registration

Register Device (Customer)

Register a new FCM device token for customer users in the customer portal.

Endpoint

POST /api/v1/devices/register/customer

Authentication: Required (Customer JWT token)

Access: Authenticated customers only

Request Body

Same as Register Device (Staff).

Response

Same as Register Device (Staff).


List User's Devices (Staff)

Retrieve all registered devices for the current staff user.

Endpoint

GET /api/v1/devices

Authentication: Required (Staff JWT token)

Query Parameters

Parameter Type Default Description
include_inactive boolean false Include inactive devices in response

Response

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439011",
      "platform": "web",
      "device_name": "Chrome on Windows",
      "app_version": "1.0.0",
      "is_active": true,
      "last_used_at": "2025-01-15T10:30:00Z",
      "notification_count": 45,
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-15T10:30:00Z"
    },
    {
      "id": "507f1f77bcf86cd799439012",
      "platform": "android",
      "device_name": "Samsung Galaxy S21",
      "app_version": "1.0.0",
      "is_active": true,
      "last_used_at": "2025-01-14T08:15:00Z",
      "notification_count": 23,
      "created_at": "2025-01-05T00:00:00Z",
      "updated_at": "2025-01-14T08:15:00Z"
    }
  ],
  "total": 2
}

Response Fields:

Field Description
id Device record ID
platform Device platform (web, android, ios, unknown)
device_name Human-readable device name
app_version Application version
is_active Whether device is active for notifications
last_used_at Last notification sent to this device
notification_count Total notifications sent to this device
created_at When device was first registered
updated_at Last update timestamp

Note: FCM token is intentionally excluded from the response for security.


List User's Devices (Customer)

Retrieve all registered devices for the current customer user.

Endpoint

GET /api/v1/devices/customer

Authentication: Required (Customer JWT token)

Query Parameters

Same as List User's Devices (Staff).

Response

Same as List User's Devices (Staff).


Update Device (Staff)

Update device information such as refreshed FCM token, device name, or active status.

Endpoint

PUT /api/v1/devices/{device_id}

Authentication: Required (Staff JWT token)

Path Parameters

Parameter Type Description
device_id string Device ID to update (MongoDB ObjectId)

Request Body

{
  "fcm_token": "new_refreshed_token...",
  "device_name": "Chrome on macOS",
  "app_version": "1.1.0",
  "is_active": true
}

Parameters:

Parameter Type Required Description
fcm_token string No Updated FCM token (100-4096 chars)
device_name string No Updated device name (max 200 chars)
app_version string No Updated app version (max 20 chars)
is_active boolean No Enable/disable device for notifications

All fields are optional. Only provided fields are updated.

Response

{
  "id": "507f1f77bcf86cd799439011",
  "platform": "web",
  "device_name": "Chrome on macOS",
  "app_version": "1.1.0",
  "is_active": true,
  "last_used_at": "2025-01-15T10:30:00Z",
  "notification_count": 45,
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-15T11:00:00Z"
}

Token Refresh Handling

When updating the FCM token:

  1. System checks if new token exists on another device
  2. If found, the conflicting device record is deleted (token moved)
  3. token_refreshed_at is recorded
  4. consecutive_failures is reset to 0

Important: Users can only update their own devices. Attempting to update another user's device returns 403 Forbidden.


Update Device (Customer)

Update device information for customer users.

Endpoint

PUT /api/v1/devices/customer/{device_id}

Authentication: Required (Customer JWT token)

Path Parameters

Same as Update Device (Staff).

Request Body

Same as Update Device (Staff).

Response

Same as Update Device (Staff).


Unregister Device (Staff)

Remove a device (delete FCM token) to stop receiving push notifications.

Endpoint

DELETE /api/v1/devices/{device_id}

Authentication: Required (Staff JWT token)

Path Parameters

Parameter Type Description
device_id string Device ID to unregister (MongoDB ObjectId)

Response

{
  "success": true,
  "message": "Device unregistered successfully"
}

Important:

  • Users can only delete their own devices
  • This is a hard delete - the device record is permanently removed
  • The device will no longer receive push notifications
  • Attempting to delete another user's device returns 403 Forbidden

Unregister Device (Customer)

Remove a device for customer users.

Endpoint

DELETE /api/v1/devices/customer/{device_id}

Authentication: Required (Customer JWT token)

Path Parameters

Same as Unregister Device (Staff).

Response

Same as Unregister Device (Staff).


Device Lifecycle

Registration Flow

flowchart TD
    A[User logs in] --> B[Request notification permission]
    B --> C{Permission granted?}
    C -->|No| D[Cannot register device]
    C -->|Yes| E[Generate FCM token]
    E --> F[Call /devices/register]
    F --> G{Token exists?}
    G -->|No| H[Create new device]
    G -->|Yes| I[Update existing device]
    H --> J[Device registered]
    I --> J
    J --> K[Ready for push notifications]

Token Refresh Flow

flowchart TD
    A[Firebase refreshes token] --> B[Detect token change]
    B --> C[Call PUT /devices/{id}]
    C --> D{Token conflicts?}
    D -->|No| E[Update token]
    D -->|Yes| F[Delete conflicting device]
    F --> E
    E --> G[Reset failure counter]
    G --> H[Device updated]

Deactivation Flow

Devices can become inactive in several ways:

Trigger Action
User manually disables PUT /devices/{id} with is_active: false
User logs out DELETE /devices/{id}
Consecutive failures System marks device inactive after multiple failures
Token expiration Firebase invalidates token, push fails

Error Handling

Common Errors

Error Status Cause Solution
Authentication required 401 Missing or invalid JWT Include valid Bearer token
User has no tenant access 403 Staff user without tenant Assign user to a tenant
Device not found 404 Invalid device_id Use valid device ID from list endpoint
Cannot delete another user's device 403 Device ownership mismatch Users can only manage their own devices
FCM token cannot be empty 422 Empty or whitespace-only token Provide valid FCM token
Invalid ObjectId format 422 Malformed device_id Use valid MongoDB ObjectId format

Error Response Format

{
  "detail": "Error message describing the issue"
}

Best Practices

For Device Registration

DO:

  • Register device immediately after user grants notification permission
  • Store device ID locally for future updates/unregistration
  • Include meaningful device_name for user identification
  • Track app_version for debugging and analytics
  • Re-register device on app launch to refresh token

DON'T:

  • Register devices without notification permission
  • Store FCM tokens in insecure locations
  • Skip token refresh handling
  • Assume tokens never change

For Token Management

DO:

  • Listen for Firebase token refresh events
  • Update token via PUT /devices/{id} when it changes
  • Handle token conflicts gracefully (let backend resolve)
  • Clear local token data on logout

DON'T:

  • Ignore token refresh callbacks from Firebase
  • Create duplicate device records for token changes
  • Keep stale tokens registered

For User Logout

DO:

  • Call DELETE /devices/{id} before clearing auth tokens
  • Clear local FCM token storage
  • Unregister service worker if appropriate

DON'T:

  • Leave devices registered after logout
  • Skip device cleanup on account deletion

Frontend Integration

JavaScript Example

class DeviceManager {
    constructor(apiBaseUrl, getAuthToken) {
        this.apiBaseUrl = apiBaseUrl;
        this.getAuthToken = getAuthToken;
        this.deviceId = localStorage.getItem('device_id');
    }

    async register(fcmToken, isCustomer = false) {
        const endpoint = isCustomer
            ? '/api/v1/devices/register/customer'
            : '/api/v1/devices/register';

        const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${await this.getAuthToken()}`
            },
            body: JSON.stringify({
                fcm_token: fcmToken,
                platform: 'web',
                device_name: this.getDeviceName(),
                app_version: '1.0.0'
            })
        });

        if (!response.ok) {
            throw new Error('Device registration failed');
        }

        const data = await response.json();
        this.deviceId = data.id;
        localStorage.setItem('device_id', data.id);
        localStorage.setItem('fcm_token', fcmToken);

        return data;
    }

    async updateToken(newToken, isCustomer = false) {
        if (!this.deviceId) {
            return this.register(newToken, isCustomer);
        }

        const endpoint = isCustomer
            ? `/api/v1/devices/customer/${this.deviceId}`
            : `/api/v1/devices/${this.deviceId}`;

        const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${await this.getAuthToken()}`
            },
            body: JSON.stringify({ fcm_token: newToken })
        });

        if (!response.ok) {
            // Device not found, re-register
            if (response.status === 404) {
                return this.register(newToken, isCustomer);
            }
            throw new Error('Token update failed');
        }

        localStorage.setItem('fcm_token', newToken);
        return response.json();
    }

    async unregister(isCustomer = false) {
        if (!this.deviceId) return;

        const endpoint = isCustomer
            ? `/api/v1/devices/customer/${this.deviceId}`
            : `/api/v1/devices/${this.deviceId}`;

        await fetch(`${this.apiBaseUrl}${endpoint}`, {
            method: 'DELETE',
            headers: {
                'Authorization': `Bearer ${await this.getAuthToken()}`
            }
        });

        localStorage.removeItem('device_id');
        localStorage.removeItem('fcm_token');
        this.deviceId = null;
    }

    async listDevices(isCustomer = false, includeInactive = false) {
        const endpoint = isCustomer
            ? '/api/v1/devices/customer'
            : '/api/v1/devices';

        const response = await fetch(
            `${this.apiBaseUrl}${endpoint}?include_inactive=${includeInactive}`,
            {
                headers: {
                    'Authorization': `Bearer ${await this.getAuthToken()}`
                }
            }
        );

        return response.json();
    }

    getDeviceName() {
        const ua = navigator.userAgent;
        const browser = this.getBrowser(ua);
        const os = this.getOS(ua);
        return `${browser} on ${os}`;
    }

    getBrowser(ua) {
        if (ua.includes('Chrome')) return 'Chrome';
        if (ua.includes('Firefox')) return 'Firefox';
        if (ua.includes('Safari')) return 'Safari';
        if (ua.includes('Edge')) return 'Edge';
        return 'Browser';
    }

    getOS(ua) {
        if (ua.includes('Windows')) return 'Windows';
        if (ua.includes('Mac')) return 'macOS';
        if (ua.includes('Linux')) return 'Linux';
        if (ua.includes('Android')) return 'Android';
        if (ua.includes('iOS')) return 'iOS';
        return 'Unknown';
    }
}

React Hook Example

import { useState, useCallback, useEffect } from 'react';
import { useAuth } from './useAuth';

export function useDevices() {
    const { token, isCustomer } = useAuth();
    const [devices, setDevices] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    const fetchDevices = useCallback(async (includeInactive = false) => {
        if (!token) return;

        setLoading(true);
        setError(null);

        try {
            const endpoint = isCustomer
                ? '/api/v1/devices/customer'
                : '/api/v1/devices';

            const response = await fetch(
                `${API_URL}${endpoint}?include_inactive=${includeInactive}`,
                { headers: { 'Authorization': `Bearer ${token}` } }
            );

            const data = await response.json();
            setDevices(data.items);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    }, [token, isCustomer]);

    const removeDevice = useCallback(async (deviceId) => {
        const endpoint = isCustomer
            ? `/api/v1/devices/customer/${deviceId}`
            : `/api/v1/devices/${deviceId}`;

        await fetch(`${API_URL}${endpoint}`, {
            method: 'DELETE',
            headers: { 'Authorization': `Bearer ${token}` }
        });

        setDevices(prev => prev.filter(d => d.id !== deviceId));
    }, [token, isCustomer]);

    useEffect(() => {
        fetchDevices();
    }, [fetchDevices]);

    return {
        devices,
        loading,
        error,
        fetchDevices,
        removeDevice
    };
}

API Reference Summary

Endpoint Method Auth Purpose
/devices/register POST Staff JWT Register FCM token (staff)
/devices/register/customer POST Customer JWT Register FCM token (customer)
/devices GET Staff JWT List staff's devices
/devices/customer GET Customer JWT List customer's devices
/devices/{id} PUT Staff JWT Update device (staff)
/devices/customer/{id} PUT Customer JWT Update device (customer)
/devices/{id} DELETE Staff JWT Unregister device (staff)
/devices/customer/{id} DELETE Customer JWT Unregister device (customer)

Document Description
Push Notification Integration Frontend FCM setup and integration guide
Notification Management Sending and scheduling notifications
Notification Settings Customize notification templates and preferences
Customer Authentication Customer login and JWT token handling
Staff Authentication Staff login and JWT token handling

Next Steps:

  1. Set up Firebase in your frontend app (see Push Notification Integration)
  2. Request notification permission from user
  3. Generate FCM token using Firebase SDK
  4. Register device: POST /devices/register
  5. Handle token refresh events
  6. Unregister device on logout: DELETE /devices/{id}