Supabase Realtime
Honeycomb uses Supabase Realtime extensively to deliver live updates without polling. There are three primary patterns: Postgres Changes for message delivery, Presence for online/typing state, and notification channels for unread counts.
Channel Naming Conventions
Section titled “Channel Naming Conventions”Each channel follows a {feature}:{id} naming scheme:
| Channel | Purpose |
|---|---|
room:{roomId} | Room messages (Postgres Changes on ext_honeycomb.room_messages) |
dm:{conversationId} | Direct messages (Postgres Changes on ext_honeycomb.direct_messages) |
chat:{chatId} | Platform messaging (Postgres Changes on public.messages) |
typing:{chatId} | Typing indicators (Presence) |
notifications:{userId} | Unread notification count (Postgres Changes on public.notifications) |
Postgres Changes — Live Messages
Section titled “Postgres Changes — Live Messages”The most common pattern subscribes to INSERT events on a messages table filtered by a room or conversation ID.
From src/routes/(app)/honeycomb/rooms/[slug]/+page.svelte:
const channel = supabase .channel(`room:${room.id}`) .on( "postgres_changes", { event: "INSERT", schema: "ext_honeycomb", table: "room_messages", filter: `room_id=eq.${room.id}`, }, async (payload) => { const newMsg = payload.new as RoomMessage; if (liveMessages.some((m) => m.id === newMsg.id)) return; // Resolve sender profile from cache or DB newMsg.sender = await resolveSender(newMsg.sender_id); liveMessages = [...liveMessages, newMsg]; }, ) .subscribe();From src/routes/(app)/honeycomb/messages/[conversationId]/+page.svelte:
const channel = supabase .channel(`dm:${conversation.id}`) .on( "postgres_changes", { event: "INSERT", schema: "ext_honeycomb", table: "direct_messages", filter: `conversation_id=eq.${conversation.id}`, }, async (payload) => { /* same pattern */ }, ) .subscribe();From src/lib/modules/messaging/stores/chat-store.svelte.ts:
channel = supabase .channel(`chat:${chatId}`) .on("postgres_changes", { event: "INSERT", schema: "public", table: "messages", filter: `chat_id=eq.${chatId}`, }, handleInsert) .on("postgres_changes", { event: "UPDATE", schema: "public", table: "messages", filter: `chat_id=eq.${chatId}`, }, handleUpdate) .subscribe();The chat store also supports UPDATE events so edited messages propagate instantly.
Deduplication
Section titled “Deduplication”Every handler checks whether the incoming message ID already exists in the local array before appending. This prevents duplicates when the same user has multiple subscriptions or when optimistic updates are in play.
Optimistic Updates
Section titled “Optimistic Updates”The chat-store implements optimistic message sending:
addOptimistic(content)inserts a temporary message with acrypto.randomUUID()ID into the local array.- When the real
INSERTevent arrives for the current user, the optimistic message is replaced with the server version. - The
pendingOptimisticIdsset tracks which temporary IDs are awaiting confirmation.
Presence — Typing Indicators
Section titled “Presence — Typing Indicators”Presence channels use Supabase’s built-in presence feature to track who is currently typing.
From src/lib/modules/messaging/components/typing-indicator.svelte:
channel = supabase.channel(`typing:${chatId}`, { config: { presence: { key: currentUserId } },});
channel .on("presence", { event: "sync" }, () => { const state = channel.presenceState(); typingUsers = Object.entries(state) .filter(([key]) => key !== currentUserId) .filter(([, presences]) => presences[0]?.typing) .map(([, presences]) => presences[0]?.name ?? "Someone"); }) .subscribe(async (status) => { if (status === "SUBSCRIBED") { await channel.track({ typing: false, name: currentUserName }); } });Typing State Management
Section titled “Typing State Management”startTyping()callschannel.track({ typing: true, name })and sets a 3-second timeout that automatically reverts tofalse.stopTyping()immediately tracks{ typing: false }and clears the timeout.- The UI renders bouncing dots with the names of users who are currently typing.
Presence State Store
Section titled “Presence State Store”The global presence state lives in src/lib/modules/honeycomb/presence/stores/presence-state.svelte.ts and uses Svelte 5 runes:
let onlineUsers = $state<Map<string, PresenceUser>>(new Map());let typingUsers = $state<Map<string, TypingState>>(new Map());Key methods:
| Method | Description |
|---|---|
setUserOnline(user) | Add user to the online map |
setUserOffline(userId) | Remove user from the online map |
setTyping(state) | Update typing state keyed by user_id:room_id or user_id:conversation_id |
isOnline(userId) | Check if a user is currently online |
getTypingInRoom(roomId) | Get all users typing in a room |
getTypingInConversation(id) | Get all users typing in a DM conversation |
type PresenceUser = { user_id: string; username: string; avatar_url: string | null; status: "online" | "idle" | "offline"; last_seen: string | null;};
type TypingState = { user_id: string; room_id: string | null; conversation_id: string | null; is_typing: boolean; updated_at: string;};Notification Channel
Section titled “Notification Channel”The notification store (src/lib/modules/notifications/stores/notification-store.svelte.ts) subscribes to new notification inserts to maintain a live unread count:
channel = supabase .channel(`notifications:${userId}`) .on("postgres_changes", { event: "INSERT", schema: "public", table: "notifications", filter: `notifiable_id=eq.${userId}`, }, () => { unreadCount++; }) .subscribe();Cleanup Pattern
Section titled “Cleanup Pattern”All channel subscriptions follow the same lifecycle pattern:
onMount(() => { const channel = supabase.channel("...").on(...).subscribe();
return () => { supabase.removeChannel(channel); };});Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/routes/(app)/honeycomb/rooms/[slug]/+page.svelte | Room message realtime subscription |
src/routes/(app)/honeycomb/messages/[conversationId]/+page.svelte | DM realtime subscription |
src/lib/modules/messaging/stores/chat-store.svelte.ts | Platform chat store with optimistic updates |
src/lib/modules/messaging/components/typing-indicator.svelte | Presence-based typing indicator |
src/lib/modules/honeycomb/presence/stores/presence-state.svelte.ts | Global presence state (Svelte 5 runes) |
src/lib/modules/notifications/stores/notification-store.svelte.ts | Live unread notification count |