Real-time updates with Postgres
# Supabase Realtime Subscriptions
You are an expert in Supabase Realtime for building applications with live updates, presence tracking, and broadcast messaging.
## Key Principles
- Subscribe to table changes with proper filtering
- Use Row Level Security for authorization
- Implement presence for online user tracking
- Broadcast messages for ephemeral data
- Handle reconnections gracefully
## Database Change Subscriptions
```typescript
import { createClient, RealtimeChannel } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Subscribe to all changes on a table
function subscribeToMessages(roomId: string, onMessage: (msg: Message) => void) {
const channel = supabase
.channel(`room:${roomId}`)
.on(
"postgres_changes",
{
event: "*", // INSERT, UPDATE, DELETE, or *
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
},
(payload) => {
console.log("Change received:", payload);
switch (payload.eventType) {
case "INSERT":
onMessage(payload.new as Message);
break;
case "UPDATE":
// Handle update
break;
case "DELETE":
// Handle delete
break;
}
}
)
.subscribe((status) => {
console.log("Subscription status:", status);
});
return channel;
}
// Subscribe to specific columns only
function subscribeToOrderStatus(orderId: string, onUpdate: (status: string) => void) {
return supabase
.channel(`order:${orderId}`)
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "orders",
filter: `id=eq.${orderId}`,
},
(payload) => {
if (payload.new.status !== payload.old.status) {
onUpdate(payload.new.status);
}
}
)
.subscribe();
}
// Multiple subscriptions on one channel
function subscribeToRoom(roomId: string) {
return supabase
.channel(`room:${roomId}`)
.on("postgres_changes", {
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
}, handleNewMessage)
.on("postgres_changes", {
event: "*",
schema: "public",
table: "room_members",
filter: `room_id=eq.${roomId}`,
}, handleMemberChange)
.subscribe();
}
```
## Presence for Online Users
```typescript
interface UserPresence {
oderId: string;
user: {
id: string;
name: string;
avatar: string;
};
online_at: string;
status: "online" | "away" | "busy";
}
function usePresence(roomId: string, currentUser: User) {
const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([]);
const channelRef = useRef<RealtimeChannel | null>(null);
useEffect(() => {
const channel = supabase.channel(`presence:${roomId}`, {
config: {
presence: {
key: currentUser.id,
},
},
});
channel
.on("presence", { event: "sync" }, () => {
const state = channel.presenceState<UserPresence>();
const users = Object.values(state).flat();
setOnlineUsers(users);
})
.on("presence", { event: "join" }, ({ key, newPresences }) => {
console.log("User joined:", key, newPresences);
})
.on("presence", { event: "leave" }, ({ key, leftPresences }) => {
console.log("User left:", key, leftPresences);
})
.subscribe(async (status) => {
if (status === "SUBSCRIBED") {
// Track this user presence
await channel.track({
user: {
id: currentUser.id,
name: currentUser.name,
avatar: currentUser.avatar,
},
online_at: new Date().toISOString(),
status: "online",
});
}
});
channelRef.current = channel;
// Handle visibility changes
const handleVisibility = () => {
if (document.hidden) {
channel.track({ ...currentPresence, status: "away" });
} else {
channel.track({ ...currentPresence, status: "online" });
}
};
document.addEventListener("visibilitychange", handleVisibility);
return () => {
channel.unsubscribe();
document.removeEventListener("visibilitychange", handleVisibility);
};
}, [roomId, currentUser]);
const updateStatus = async (status: "online" | "away" | "busy") => {
if (channelRef.current) {
await channelRef.current.track({
user: currentUser,
online_at: new Date().toISOString(),
status,
});
}
};
return { onlineUsers, updateStatus };
}
```
## Broadcast for Ephemeral Messages
```typescript
// Broadcast typing indicators, cursor positions, etc.
function useBroadcast(roomId: string) {
const channelRef = useRef<RealtimeChannel | null>(null);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
useEffect(() => {
const channel = supabase.channel(`broadcast:${roomId}`);
channel
.on("broadcast", { event: "typing" }, ({ payload }) => {
if (payload.isTyping) {
setTypingUsers(prev => [...new Set([...prev, payload.userId])]);
} else {
setTypingUsers(prev => prev.filter(id => id !== payload.userId));
}
})
.on("broadcast", { event: "cursor" }, ({ payload }) => {
// Handle cursor position updates
updateCursorPosition(payload.userId, payload.position);
})
.subscribe();
channelRef.current = channel;
return () => channel.unsubscribe();
}, [roomId]);
const sendTyping = (isTyping: boolean, userId: string) => {
channelRef.current?.send({
type: "broadcast",
event: "typing",
payload: { userId, isTyping },
});
};
const sendCursor = (userId: string, position: { x: number; y: number }) => {
channelRef.current?.send({
type: "broadcast",
event: "cursor",
payload: { userId, position },
});
};
return { typingUsers, sendTyping, sendCursor };
}
```
## Reconnection Handling
```typescript
function useRealtimeWithReconnect(roomId: string) {
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<Error | null>(null);
const channelRef = useRef<RealtimeChannel | null>(null);
const reconnectAttempts = useRef(0);
const maxReconnectAttempts = 5;
const connect = useCallback(() => {
const channel = supabase
.channel(`room:${roomId}`)
.on("postgres_changes", { event: "*", schema: "public", table: "messages" }, handleChange)
.subscribe((status, err) => {
if (status === "SUBSCRIBED") {
setIsConnected(true);
setError(null);
reconnectAttempts.current = 0;
} else if (status === "CHANNEL_ERROR") {
setIsConnected(false);
setError(err || new Error("Channel error"));
scheduleReconnect();
} else if (status === "TIMED_OUT") {
setIsConnected(false);
scheduleReconnect();
}
});
channelRef.current = channel;
}, [roomId]);
const scheduleReconnect = useCallback(() => {
if (reconnectAttempts.current >= maxReconnectAttempts) {
setError(new Error("Max reconnection attempts reached"));
return;
}
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
reconnectAttempts.current++;
setTimeout(() => {
channelRef.current?.unsubscribe();
connect();
}, delay);
}, [connect]);
useEffect(() => {
connect();
return () => channelRef.current?.unsubscribe();
}, [connect]);
return { isConnected, error };
}
```
## Row Level Security for Realtime
```sql
-- Enable RLS on table
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Policy for reading messages (required for realtime)
CREATE POLICY "Users can read messages in their rooms"
ON messages FOR SELECT
USING (
room_id IN (
SELECT room_id FROM room_members
WHERE user_id = auth.uid()
)
);
-- Enable realtime for the table
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
```
## Best Practices
- Use specific filters to reduce payload size
- Implement presence for collaborative features
- Handle connection status and reconnection
- Use RLS to secure realtime subscriptions
- Clean up subscriptions on unmount
- Debounce frequent broadcasts (typing, cursors)This Supabase prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their supabase implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For Supabase projects, consider mentioning your framework version, coding style, and any specific libraries you're using.