This document is the official API and integration reference for the SOL9X Mobile App engineering team (iOS, Android, Flutter, React Native). It details how to authenticate members, manage Firebase Cloud Messaging (FCM) device tokens, receive real-time push notifications for critical threats, and build the incident triage dashboard.
| Environment | Base URL |
|---|---|
| Production | https://api.logger.sol9x.in/api/v1 |
| Local Development | http://localhost:4000/api/v1 |
Every authenticated endpoint requires standard JSON headers and the Bearer JWT received upon login:
Content-Type: application/json
Authorization: Bearer <JWT_ACCESS_TOKEN>
The backend ties device FCM registration tokens to user accounts for targeted push delivery to assigned project members. Follow this 3-step lifecycle:
[ Mobile App Launch ] ──► [ Request FCM Token ]
│
▼
┌─────────────────── [ User Logs In ] ──────────────────┐
│ (Sends email, password, and device fcmToken) │
▼ ▼
[ Backend Stores Token ] [ Mobile Stores JWT ]
│ │
▼ ▼
[ FCM onTokenRefresh Triggered? ] ──► [ PUT /auth/fcm-token ] │
▼
[ User Logs Out ]
│
▼
[ POST /auth/logout ]
(Backend clears device token)
Authenticate the user with email/password and automatically register the current device's FCM token.
POST /auth/login{
"email": "engineer@company.com",
"password": "mySecurePassword123",
"fcmToken": "c1a2b3c4d5e6f7g8h9i0_sample_fcm_registration_token"
}
Note:
fcmTokenis optional. If the user denies notification permissions, omitfcmToken. If granted, always send it.
{
"success": true,
"message": "Login successful.",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "cmvetybcu0002itfnmirhvn2d",
"name": "Jane Developer",
"email": "engineer@company.com",
"role": "MEMBER",
"assignedProjects": [
{
"id": "proj_clv123456",
"name": "Payment Service",
"slug": "payment-service"
},
{
"id": "proj_clv987654",
"name": "Auth Gateway",
"slug": "auth-gateway"
}
]
}
}
}
If
roleis"ADMIN",assignedProjectswill be the string"ALL".
When the Firebase SDK rotates the device registration token (e.g., app reinstall, OS update, or periodic refresh), call this endpoint silently in the background.
PUT /auth/fcm-tokenBearer <token>){
"fcmToken": "new_refreshed_fcm_token_xxxxxxxxxxxxxxxx"
}
{
"success": true,
"message": "FCM token updated successfully."
}
Always call this endpoint when the user taps Log Out. It removes the device's fcmToken from the database so the device stops receiving notifications for that account.
POST /auth/logoutBearer <token>)Authorization: Bearer <JWT_ACCESS_TOKEN>
{
"success": true,
"message": "Logged out successfully."
}
Used on app launch to validate stored JWT sessions and retrieve assigned projects.
GET /auth/meBearer <token>){
"success": true,
"data": {
"id": "cmvetybcu0002itfnmirhvn2d",
"name": "Jane Developer",
"email": "engineer@company.com",
"role": "MEMBER",
"assignedProjects": [
{
"id": "proj_clv123456",
"name": "Payment Service",
"slug": "payment-service"
}
]
}
}
When a critical alert triggers (e.g., server crash, cascading 5xx surge, or DB connection outage), the backend pushes an FCM multicast notification to all active devices of members assigned to the affected project.
Configure these two notification channels in your Android Application class or Flutter/React Native setup:
critical_alerts — Importance: MAX, Sound: Default (or High-priority alarm)threat_alerts — Importance: HIGH, Sound: Default{
"notification": {
"title": "🚨 [CRITICAL] Infrastructure / Database Failures",
"body": "payment-api (production): PrismaClientKnownRequestError: Can't reach database server"
},
"data": {
"incidentId": "cmveu174i0003ithyy2twy9lp",
"projectId": "proj_clv123456",
"ruleId": "rule_cmi789012",
"severity": "CRITICAL",
"service": "payment-api",
"environment": "production",
"rootLogId": "log_clv654321",
"click_action": "FLUTTER_NOTIFICATION_CLICK"
}
}
When the user taps on the push notification:
data["incidentId"].Navigator.pushNamed(context, '/threats/detail', arguments: incidentId);Fetches threat incidents. The response is automatically scoped to the projects the logged-in user is assigned to.
GET /threatsBearer <token>)| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
projectId |
string | No | all | Filter by specific project ID |
status |
string | No | all | OPEN, ACKNOWLEDGED, or RESOLVED |
severity |
string | No | all | CRITICAL or INTERMEDIATE |
page |
integer | No | 1 |
Page number (1-based) |
limit |
integer | No | 20 |
Items per page (max 100) |
curl -X GET "https://api.logger.sol9x.in/api/v1/threats?status=OPEN&severity=CRITICAL&page=1&limit=20" \
-H "Authorization: Bearer <JWT_ACCESS_TOKEN>"
{
"success": true,
"data": {
"threats": [
{
"id": "cmveu174i0003ithyy2twy9lp",
"projectId": "proj_clv123456",
"ruleId": "rule_cmi789012",
"severity": "CRITICAL",
"status": "OPEN",
"fingerprint": "a4b2c1d9e8f7...",
"title": "5xx Cascading Error Surge",
"service": "order-service",
"environment": "production",
"errorType": "InternalServerError",
"message": "Transaction failed: DB lock timeout exceeded",
"rootLogId": "log_clv654321",
"occurrenceCount": 14,
"lastSeenAt": "2026-10-19T05:47:18.000Z",
"acknowledgedAt": null,
"acknowledgedById": null,
"resolvedAt": null,
"resolvedById": null,
"createdAt": "2026-10-19T05:30:00.000Z",
"project": {
"id": "proj_clv123456",
"name": "Order Service",
"slug": "order-service"
},
"rule": {
"id": "rule_cmi789012",
"name": "5xx Cascading Error Surge",
"severity": "CRITICAL"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}
}
Fetches complete details for a single incident, including the triggering alert rule and the full root-cause rootLog (stack trace, request/trace ID, HTTP status, and duration).
Endpoint: GET /threats/:id
Access: Authenticated (Bearer <token>)
Path Parameters:
id: The incidentId (e.g. cmveu174i0003ithyy2twy9lp)Response (200 OK):
{
"success": true,
"data": {
"id": "cmveu174i0003ithyy2twy9lp",
"projectId": "proj_clv123456",
"ruleId": "rule_cmi789012",
"severity": "CRITICAL",
"status": "OPEN",
"fingerprint": "a4b2c1d9e8f7...",
"title": "5xx Cascading Error Surge",
"service": "order-service",
"environment": "production",
"errorType": "PaymentDeadlockException",
"message": "Critical error: PaymentDeadlock detected on checkout table",
"rootLogId": "log_clv654321",
"occurrenceCount": 14,
"lastSeenAt": "2026-10-19T05:47:18.000Z",
"acknowledgedAt": null,
"acknowledgedById": null,
"resolvedAt": null,
"resolvedById": null,
"createdAt": "2026-10-19T05:30:00.000Z",
"project": {
"id": "proj_clv123456",
"name": "Order Service",
"slug": "order-service"
},
"rule": {
"id": "rule_cmi789012",
"name": "5xx Cascading Error Surge",
"severity": "CRITICAL",
"thresholdCount": 10,
"windowSeconds": 60,
"cooldownMinutes": 15
},
"rootLog": {
"id": "log_clv654321",
"projectId": "proj_clv123456",
"environment": "production",
"service": "order-service",
"level": "ERROR",
"message": "Critical error: PaymentDeadlock detected on checkout table",
"errorType": "PaymentDeadlockException",
"stackTrace": "Error: PaymentDeadlock\n at checkout (/app/dist/order.js:42:15)\n at processTicksAndRejections...",
"httpMethod": "POST",
"endpoint": "/api/v1/orders/checkout",
"statusCode": 500,
"duration": 1200.5,
"requestId": "req_a1b2c3d4",
"traceId": "trace_9988776655",
"userId": "usr_test_1",
"metadata": {
"cartId": "cart_908123",
"attempt": 3
},
"createdAt": "2026-10-19T05:30:00.000Z"
}
}
}
Allows on-call mobile responders to acknowledge an active alert or resolve it once fixed.
Endpoint: PATCH /threats/:id/status
Access: Authenticated (Bearer <token>)
Path Parameters:
id: The incidentIdRequest Body (To Acknowledge):
{
"status": "ACKNOWLEDGED"
}
{
"status": "RESOLVED"
}
{
"success": true,
"message": "Threat incident status updated successfully.",
"data": {
"id": "cmveu174i0003ithyy2twy9lp",
"status": "ACKNOWLEDGED",
"acknowledgedAt": "2026-10-19T05:52:00.000Z",
"acknowledgedById": "cmvetybcu0002itfnmirhvn2d",
"resolvedAt": null,
"resolvedById": null,
"occurrenceCount": 14
}
}
Mobile members can also inspect surrounding logs when diagnosing an incident.
GET /logsBearer <token>)| Parameter | Type | Example | Description |
|---|---|---|---|
projectId |
string | proj_clv123456 |
Scoped to project |
levels |
string | ERROR,FATAL |
Comma-separated log levels |
service |
string | order-service |
Filter by microservice name |
environment |
string | production |
production, staging, etc. |
search |
string | deadlock |
Free-text search on message & errorType |
page |
integer | 1 |
Page number |
limit |
integer | 25 |
Default 20, max 100 |
{
"data": [
{
"id": "log_clv654321",
"projectId": "proj_clv123456",
"service": "order-service",
"level": "ERROR",
"message": "Transaction failed: deadlock detected",
"statusCode": 500,
"duration": 450.2,
"timestamp": "2026-10-19T05:46:10.000Z"
}
],
"meta": {
"total": 1,
"page": 1,
"limit": 25,
"totalPages": 1
}
}
GET /logs/:idBearer <token>){
"success": true,
"data": {
"id": "log_clv654321",
"projectId": "proj_clv123456",
"service": "order-service",
"level": "ERROR",
"message": "Transaction failed: deadlock detected",
"errorType": "PaymentDeadlockException",
"stackTrace": "...",
"metadata": {}
}
}
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class NotificationService {
final FirebaseMessaging _fcm = FirebaseMessaging.instance;
String? _jwtToken;
void setAuthToken(String token) {
_jwtToken = token;
}
Future<void> initialize(Function(String incidentId) onOpenIncident) async {
// 1. Request OS notification permission
NotificationSettings settings = await _fcm.requestPermission(
alert: true,
badge: true,
sound: true,
);
if (settings.authorizationStatus != AuthorizationStatus.authorized) {
return;
}
// 2. Handle token refresh silently
_fcm.onTokenRefresh.listen((newToken) async {
if (_jwtToken != null) {
await http.put(
Uri.parse('https://api.logger.sol9x.in/api/v1/auth/fcm-token'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_jwtToken',
},
body: jsonEncode({'fcmToken': newToken}),
);
}
});
// 3. Handle Notification Tap when app is running in background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
final incidentId = message.data['incidentId'];
if (incidentId != null) {
onOpenIncident(incidentId);
}
});
// 4. Handle Notification Tap when app was completely terminated (Cold Start)
RemoteMessage? initialMessage = await _fcm.getInitialMessage();
if (initialMessage != null) {
final incidentId = initialMessage.data['incidentId'];
if (incidentId != null) {
onOpenIncident(incidentId);
}
}
}
// Retrieve current token for Login API
Future<String?> getDeviceToken() async {
return await _fcm.getToken();
}
}
Future<void> updateIncidentStatus(String incidentId, String status, String jwt) async {
final response = await http.patch(
Uri.parse('https://api.logger.sol9x.in/api/v1/threats/$incidentId/status'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwt',
},
body: jsonEncode({'status': status}), // "ACKNOWLEDGED" or "RESOLVED"
);
if (response.statusCode != 200) {
throw Exception('Failed to update incident: ${response.body}');
}
}
import messaging from '@react-native-firebase/messaging';
import axios from 'axios';
const API_BASE = 'https://api.logger.sol9x.in/api/v1';
export async function loginMobile(email: string, password: string) {
// 1. Request notification permissions
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
let fcmToken: string | undefined;
if (enabled) {
fcmToken = await messaging().getToken();
}
// 2. Authenticate
const response = await axios.post(`${API_BASE}/auth/login`, {
email,
password,
fcmToken,
});
const { token, user } = response.data.data;
// 3. Listen for token refreshes
messaging().onTokenRefresh(async (newToken) => {
await axios.put(
`${API_BASE}/auth/fcm-token`,
{ fcmToken: newToken },
{ headers: { Authorization: `Bearer ${token}` } }
);
});
return { token, user };
}
export function setupNotificationHandlers(navigation: any) {
// Foreground notification handler
messaging().onMessage(async (remoteMessage) => {
console.log('Received foreground threat alert:', remoteMessage.notification);
});
// Background notification tap
messaging().onNotificationOpenedApp((remoteMessage) => {
const incidentId = remoteMessage.data?.incidentId;
if (incidentId) {
navigation.navigate('ThreatDetailScreen', { incidentId });
}
});
// Cold start notification tap
messaging()
.getInitialNotification()
.then((remoteMessage) => {
const incidentId = remoteMessage?.data?.incidentId;
if (incidentId) {
navigation.navigate('ThreatDetailScreen', { incidentId });
}
});
}
All endpoints adhere to standard HTTP status codes and provide a consistent JSON error schema:
{
"success": false,
"error": "ForbiddenError",
"message": "You do not have access to logs of this project."
}
| HTTP Status | Error Type | Cause / Solution |
|---|---|---|
400 |
ValidationError |
Malformed body or missing required field (check message) |
401 |
UnauthorizedError |
Missing, expired, or invalid Bearer JWT token |
403 |
ForbiddenError |
Attempted to access an incident or project not assigned to this user |
404 |
NotFoundError |
Incident or Log ID does not exist |
500 |
InternalServerError |
Server-side unexpected exception |
backend-infra@sol9x.comGET https://api.logger.sol9x.in/api/v1/health