Design and implement real-time data synchronization: WebSocket vs SSE vs long polling decision, Supabase Realtime, Firebase RTDB, optimistic updates, conflict resolution, and connection resilience patterns.
From sanpx claudepluginhub javimontano/jm-adk --plugin sovereign-architectThis skill is limited to using the following tools:
agents/conflict-resolution-agent.mdagents/realtime-data-sync-agent.mdevals/evals.jsonexamples/sample-output.mdprompts/use-case-prompts.mdreferences/body-of-knowledge.mdreferences/knowledge-graph.mmdreferences/state-of-the-art.md"Real-time sync is about maintaining an illusion of consistency across an inherently distributed system — design for failure, not success."
Five-step procedure to design a real-time sync architecture: transport selection, optimistic update patterns, conflict resolution strategy, connection lifecycle management, and server-sent vs. bidirectional trade-offs.
| Criterion | WebSocket | SSE | Long Polling |
|---|---|---|---|
| Browser support | All modern | All modern (IE11 excl.) | All |
| Direction | Bidirectional | Server → Client only | Server → Client |
| Protocol | TCP framing | HTTP/1.1 or HTTP/2 | HTTP |
| Load balancer friendly | Sticky sessions required | Standard HTTP routing | Standard HTTP routing |
| Message size | Any | Text only (natively) | Text only |
| Auto-reconnect | Manual | Native EventSource | Manual |
| Proxy/firewall issues | Some corporate firewalls block | Rare | Never |
| Use case | Chat, collaborative editing, gaming | Notifications, feeds, dashboards | Simple polling fallback |
Optimistic updates apply mutations to local state immediately before server confirmation.
// hooks/useOptimisticMutation.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useUpdateTodo(todoId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (update: Partial<Todo>) =>
fetch(`/api/todos/${todoId}`, {
method: 'PATCH',
body: JSON.stringify(update),
}).then(r => r.json()),
// Apply optimistically before API call
onMutate: async (update) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo => todo.id === todoId ? { ...todo, ...update } : todo) ?? []
);
return { previous }; // Snapshot for rollback
},
onError: (_err, _vars, context) => {
// Roll back to pre-mutation state
queryClient.setQueryData(['todos'], context?.previous);
},
onSettled: () => {
// Sync with server reality regardless of outcome
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}
// When creating new records optimistically
const tempId = `temp-${crypto.randomUUID()}`;
const optimisticTodo: Todo = { id: tempId, title, completed: false };
// After server responds, replace tempId with real id
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo => todo.id === tempId ? serverResponse : todo) ?? []
);
// lib/realtime.ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Subscribe to Postgres changes (CDC)
export function subscribeToTodos(
userId: string,
onInsert: (todo: Todo) => void,
onUpdate: (todo: Todo) => void,
onDelete: (id: string) => void,
) {
const channel = supabase
.channel(`todos:${userId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'todos',
filter: `user_id=eq.${userId}`,
},
(payload) => {
switch (payload.eventType) {
case 'INSERT': onInsert(payload.new as Todo); break;
case 'UPDATE': onUpdate(payload.new as Todo); break;
case 'DELETE': onDelete(payload.old.id as string); break;
}
}
)
.subscribe((status) => {
if (status === 'CHANNEL_ERROR') {
console.error('Realtime channel error — will retry');
}
});
return () => supabase.removeChannel(channel);
}
// Presence — who is online
export function usePresence(roomId: string) {
const channel = supabase.channel(`room:${roomId}`);
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
// { userId: [{ online_at, ... }] }
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ user_id: currentUserId, online_at: new Date().toISOString() });
}
});
}
// Simple: highest timestamp wins
function mergeUpdates<T extends { updatedAt: string }>(local: T, remote: T): T {
return new Date(local.updatedAt) > new Date(remote.updatedAt) ? local : remote;
}
// For collaborative text editing without full OT library
// Position-based transforms
function transformInsert(
op: InsertOp,
against: InsertOp | DeleteOp
): InsertOp {
if (against.type === 'insert' && against.position <= op.position) {
return { ...op, position: op.position + against.text.length };
}
if (against.type === 'delete' && against.position < op.position) {
return { ...op, position: op.position - against.length };
}
return op;
}
// For production: use Yjs (CRDT) or ShareDB (OT)
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const doc = new Y.Doc();
const provider = new WebsocketProvider(
'wss://your-yjs-server.com',
`document:${docId}`,
doc,
{ connect: true }
);
// Shared types — CRDT-safe
const text = doc.getText('content');
const map = doc.getMap('metadata');
// Bind to editor (e.g., TipTap, CodeMirror)
provider.awareness.setLocalStateField('user', {
name: currentUser.name,
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
});
// lib/ws-client.ts
export class ReliableWebSocket {
private ws: WebSocket | null = null;
private reconnectAttempt = 0;
private maxAttempts = 10;
constructor(
private url: string,
private handlers: {
onMessage: (data: unknown) => void;
onReconnect: () => void;
}
) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectAttempt = 0;
this.handlers.onReconnect(); // Re-subscribe to rooms
};
this.ws.onmessage = (event) => {
this.handlers.onMessage(JSON.parse(event.data));
};
this.ws.onclose = (event) => {
if (event.code !== 1000 && this.reconnectAttempt < this.maxAttempts) {
const delay = Math.min(1000 * 2 ** this.reconnectAttempt, 30000);
const jitter = Math.random() * 1000;
setTimeout(() => {
this.reconnectAttempt++;
this.connect();
}, delay + jitter);
}
};
}
send(data: unknown) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
// Queue for when connection restores
this.messageQueue.push(data);
}
}
}
// Throttle mouse movements — never send every mousemove event
import { throttle } from 'lodash-es';
const sendCursorPosition = throttle((x: number, y: number) => {
ws.send({ type: 'cursor', x, y, userId });
}, 50); // Max 20 updates/second per user
document.addEventListener('mousemove', (e) => {
sendCursorPosition(e.clientX, e.clientY);
});
supabase.channel('*') for a multi-tenant app streams every tenant's data to every user; always filter by user_id or org_id.debounce instead of throttle for cursor/presence — Debounce delays all events until pause; cursor appears frozen during continuous movement.visibilitychange — Page hidden → connection often drops silently; reconnect when visibility returns.Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.