Vai al contenuto principale
Backend

Supabase Realtime: Costruire Feature Multiplayer Senza Gestire un Singolo Server WebSocket

4 min lettura
LD
Lucio Durán
Engineering Manager & AI Solutions Architect
Disponibile anche in: English, Español

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.

supabaserealtimewebsocketspostgrespresencemultiplayer

Strumenti menzionati in questo articolo

SupabaseProva Supabase
VercelProva Vercel
Divulgazione: Alcuni link in questo articolo sono link di affiliazione. Se ti registri tramite questi, potrei guadagnare una commissione senza costi aggiuntivi per te. Raccomando solo strumenti che uso e di cui mi fido personalmente.
Compartir
Seguime