Data Flow
This page documents the primary data flow patterns in Client Portal: how data moves from the database to the UI, how mutations work, how real-time updates propagate, and how external webhooks are ingested.
React Query Data Fetching
All reads use TanStack React Query wrapping Supabase client calls. The pattern is consistent across the entire application:
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
const { data, isLoading, error } = useQuery({
queryKey: ['campaigns', clientId],
queryFn: async () => {
const { data, error } = await supabase
.from('campaigns')
.select('*, contacts(count)')
.eq('client_id', clientId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
queryKeydetermines cache identity. Always include relevant IDs/filters.- Supabase errors are thrown to trigger React Query's error handling.
- Stale time and refetch behavior use React Query defaults unless overridden.
Mutation Pattern
Writes follow the useMutation pattern with cache invalidation:
import { useMutation, useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const updateCampaign = useMutation({
mutationFn: async (values: CampaignUpdate) => {
const { data, error } = await supabase
.from('campaigns')
.update(values)
.eq('id', campaignId)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['campaigns'] });
toast({ title: 'Campaign updated' });
},
});
After a mutation, invalidateQueries marks cached data as stale and triggers a background refetch. This ensures the UI reflects the latest state without manual cache updates.
Real-time Subscriptions
Supabase real-time channels listen for PostgreSQL changes and automatically invalidate React Query caches:
useEffect(() => {
const channel = supabase
.channel('calls-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'calls',
filter: `campaign_id=eq.${campaignId}`,
},
() => {
queryClient.invalidateQueries({ queryKey: ['calls', campaignId] });
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [campaignId, queryClient]);
Edge Function Invocation
Edge functions are called from the client using supabase.functions.invoke():
const { data, error } = await supabase.functions.invoke('summarize-sales-call', {
body: { callId: call.id },
});
Edge functions receive the user's JWT automatically and can use the Supabase Admin client for privileged operations.
Sequence Diagrams
1. User Loads Campaign List
2. GHL Call Webhook to AI Summary
This is the full pipeline when a GHL call completes: the webhook fires, the call is logged, and if a transcript is present, AI summarization is triggered automatically.
GHL fires two webhook hits per call (Call Completed + Disposition workflows). The edge function deduplicates using external_call_id -- if a record with that ID already exists, the second webhook updates rather than inserts.
3. Calendar Booking Flow
This diagram shows the full flow when a prospect books a meeting through a booking link.
External Webhook Flow
External services push data into Client Portal via edge function endpoints. All webhook edge functions have verify_jwt = false in supabase/config.toml so they can accept unauthenticated requests.
Data Flow Summary
| Pattern | Source | Mechanism | Destination |
|---|---|---|---|
| Read | PostgreSQL | useQuery + supabase.from().select() | React component |
| Write | React component | useMutation + supabase.from().insert/update/delete() | PostgreSQL |
| Real-time | PostgreSQL | supabase.channel().on('postgres_changes') | React Query cache invalidation |
| Edge function | React component | supabase.functions.invoke() | External API + PostgreSQL |
| Webhook ingest | External service | HTTP POST to edge function URL | PostgreSQL |