Real-time data synchronization with Supabase including presence, broadcasts, and database changes
# Supabase Realtime Patterns for Google Antigravity
Build real-time applications with Supabase using Google Antigravity's Gemini 3 engine. This guide covers presence tracking, broadcast messaging, database change subscriptions, and channel management.
## Supabase Client Setup
```typescript
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
import type { Database } from '@/types/database';
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
import type { Database } from '@/types/database';
export function createServerSupabaseClient() {
const cookieStore = cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options });
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options });
},
},
}
);
}
```
## Real-time Database Subscriptions
```typescript
// hooks/use-realtime-messages.ts
import { useEffect, useState, useCallback } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { RealtimePostgresChangesPayload } from '@supabase/supabase-js';
interface Message {
id: string;
content: string;
user_id: string;
channel_id: string;
created_at: string;
user: { name: string; avatar_url: string };
}
export function useRealtimeMessages(channelId: string) {
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(true);
const supabase = createClient();
// Fetch initial messages
useEffect(() => {
async function fetchMessages() {
const { data, error } = await supabase
.from('messages')
.select('*, user:users(name, avatar_url)')
.eq('channel_id', channelId)
.order('created_at', { ascending: true })
.limit(100);
if (!error && data) {
setMessages(data as Message[]);
}
setIsLoading(false);
}
fetchMessages();
}, [channelId, supabase]);
// Subscribe to real-time changes
useEffect(() => {
const channel = supabase
.channel(`messages:${channelId}`)
.on<Message>(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
async (payload: RealtimePostgresChangesPayload<Message>) => {
const newMessage = payload.new as Message;
// Fetch user data for new message
const { data: user } = await supabase
.from('users')
.select('name, avatar_url')
.eq('id', newMessage.user_id)
.single();
setMessages((prev) => [
...prev,
{ ...newMessage, user: user || { name: 'Unknown', avatar_url: '' } },
]);
}
)
.on<Message>(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
(payload) => {
const updatedMessage = payload.new as Message;
setMessages((prev) =>
prev.map((msg) =>
msg.id === updatedMessage.id ? { ...msg, ...updatedMessage } : msg
)
);
}
)
.on<Message>(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
(payload) => {
const deletedId = (payload.old as Message).id;
setMessages((prev) => prev.filter((msg) => msg.id !== deletedId));
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [channelId, supabase]);
const sendMessage = useCallback(
async (content: string, userId: string) => {
const { error } = await supabase.from('messages').insert({
content,
user_id: userId,
channel_id: channelId,
});
if (error) throw error;
},
[channelId, supabase]
);
return { messages, isLoading, sendMessage };
}
```
## Presence Tracking
```typescript
// hooks/use-presence.ts
import { useEffect, useState, useCallback } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { RealtimePresenceState } from '@supabase/supabase-js';
interface PresenceUser {
id: string;
name: string;
avatar_url: string;
online_at: string;
status: 'online' | 'away' | 'busy';
}
export function usePresence(roomId: string, currentUser: PresenceUser) {
const [presenceState, setPresenceState] = useState<PresenceUser[]>([]);
const supabase = createClient();
useEffect(() => {
const channel = supabase.channel(`room:${roomId}`);
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<PresenceUser>();
const users = Object.values(state).flat();
setPresenceState(users);
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', newPresences);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', leftPresences);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
...currentUser,
online_at: new Date().toISOString(),
});
}
});
return () => {
channel.untrack();
supabase.removeChannel(channel);
};
}, [roomId, currentUser, supabase]);
const updateStatus = useCallback(
async (status: PresenceUser['status']) => {
const channel = supabase.channel(`room:${roomId}`);
await channel.track({
...currentUser,
status,
online_at: new Date().toISOString(),
});
},
[roomId, currentUser, supabase]
);
return { presenceState, updateStatus };
}
```
## Broadcast Messaging
```typescript
// hooks/use-broadcast.ts
import { useEffect, useCallback } from 'react';
import { createClient } from '@/lib/supabase/client';
interface BroadcastEvent<T = any> {
type: string;
payload: T;
}
export function useBroadcast<T>(channelName: string) {
const supabase = createClient();
const broadcast = useCallback(
async (event: BroadcastEvent<T>) => {
const channel = supabase.channel(channelName);
await channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
channel.send({
type: 'broadcast',
event: event.type,
payload: event.payload,
});
}
});
},
[channelName, supabase]
);
const subscribe = useCallback(
(eventType: string, callback: (payload: T) => void) => {
const channel = supabase
.channel(channelName)
.on('broadcast', { event: eventType }, ({ payload }) => {
callback(payload as T);
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
},
[channelName, supabase]
);
return { broadcast, subscribe };
}
// Example: Typing indicators
interface TypingPayload {
userId: string;
userName: string;
isTyping: boolean;
}
export function useTypingIndicator(channelId: string, currentUser: { id: string; name: string }) {
const { broadcast, subscribe } = useBroadcast<TypingPayload>(`typing:${channelId}`);
const [typingUsers, setTypingUsers] = useState<Map<string, string>>(new Map());
useEffect(() => {
const unsubscribe = subscribe('typing', (payload) => {
setTypingUsers((prev) => {
const next = new Map(prev);
if (payload.isTyping) {
next.set(payload.userId, payload.userName);
} else {
next.delete(payload.userId);
}
return next;
});
});
return unsubscribe;
}, [subscribe]);
const setTyping = useCallback(
(isTyping: boolean) => {
broadcast({
type: 'typing',
payload: {
userId: currentUser.id,
userName: currentUser.name,
isTyping,
},
});
},
[broadcast, currentUser]
);
return { typingUsers: Array.from(typingUsers.values()), setTyping };
}
```
## Best Practices
Google Antigravity's Gemini 3 engine recommends these Supabase Realtime patterns: Use channel filters to reduce unnecessary data transfer. Implement presence for collaborative features. Clean up subscriptions on component unmount. Use broadcast for ephemeral events like typing indicators. Combine database subscriptions with initial data fetches for complete state.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.