Supabase Realtime: Costruire Feature Multiplayer Senza Gestire un Singolo Server WebSocket
Le Tre Primitive
1. Broadcast — Messaggistica client-to-client attraverso il server. Nessuna persistenza. Latenza sub-20ms.
2. Presence — Stato condiviso che traccia chi è online e il loro stato attuale. Sincronizzato automaticamente.
3. Postgres Changes — CDC dal WAL di Postgres. Qualsiasi INSERT, UPDATE, DELETE su una tabella sottoscritta viene pushato ai client connessi.
Costruire un Editor Documenti Collaborativo
Configurare il Canale
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
realtime: {
params: {
eventsPerSecond: 20,
},
},
});
const channel = supabase.channel(`document:${documentId}`, {
config: {
broadcast: { self: false },
presence: { key: userId },
},
});
Presence: Chi C'è?
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
userId: user.id,
name: user.name,
avatar: user.avatar_url,
color: assignedColor,
cursor: null,
lastActive: new Date().toISOString(),
});
}
});
channel.on('presence', { event: 'sync' }, () => {
const presenceState = channel.presenceState();
const activeUsers = Object.entries(presenceState).map(([key, values]) => ({
...values[0],
presenceKey: key,
}));
setActiveUsers(activeUsers);
});
channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
showToast(`${newPresences[0].name} sta visualizzando questo documento`);
});
channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
removeCursor(leftPresences[0].userId);
});
Broadcast: Cursori Live
function handleMouseMove(e) {
const position = editorToDocumentCoords(e.clientX, e.clientY);
channel.send({
type: 'broadcast',
event: 'cursor',
payload: {
userId: user.id,
x: position.x,
y: position.y,
selection: getCurrentSelection(),
},
});
}
channel.on('broadcast', { event: 'cursor' }, ({ payload }) => {
updateRemoteCursor(payload.userId, payload.x, payload.y, payload.selection);
});
I messaggi Broadcast passano attraverso il server Realtime di Supabase ma non toccano mai il database. La latenza è tipicamente 8-15ms tra client nella stessa regione.
Postgres Changes: Stato Documento Persistente
channel.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'document_blocks',
filter: `document_id=eq.${documentId}`,
},
(payload) => {
switch (payload.eventType) {
case 'INSERT':
insertBlock(payload.new);
break;
case 'UPDATE':
updateBlock(payload.new, payload.old);
break;
case 'DELETE':
deleteBlock(payload.old);
break;
}
}
);
Risoluzione Conflitti
async function saveBlockEdit(blockId, newContent, baseVersion) {
const { data, error } = await supabase
.from('document_blocks')
.update({
content: newContent,
version: baseVersion + 1,
last_edited_by: user.id,
last_edited_at: new Date().toISOString(),
})
.eq('id', blockId)
.eq('version', baseVersion) // Controllo concorrenza ottimistico
.select()
.single();
if (error) {
const { data: current } = await supabase
.from('document_blocks')
.select('*')
.eq('id', blockId)
.single();
const mergedContent = transformEdit(newContent, current.content, baseVersion);
return saveBlockEdit(blockId, mergedContent, current.version);
}
return data;
}
Considerazioni sullo Scaling
Gestione Connessioni
const broadcastChannel = new BroadcastChannel('supabase-realtime');
let isLeader = false;
broadcastChannel.onmessage = (event) => {
if (event.data.type === 'leader-heartbeat') {
isLeader = false;
}
if (event.data.type === 'realtime-event') {
handleRealtimeEvent(event.data.payload);
}
};
setTimeout(() => {
if (!isLeader) {
isLeader = true;
setInterval(() => {
broadcastChannel.postMessage({ type: 'leader-heartbeat' });
}, 1000);
initializeSupabaseRealtime();
}
}, Math.random() * 1000);
Ottimizzazione a Livello Database
CREATE PUBLICATION supabase_realtime FOR TABLE
document_blocks,
comments,
activity_feed;
CREATE INDEX idx_document_blocks_document_id
ON document_blocks (document_id);
Numeri di Performance Reali
| Feature | Latenza (p50) | Latenza (p99) | Messaggi/sec | |---------|---------------|---------------|--------------| | Broadcast (stessa regione) | 12ms | 28ms | 500+ per client | | Broadcast (cross-regione) | 65ms | 120ms | 500+ per client | | Presence sync | 15ms | 40ms | N/A | | Postgres Changes (filtrato) | 35ms | 85ms | Limitato da write DB |
Edge Function per Validazione Server-Side
Deno.serve(async (req) => {
const { gameId, playerId, move } = await req.json();
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { data: game } = await supabase
.from('games')
.select('*')
.eq('id', gameId)
.single();
if (game.current_turn !== playerId) {
return new Response(JSON.stringify({ error: 'Non è il tuo turno' }), { status: 400 });
}
if (!isValidMove(game.board, move)) {
return new Response(JSON.stringify({ error: 'Mossa non valida' }), { status: 400 });
}
const { data: updated } = await supabase
.from('games')
.update({
board: applyMove(game.board, move),
current_turn: nextPlayer(game),
move_history: [...game.move_history, { playerId, move, timestamp: new Date() }],
})
.eq('id', gameId)
.select()
.single();
return new Response(JSON.stringify(updated));
});
L'Architettura Emersa
[Browser] ←→ [Supabase Realtime (WebSocket)]
├── Broadcast: cursori, digitazione, eventi effimeri
├── Presence: utenti online, stato
└── Postgres Changes: contenuto documento, game state
↑
[Vercel Edge] → [Supabase Edge Functions] → [Postgres]
Validazione, auth, logica di business
Il modello mentale: Broadcast è un relay WebSocket, Presence è una hash table distribuita, e Postgres Changes è un sistema di notifiche push per il tuo database. Combinali insieme e ottieni una piattaforma real-time sorprendentemente capace senza gestire infrastruttura.
Dopo sei mesi in produzione con ~300 utenti attivi giornalieri, le feature di collaborazione sono state solide. Abbiamo avuto due interruzioni Supabase Realtime (entrambe sotto i 5 minuti), zero perdita dati (perché lo stato persistente è in Postgres, non nel layer WebSocket), e la nostra fattura Supabase è $25/mese per il tier Pro. L'infrastruttura custom equivalente costerebbe 10x di più sia in denaro che in tempo di manutenzione.