Skip to main content

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;
},
});
Key Points
  • queryKey determines 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' });
},
});
Invalidation Strategy

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.

Deduplication

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​

PatternSourceMechanismDestination
ReadPostgreSQLuseQuery + supabase.from().select()React component
WriteReact componentuseMutation + supabase.from().insert/update/delete()PostgreSQL
Real-timePostgreSQLsupabase.channel().on('postgres_changes')React Query cache invalidation
Edge functionReact componentsupabase.functions.invoke()External API + PostgreSQL
Webhook ingestExternal serviceHTTP POST to edge function URLPostgreSQL