PatternsReal-Time Architecture
Presence System
Track and display user online status, last seen, and activity in real-time
Show who's online, when they were last seen, and what they're doing. This guide covers production-ready presence patterns.
🎯 Goals
What we need:
✅ Online/Offline status
✅ Last seen timestamp
✅ Activity status ("typing", "viewing", etc)
✅ Efficient updates
✅ Handle reconnections
✅ Privacy controls🟢 Pattern 1: Basic Presence
Simple online/offline tracking.
interface PresenceState {
userId: string;
status: 'online' | 'offline' | 'away';
lastSeen: Date;
device?: 'web' | 'mobile' | 'desktop';
}
class PresenceManager {
private ws: WebSocket;
private presenceMap = new Map<string, PresenceState>();
private heartbeatInterval: NodeJS.Timeout | null = null;
constructor(private userId: string, ws: WebSocket) {
this.ws = ws;
this.startHeartbeat();
this.trackVisibility();
}
private startHeartbeat() {
// Send presence every 30 seconds
this.heartbeatInterval = setInterval(() => {
this.sendPresence('online');
}, 30000);
// Send initial presence
this.sendPresence('online');
}
private sendPresence(status: PresenceState['status']) {
this.ws.send(JSON.stringify({
type: 'presence',
userId: this.userId,
status,
timestamp: Date.now()
}));
}
private trackVisibility() {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.sendPresence('away');
} else {
this.sendPresence('online');
}
});
// Track window focus
window.addEventListener('blur', () => {
this.sendPresence('away');
});
window.addEventListener('focus', () => {
this.sendPresence('online');
});
}
updatePresence(userId: string, state: PresenceState) {
this.presenceMap.set(userId, state);
this.notifyListeners(userId, state);
}
getPresence(userId: string): PresenceState | undefined {
return this.presenceMap.get(userId);
}
isOnline(userId: string): boolean {
const presence = this.presenceMap.get(userId);
return presence?.status === 'online';
}
private notifyListeners(userId: string, state: PresenceState) {
window.dispatchEvent(new CustomEvent('presence-change', {
detail: { userId, state }
}));
}
cleanup() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.sendPresence('offline');
}
}React Hook
function usePresence(userId: string) {
const [presence, setPresence] = useState<PresenceState | null>(null);
useEffect(() => {
const handler = (event: CustomEvent) => {
if (event.detail.userId === userId) {
setPresence(event.detail.state);
}
};
window.addEventListener('presence-change', handler as EventListener);
return () => {
window.removeEventListener('presence-change', handler as EventListener);
};
}, [userId]);
return presence;
}
// Usage
function UserAvatar({ userId }: { userId: string }) {
const presence = usePresence(userId);
return (
<div className="avatar">
<img src={`/avatars/${userId}.jpg`} alt="Avatar" />
<span className={`status-indicator ${presence?.status}`}>
{presence?.status === 'online' && '🟢'}
{presence?.status === 'away' && '🟡'}
{presence?.status === 'offline' && '⚫'}
</span>
</div>
);
}⏰ Pattern 2: Last Seen
Show when user was last active.
class LastSeenTracker {
private lastSeenMap = new Map<string, Date>();
updateLastSeen(userId: string, timestamp: Date) {
this.lastSeenMap.set(userId, timestamp);
}
getLastSeen(userId: string): Date | null {
return this.lastSeenMap.get(userId) || null;
}
getLastSeenText(userId: string): string {
const lastSeen = this.getLastSeen(userId);
if (!lastSeen) {
return 'Never';
}
const now = Date.now();
const diff = now - lastSeen.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) {
return 'Just now';
} else if (minutes < 60) {
return `${minutes}m ago`;
} else if (hours < 24) {
return `${hours}h ago`;
} else if (days < 7) {
return `${days}d ago`;
} else {
return lastSeen.toLocaleDateString();
}
}
}
// React Component
function LastSeenIndicator({ userId }: { userId: string }) {
const presence = usePresence(userId);
const [lastSeenText, setLastSeenText] = useState('');
useEffect(() => {
if (presence?.status === 'offline' && presence?.lastSeen) {
const tracker = new LastSeenTracker();
tracker.updateLastSeen(userId, presence.lastSeen);
const updateText = () => {
setLastSeenText(tracker.getLastSeenText(userId));
};
updateText();
// Update every minute
const interval = setInterval(updateText, 60000);
return () => clearInterval(interval);
} else if (presence?.status === 'online') {
setLastSeenText('Online');
} else if (presence?.status === 'away') {
setLastSeenText('Away');
}
}, [userId, presence]);
return <span className="last-seen">{lastSeenText}</span>;
}💼 Pattern 3: Activity Status
Show what users are doing.
type Activity =
| { type: 'viewing'; page: string }
| { type: 'typing'; conversationId: string }
| { type: 'idle' }
| { type: 'busy'; reason: string };
interface UserActivity {
userId: string;
activity: Activity;
timestamp: Date;
}
class ActivityTracker {
private ws: WebSocket;
private activities = new Map<string, UserActivity>();
private currentActivity: Activity = { type: 'idle' };
private activityTimeout: NodeJS.Timeout | null = null;
constructor(private userId: string, ws: WebSocket) {
this.ws = ws;
this.trackActivity();
}
setActivity(activity: Activity) {
this.currentActivity = activity;
this.sendActivity();
// Auto-clear after 3 seconds
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
this.activityTimeout = setTimeout(() => {
this.setActivity({ type: 'idle' });
}, 3000);
}
private sendActivity() {
this.ws.send(JSON.stringify({
type: 'activity',
userId: this.userId,
activity: this.currentActivity,
timestamp: Date.now()
}));
}
private trackActivity() {
// Track page views
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
this.setActivity({
type: 'viewing',
page: window.location.pathname
});
}
}
});
observer.observe({ entryTypes: ['navigation'] });
}
updateActivity(userId: string, activity: UserActivity) {
this.activities.set(userId, activity);
}
getActivity(userId: string): UserActivity | undefined {
return this.activities.get(userId);
}
getActivityText(userId: string): string {
const activity = this.getActivity(userId);
if (!activity) {
return '';
}
switch (activity.activity.type) {
case 'viewing':
return `Viewing ${activity.activity.page}`;
case 'typing':
return 'Typing...';
case 'busy':
return activity.activity.reason;
case 'idle':
default:
return '';
}
}
}
// React Hook
function useActivity(userId: string) {
const [activity, setActivity] = useState<string>('');
useEffect(() => {
const handler = (event: CustomEvent) => {
if (event.detail.userId === userId) {
const tracker = new ActivityTracker(userId, event.detail.ws);
setActivity(tracker.getActivityText(userId));
}
};
window.addEventListener('activity-change', handler as EventListener);
return () => {
window.removeEventListener('activity-change', handler as EventListener);
};
}, [userId]);
return activity;
}📊 Pattern 4: Bulk Presence Updates
Efficiently update presence for many users.
class BulkPresenceManager {
private ws: WebSocket;
private presenceCache = new Map<string, PresenceState>();
private subscribedUsers = new Set<string>();
constructor(ws: WebSocket) {
this.ws = ws;
}
subscribe(userIds: string[]) {
userIds.forEach(id => this.subscribedUsers.add(id));
// Request presence for all users
this.ws.send(JSON.stringify({
type: 'presence_subscribe',
userIds: Array.from(this.subscribedUsers)
}));
}
unsubscribe(userIds: string[]) {
userIds.forEach(id => this.subscribedUsers.delete(id));
this.ws.send(JSON.stringify({
type: 'presence_unsubscribe',
userIds
}));
}
handleBulkUpdate(updates: PresenceState[]) {
updates.forEach(update => {
this.presenceCache.set(update.userId, update);
});
// Notify all listeners
window.dispatchEvent(new CustomEvent('bulk-presence-update', {
detail: { updates }
}));
}
getPresence(userId: string): PresenceState | undefined {
return this.presenceCache.get(userId);
}
getAllPresence(): Map<string, PresenceState> {
return new Map(this.presenceCache);
}
}
// React Hook for multiple users
function useBulkPresence(userIds: string[]) {
const [presenceMap, setPresenceMap] = useState<Map<string, PresenceState>>(new Map());
useEffect(() => {
const manager = new BulkPresenceManager(ws);
manager.subscribe(userIds);
const handler = (event: CustomEvent) => {
setPresenceMap(new Map(manager.getAllPresence()));
};
window.addEventListener('bulk-presence-update', handler as EventListener);
return () => {
manager.unsubscribe(userIds);
window.removeEventListener('bulk-presence-update', handler as EventListener);
};
}, [userIds]);
return presenceMap;
}
// Usage - User List
function UserList({ users }: { users: User[] }) {
const presenceMap = useBulkPresence(users.map(u => u.id));
return (
<div>
{users.map(user => {
const presence = presenceMap.get(user.id);
return (
<div key={user.id} className="user-item">
<img src={user.avatar} alt={user.name} />
<span>{user.name}</span>
<span className={`status ${presence?.status}`}>
{presence?.status === 'online' ? '🟢' : '⚫'}
</span>
</div>
);
})}
</div>
);
}🔐 Pattern 5: Privacy Controls
Let users control their presence visibility.
type PresenceVisibility = 'everyone' | 'contacts' | 'nobody';
interface PresenceSettings {
showOnlineStatus: PresenceVisibility;
showLastSeen: PresenceVisibility;
showActivity: PresenceVisibility;
}
class PrivatePresenceManager {
private settings: PresenceSettings;
constructor(settings: PresenceSettings) {
this.settings = settings;
}
shouldShowOnlineStatus(viewerId: string, isContact: boolean): boolean {
switch (this.settings.showOnlineStatus) {
case 'everyone':
return true;
case 'contacts':
return isContact;
case 'nobody':
return false;
}
}
shouldShowLastSeen(viewerId: string, isContact: boolean): boolean {
switch (this.settings.showLastSeen) {
case 'everyone':
return true;
case 'contacts':
return isContact;
case 'nobody':
return false;
}
}
shouldShowActivity(viewerId: string, isContact: boolean): boolean {
switch (this.settings.showActivity) {
case 'everyone':
return true;
case 'contacts':
return isContact;
case 'nobody':
return false;
}
}
getVisiblePresence(
presence: PresenceState,
viewerId: string,
isContact: boolean
): Partial<PresenceState> {
const visible: Partial<PresenceState> = {
userId: presence.userId
};
if (this.shouldShowOnlineStatus(viewerId, isContact)) {
visible.status = presence.status;
}
if (this.shouldShowLastSeen(viewerId, isContact)) {
visible.lastSeen = presence.lastSeen;
}
return visible;
}
}
// React Component - Privacy Settings
function PresenceSettings() {
const [settings, setSettings] = useState<PresenceSettings>({
showOnlineStatus: 'everyone',
showLastSeen: 'contacts',
showActivity: 'contacts'
});
const updateSetting = (key: keyof PresenceSettings, value: PresenceVisibility) => {
setSettings(prev => ({ ...prev, [key]: value }));
// Save to server
fetch('/api/settings/presence', {
method: 'PUT',
body: JSON.stringify({ ...settings, [key]: value })
});
};
return (
<div className="presence-settings">
<h3>Privacy Settings</h3>
<div>
<label>Show online status:</label>
<select
value={settings.showOnlineStatus}
onChange={(e) => updateSetting('showOnlineStatus', e.target.value as PresenceVisibility)}
>
<option value="everyone">Everyone</option>
<option value="contacts">Contacts only</option>
<option value="nobody">Nobody</option>
</select>
</div>
<div>
<label>Show last seen:</label>
<select
value={settings.showLastSeen}
onChange={(e) => updateSetting('showLastSeen', e.target.value as PresenceVisibility)}
>
<option value="everyone">Everyone</option>
<option value="contacts">Contacts only</option>
<option value="nobody">Nobody</option>
</select>
</div>
<div>
<label>Show activity:</label>
<select
value={settings.showActivity}
onChange={(e) => updateSetting('showActivity', e.target.value as PresenceVisibility)}
>
<option value="everyone">Everyone</option>
<option value="contacts">Contacts only</option>
<option value="nobody">Nobody</option>
</select>
</div>
</div>
);
}📚 Key Takeaways
- Heartbeat every 30s - Keep presence fresh
- Track visibility - Go "away" when tab hidden
- Last seen - Update on every activity
- Bulk updates - Don't subscribe to 1000 users individually
- Privacy controls - Let users choose visibility
- Activity status - Clear after 3s of inactivity
- Test reconnections - Presence should restore
Presence is expected in modern apps. Implement it early, not as an afterthought.