Build live updating features in Google Antigravity with Supabase Realtime and WebSocket patterns.
# Realtime Subscriptions for Google Antigravity
Build live updating features with Supabase Realtime and WebSocket patterns.
## Basic Subscription
```typescript
// hooks/useRealtimeSubscription.ts
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { RealtimeChannel, RealtimePostgresChangesPayload } from "@supabase/supabase-js";
export function useRealtimeSubscription<T extends { id: string }>(
table: string,
filter?: { column: string; value: string }
) {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const supabase = createClient();
useEffect(() => {
let channel: RealtimeChannel;
async function setup() {
// Initial fetch
let query = supabase.from(table).select("*");
if (filter) query = query.eq(filter.column, filter.value);
const { data: initial } = await query;
setData((initial as T[]) || []);
setLoading(false);
// Subscribe to changes
const channelConfig = filter ? { event: "*", schema: "public", table, filter: `${filter.column}=eq.${filter.value}` } : { event: "*", schema: "public", table };
channel = supabase.channel(`${table}_changes`).on("postgres_changes", channelConfig as any, (payload: RealtimePostgresChangesPayload<T>) => {
switch (payload.eventType) {
case "INSERT":
setData((prev) => [...prev, payload.new as T]);
break;
case "UPDATE":
setData((prev) => prev.map((item) => (item.id === (payload.new as T).id ? (payload.new as T) : item)));
break;
case "DELETE":
setData((prev) => prev.filter((item) => item.id !== (payload.old as T).id));
break;
}
}).subscribe();
}
setup();
return () => { channel?.unsubscribe(); };
}, [table, filter?.column, filter?.value, supabase]);
return { data, loading };
}
```
## Presence for Online Users
```typescript
// hooks/usePresence.ts
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { RealtimeChannel } from "@supabase/supabase-js";
interface PresenceState {
id: string;
name: string;
avatar?: string;
status: "online" | "away";
}
export function usePresence(roomId: string, user: { id: string; name: string; avatar?: string }) {
const [users, setUsers] = useState<PresenceState[]>([]);
const supabase = createClient();
useEffect(() => {
const channel = supabase.channel(`room:${roomId}`);
channel
.on("presence", { event: "sync" }, () => {
const state = channel.presenceState<PresenceState>();
const presenceUsers = Object.values(state).flat();
setUsers(presenceUsers);
})
.subscribe(async (status) => {
if (status === "SUBSCRIBED") {
await channel.track({ id: user.id, name: user.name, avatar: user.avatar, status: "online" });
}
});
return () => { channel.unsubscribe(); };
}, [roomId, user, supabase]);
return users;
}
```
## Broadcast Messages
```typescript
// hooks/useBroadcast.ts
"use client";
import { useEffect, useCallback } from "react";
import { createClient } from "@/lib/supabase/client";
export function useBroadcast<T>(channel: string, onMessage: (payload: T) => void) {
const supabase = createClient();
useEffect(() => {
const ch = supabase.channel(channel).on("broadcast", { event: "message" }, ({ payload }) => { onMessage(payload as T); }).subscribe();
return () => { ch.unsubscribe(); };
}, [channel, onMessage, supabase]);
const send = useCallback(async (payload: T) => {
await supabase.channel(channel).send({ type: "broadcast", event: "message", payload });
}, [channel, supabase]);
return send;
}
```
## Live Chat Component
```typescript
// components/LiveChat.tsx
"use client";
import { useState, useEffect, useRef } from "react";
import { useRealtimeSubscription } from "@/hooks/useRealtimeSubscription";
import { createClient } from "@/lib/supabase/client";
interface Message {
id: string;
room_id: string;
user_id: string;
content: string;
created_at: string;
}
export function LiveChat({ roomId, userId }: { roomId: string; userId: string }) {
const { data: messages } = useRealtimeSubscription<Message>("messages", { column: "room_id", value: roomId });
const [newMessage, setNewMessage] = useState("");
const scrollRef = useRef<HTMLDivElement>(null);
const supabase = createClient();
useEffect(() => {
scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight);
}, [messages]);
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!newMessage.trim()) return;
await supabase.from("messages").insert({ room_id: roomId, user_id: userId, content: newMessage });
setNewMessage("");
};
return (
<div className="chat-container">
<div ref={scrollRef} className="messages">
{messages.map((msg) => (
<div key={msg.id} className={`message ${msg.user_id === userId ? "own" : ""}`}>
<p>{msg.content}</p>
</div>
))}
</div>
<form onSubmit={sendMessage}>
<input value={newMessage} onChange={(e) => setNewMessage(e.target.value)} placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>
);
}
```
## Typing Indicators
```typescript
// hooks/useTypingIndicator.ts
import { useEffect, useState, useCallback } from "react";
import { createClient } from "@/lib/supabase/client";
import debounce from "lodash.debounce";
export function useTypingIndicator(roomId: string, userId: string) {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const supabase = createClient();
useEffect(() => {
const channel = supabase.channel(`typing:${roomId}`).on("broadcast", { event: "typing" }, ({ payload }) => {
if (payload.userId !== userId) {
setTypingUsers((prev) => payload.isTyping ? [...new Set([...prev, payload.userId])] : prev.filter((id) => id !== payload.userId));
}
}).subscribe();
return () => { channel.unsubscribe(); };
}, [roomId, userId, supabase]);
const setTyping = useCallback(debounce(async (isTyping: boolean) => {
await supabase.channel(`typing:${roomId}`).send({ type: "broadcast", event: "typing", payload: { userId, isTyping } });
}, 300), [roomId, userId, supabase]);
return { typingUsers, setTyping };
}
```
## Best Practices
1. **Cleanup**: Always unsubscribe when components unmount
2. **Optimistic Updates**: Update UI immediately, then sync
3. **Error Handling**: Handle reconnection gracefully
4. **Rate Limiting**: Debounce frequent updates
5. **Presence TTL**: Set appropriate timeouts for presenceThis realtime 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 realtime 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 realtime projects, consider mentioning your framework version, coding style, and any specific libraries you're using.