Implementing Presence (Online Status, Activity) for Collaboration in Mobile Applications
Presence — ephemeral data layer about what user does right now: online, on which screen, what editing, where cursor. Unlike regular app data, presence doesn't store in DB — lives only while connection active. But correct implementation harder than seems.
Why Can't Just Store is_online in Firestore
Classic mistake: add lastSeen field to user document and update every 30 seconds. Problems:
- Battery kill: constant write operations in background. Android 8+ limits background tasks, iOS kills Background Fetch in minutes.
-
Wrong status on crash: app crashed —
is_online: truestays until next update. - Races on multiple devices: user online on phone and tablet. Left tablet — reset status, but phone still active.
Firebase Realtime Database: Correct Implementation via onDisconnect
Firebase RTDB has built-in mechanism — onDisconnect(). Server executes specified operation automatically on connection break, even if client just lost network:
import database from '@react-native-firebase/database';
const userStatusRef = database().ref(`/status/${userId}`);
const isOfflineData = { state: 'offline', lastChanged: database.ServerValue.TIMESTAMP };
const isOnlineData = { state: 'online', lastChanged: database.ServerValue.TIMESTAMP };
// Register disconnect action BEFORE setting online
await userStatusRef.onDisconnect().set(isOfflineData);
await userStatusRef.set(isOnlineData);
database.ServerValue.TIMESTAMP — server timestamp, doesn't depend on device timezone. Important: onDisconnect registered before set(isOnlineData) — otherwise race condition, client may disconnect between two calls.
For multiple device support — counter of active sessions instead of boolean flag:
// Use transaction for atomic increment
const sessionsRef = database().ref(`/sessions/${userId}`);
await sessionsRef.transaction(current => (current || 0) + 1);
await sessionsRef.onDisconnect().transaction(current => Math.max((current || 1) - 1, 0));
is_online = sessions > 0. Crash on one device decrements counter via onDisconnect, doesn't affect other sessions.
AppState: Syncing with iOS/Android Lifecycle
import { AppState, AppStateStatus } from 'react-native';
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
if (nextState === 'active') {
userStatusRef.onDisconnect().set(isOfflineData);
userStatusRef.set(isOnlineData);
} else if (nextState === 'background' || nextState === 'inactive') {
userStatusRef.set(isOfflineData);
userStatusRef.onDisconnect().cancel(); // cancel, already did manually
}
});
return () => subscription.remove();
}, []);
On Android on background you have seconds before system freezes JS thread. userStatusRef.set() — async operation, not guaranteed. onDisconnect() as fallback mandatory.
Typed Presence with Additional Context
Beyond online/offline, often need to know what user does:
type PresenceState = {
status: 'online' | 'idle' | 'offline';
currentScreen: string | null;
editingItemId: string | null;
lastChanged: number;
};
idle — user opened app but 5+ minutes didn't touch screen. Detected via PanResponder or TouchableWithoutFeedback on root component with debounce timer.
Display: Avatars with Indicator
In participants list with presence data add colored badge:
- Green:
status === 'online' - Yellow:
status === 'idle' - Gray:
status === 'offline', showlastChangedas "was online N minutes ago"
Nuance: don't update lastChanged on every presence change — only on status change. Otherwise list redraws every seconds for each active user.
Assessment
Firebase RTDB presence with multiple device support, idle-detection and UI component: 1–3 weeks. Custom WebSocket presence on own backend: 2–4 weeks.







