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