FCM - Device Token Management
๐ Overviewโ
FCM device token management handles registration and storage of Firebase Cloud Messaging tokens for web browsers and mobile devices. Each user can have multiple active tokens (supporting concurrent sessions across devices), and tokens are stored in MongoDB for notification delivery.
Source Files:
- Controller:
external/Integrations/FCM/Controller/fcm.js(saveWebToken) - Model:
external/Integrations/FCM/Model/fcm.js - Routes:
external/Integrations/FCM/Routes/fcm.js
External API: Firebase Admin SDK (server-side, no direct Firebase API calls for token registration)
๐๏ธ Collections Usedโ
fcm.tokensโ
- Operations: Create, Read, Update
- Model:
shared/models/fcm-token.js - Usage Context: Store and retrieve FCM tokens for notification delivery
Document Structure:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"account_id": ObjectId("507f1f77bcf86cd799439012"),
"user_id": ObjectId("507f1f77bcf86cd799439013"),
"web_token": [
"fcm_token_abc123...",
"fcm_token_xyz789..." // Supports multiple concurrent sessions
],
"createdAt": ISODate("2023-10-01T12:00:00Z"),
"updatedAt": ISODate("2023-10-05T14:30:00Z")
}
๐ Data Flowโ
Token Registration Flowโ
sequenceDiagram
participant Browser as Web Browser
participant FCMClient as FCM SDK (Client)
participant API as FCM Controller
participant Model as FCM Model
participant DB as MongoDB (fcm.tokens)
Browser->>FCMClient: Request notification permission
FCMClient->>FCMClient: Generate FCM token
FCMClient-->>Browser: Return token string
Browser->>API: POST /v1/e/fcm/web {token}
API->>API: Extract user_id from JWT
API->>Model: findFcm(user_id, account_id)
Model->>DB: findOne query
alt Token document exists
DB-->>Model: Return existing document
Model-->>API: Return document with _id
API->>Model: updateToken(_id, token, type)
Model->>DB: $addToSet token to web_token array
else No document exists
DB-->>Model: Return null
Model-->>API: Return null
API->>Model: saveToken(user_id, account_id, type, token)
Model->>DB: Create new document with token array
end
DB-->>API: Success
API-->>Browser: {success: true, message: "SUCCESS"}
๐ง Business Logic & Functionsโ
Controller Functionsโ
saveWebToken(req, res, next)โ
Purpose: Register FCM device token for authenticated user
Source: Controller/fcm.js
External API Endpoint: N/A (database operation only)
Parameters:
req.body.token(String) - FCM device token from client SDKreq.body.type(String, optional) - Token type (default:web_token)req.auth.account_id(ObjectId) - Account ID from JWTreq.auth.uid(ObjectId) - User ID from JWT
Returns: JSON response with success status
{
"success": true,
"message": "SUCCESS"
}
Business Logic Flow:
-
Extract Authentication Context
- Get account_id and uid from JWT token
- Convert to strings for MongoDB query
-
Validate Token Parameter
- Check if token provided in request body
- Return 400 error if missing
-
Check Existing Tokens
- Query database for existing token document
- Search by user_id and account_id
-
Update or Create Token Document
- If exists: Add token to existing web_token array
- If not exists: Create new document with token
-
Return Success Response
- Confirm token registration
Request Example:
POST /v1/e/fcm/web
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"token": "fcm_token_abc123def456...",
"type": "web_token"
}
Success Response:
{
"success": true,
"message": "SUCCESS"
}
Error Response (Missing Token):
{
"success": false,
"errno": 400,
"message": "Token not provided. (required)"
}
Error Handling:
- 400 Bad Request: Token not provided in request body
- Database Errors: Passed to error middleware via
next(error) - Duplicate Tokens: Handled by
$addToSet(no duplicates added)
Example Usage:
// Client-side (browser)
import { getMessaging, getToken } from 'firebase/messaging';
const messaging = getMessaging();
const token = await getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' });
// Register token with backend
await fetch('/v1/e/fcm/web', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
Side Effects:
- โ ๏ธ Database Write: Creates or updates token document
- โ ๏ธ Array Addition: Token added to web_token array (no duplicates)
- โน๏ธ No Firebase API Call: Server-side registration only
Model Functionsโ
findFcm(userID, accountID)โ
Purpose: Find existing FCM token document for user
Source: Model/fcm.js
Parameters:
userID(String) - User ID to search foraccountID(String) - Account ID to search for
Returns: Promise<Object|null> - Token document or null
{
"_id": ObjectId,
"account_id": ObjectId,
"user_id": ObjectId,
"web_token": ["token1", "token2"],
"createdAt": ISODate,
"updatedAt": ISODate
}
Business Logic Flow:
-
Query MongoDB
- Find document matching account_id and user_id
- Use
.lean()for plain JavaScript object - Execute query
-
Return Result
- Return document if found
- Return null if not found
Example Usage:
const existingTokens = await fcmTokenModal.findFcm('user_123', 'account_456');
if (existingTokens) {
// Update existing document
} else {
// Create new document
}
Side Effects:
- โน๏ธ Database Read: Queries fcm.tokens collection
saveToken(userID, accountID, type, token)โ
Purpose: Create new FCM token document
Source: Model/fcm.js
Parameters:
userID(String) - User IDaccountID(String) - Account IDtype(String) - Token type field name (default:web_token)token(String) - FCM token string
Returns: Promise<Boolean> - true on success
Business Logic Flow:
-
Build Document Data
- Create object with user_id, account_id
- Add token to array using dynamic field name (type)
-
Create MongoDB Document
- Instantiate new FcmToken model
- Save to database
-
Return Success
- Resolve with true
Document Structure Created:
{
"user_id": "user_123",
"account_id": "account_456",
"web_token": ["fcm_token_abc123..."]
}
Example Usage:
await fcmTokenModal.saveToken('user_123', 'account_456', 'web_token', 'fcm_token_abc123...');
// New document created in fcm.tokens
Side Effects:
- โ ๏ธ Database Write: Creates new document in fcm.tokens
updateToken(ID, token, type)โ
Purpose: Add token to existing document's token array
Source: Model/fcm.js
Parameters:
ID(String) - MongoDB document _idtoken(String) - FCM token to addtype(String) - Token type field name (default:web_token)
Returns: Promise<Boolean> - true on success
Business Logic Flow:
-
Update Document
- Use
$addToSetto add token to array - Prevents duplicate tokens
- Update by _id
- Use
-
Return Success
- Resolve with true
MongoDB Operation:
// Updates web_token array
db.fcm.tokens.updateOne({ _id: ObjectId('...') }, { $addToSet: { web_token: ['fcm_token_new'] } });
Example Usage:
await fcmTokenModal.updateToken('507f1f77bcf86cd799439011', 'fcm_token_new', 'web_token');
// Token added to existing web_token array (if not already present)
Side Effects:
- โ ๏ธ Database Update: Modifies existing document
- โน๏ธ Duplicate Prevention:
$addToSetensures no duplicate tokens
findAll(userIDs, accountID)โ
Purpose: Find all FCM tokens for multiple users (used for notification sending)
Source: Model/fcm.js
Parameters:
userIDs(Array<String>) - Array of user IDsaccountID(String) - Account ID to filter by
Returns: Promise<Array<Object>> - Array of token documents
[
{
_id: ObjectId,
account_id: ObjectId,
user_id: ObjectId,
web_token: ['token1', 'token2'],
},
// ... more documents
];
Business Logic Flow:
-
Query Multiple Users
- Find documents where user_id in userIDs array
- Filter by account_id
- Use
.lean()for plain objects
-
Return All Matching Documents
- Return array of token documents
Example Usage:
const userTokens = await fcmTokenModal.findAll(['user_123', 'user_456', 'user_789'], 'account_abc');
// Extract all tokens for notification
let allTokens = [];
for (const doc of userTokens) {
allTokens.push(...doc.web_token);
}
// allTokens: ['token1', 'token2', 'token3', ...]
Side Effects:
- โน๏ธ Database Read: Queries fcm.tokens collection
๐ Integration Pointsโ
Internal Servicesโ
Token Registration Trigger Points:
- User Login: Client should register token after successful login
- Session Start: Register token when user opens application
- Token Refresh: Re-register when FCM SDK generates new token
- Device Switch: Register new token when user switches devices
Used By:
- Web application (browser FCM SDK)
- Mobile applications (iOS/Android FCM SDK)
- Desktop applications (Electron with FCM)
Client-Side Integration Patternโ
Web Browser Example:
// 1. Initialize Firebase in client
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken } from 'firebase/messaging';
const firebaseConfig = {
/* your config */
};
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
// 2. Request permission and get token
Notification.requestPermission().then(async permission => {
if (permission === 'granted') {
const token = await getToken(messaging, {
vapidKey: 'YOUR_VAPID_KEY',
});
// 3. Register token with DashClicks backend
await fetch('/v1/e/fcm/web', {
method: 'POST',
headers: {
Authorization: `Bearer ${jwtToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
}
});
External API Dependenciesโ
Provider: Google Firebase
Client SDK: Firebase JavaScript SDK (web) or Firebase Admin SDK (server)
Token Generation: Client-side only (browser or mobile app)
Token Validation: Firebase automatically validates tokens during message delivery
๐งช Edge Cases & Special Handlingโ
Multiple Device Sessionsโ
Issue: User may have multiple browser tabs or devices open simultaneously
Handling:
- All tokens stored in
web_tokenarray $addToSetprevents duplicates- Notifications sent to all registered tokens
Token Array Example:
{
"web_token": [
"token_from_chrome_laptop",
"token_from_firefox_laptop",
"token_from_chrome_mobile"
]
}
Token Regenerationโ
Issue: FCM SDK may generate new token when:
- Service worker updated
- Token manually deleted
- App re-installed
Handling:
- Client should call token registration endpoint again
- New token added to array
- Old tokens remain until manually cleaned up
Expired or Invalid Tokensโ
Issue: Tokens become invalid when:
- User clears browser data
- App uninstalled
- Token manually revoked
Handling:
- No automatic cleanup mechanism
- Failed deliveries tracked but tokens not removed
- Manual cleanup required (not implemented)
Future Enhancement Needed:
// Example cleanup logic (not implemented)
if (fcmResponse.failureCount > 0) {
// Remove invalid tokens based on error codes
fcmResponse.responses.forEach((resp, idx) => {
if (resp.error?.code === 'messaging/invalid-registration-token') {
// Remove tokens[idx] from database
}
});
}
Token Type Extensibilityโ
Issue: System designed for multiple token types (web, mobile, etc.)
Current Implementation:
- Only
web_tokenfield used typeparameter supports different field names- Schema allows adding
mobile_token,ios_token, etc.
Schema Flexibility:
// Current schema (strict: false allows dynamic fields)
{
"web_token": ["token1"],
// Could add in future:
// "mobile_token": ["token2"],
// "ios_token": ["token3"]
}
Concurrent Token Registrationโ
Issue: Same user might register tokens from multiple devices simultaneously
Handling:
- MongoDB
$addToSetatomic operation - No race conditions
- All tokens preserved
โ ๏ธ Important Notesโ
- ๐ฑ Client SDK Required: Tokens must be generated by Firebase client SDK
- ๐ Duplicate Prevention:
$addToSetensures no duplicate tokens in array - ๐ฅ Multi-Device Support: Single user can have many tokens simultaneously
- ๐งน No Auto-Cleanup: Invalid tokens remain in database indefinitely
- ๐ Account Scoped: Tokens tied to account_id and user_id pair
- ๐ Token Type: Currently only
web_tokenused, system extensible for mobile - โก Immediate Availability: Registered tokens available for notifications immediately
- ๐ Authentication Required: All endpoints require valid JWT token
๐ Related Documentationโ
- Integration Overview: FCM Integration
- Send Notifications: ./notifications.md
- Firebase Client SDK: FCM Web Setup
- Firebase Admin SDK: Admin SDK Setup