Skip to content

Commit 55bb1ee

Browse files
committed
Scalability hardening: secure API keys, fix O(N²) realtime, cache auth token, reduce polling
CRITICAL-1 — Move PAGEINDEX and GROQ keys server-side - Create supabase/functions/api-page-index edge function to proxy all PageIndex and Groq calls, reading keys from Deno env (never in bundle) - Rewrite pageIndexService.ts to call the edge function via Supabase auth; remove VITE_PAGEINDEX_API_KEY and VITE_GROQ_API_KEY from frontend HIGH-1 — Scope active_sessions Realtime channel to current user - Add filter `user_id=eq.${user.id}` to postgres_changes subscription - Prevents O(N²) fan-out where 1M users receive every other user's events - Also add fetchActiveSessions to the cleanup interval so global count stays fresh via periodic polling when other users' sessions change HIGH-2 — Paginate initial call_history fetch - Add .limit(50) to the Supabase query; prevents loading 10–50 MB of JSON into browser memory for users with large call histories HIGH-3 — Cache Supabase auth token in api.ts interceptor - Module-level token + expiry cache; skips getSession() when token is fresh - Invalidated immediately via onAuthStateChange; re-fetched only when <60 s from expiry — eliminates 1M auth reads/s at peak load MEDIUM-1 — Remove 30 s KnowledgeBase polling - setInterval(testConnection, 30000) fired 33 K req/s at 1M users - Replaced with single on-mount check; button remains for manual re-test MEDIUM-2 — Throttle mousemove handlers via requestAnimationFrame - MousePosition.tsx and useMouseGradient.ts both fired setState on every raw mousemove (60+ Hz); wrapped in rAF to cap at display refresh rate MEDIUM-3 — Add jitter to token refresh interval - Fixed 60 s interval caused thundering herd (16 K Supabase auth hits/min) - Added Math.random() * 30_000 ms jitter to spread load across time https://claude.ai/code/session_01SGdxNUC1TVMDtW73TZbxjW
1 parent 97b8c67 commit 55bb1ee

File tree

7 files changed

+272
-123
lines changed

7 files changed

+272
-123
lines changed

src/components/DemoCall/KnowledgeBase.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,6 @@ export default function KnowledgeBase({ isDark = true, activeSection }: Knowledg
199199

200200
useEffect(() => {
201201
testConnection();
202-
const interval = setInterval(testConnection, 30000);
203-
return () => clearInterval(interval);
204202
}, []);
205203

206204
// Update local state and context

src/components/Landing/Particles/MousePosition.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ export function useMousePosition(): MousePosition {
1212
});
1313

1414
useEffect(() => {
15+
let rafId: number;
1516
const handleMouseMove = (event: MouseEvent) => {
16-
setMousePosition({ x: event.clientX, y: event.clientY });
17+
cancelAnimationFrame(rafId);
18+
rafId = requestAnimationFrame(() => {
19+
setMousePosition({ x: event.clientX, y: event.clientY });
20+
});
1721
};
1822

1923
window.addEventListener("mousemove", handleMouseMove);
20-
return () => window.removeEventListener("mousemove", handleMouseMove);
24+
return () => {
25+
window.removeEventListener("mousemove", handleMouseMove);
26+
cancelAnimationFrame(rafId);
27+
};
2128
}, []);
2229

2330
return mousePosition;

src/config/api.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,35 @@ const api = axios.create({
1717
withCredentials: true, // Important for cookies/sessions
1818
});
1919

20+
// ── Token cache — avoids a Supabase auth read on every request ───────────────
21+
// At scale (1M users × N requests/s) calling getSession() per request is
22+
// prohibitively expensive. Cache the token and only refresh when near expiry.
23+
let _cachedToken: string | null = null;
24+
let _tokenExpiry = 0;
25+
26+
supabase.auth.onAuthStateChange((_event, session) => {
27+
_cachedToken = session?.access_token ?? null;
28+
_tokenExpiry = session ? session.expires_at! * 1000 : 0;
29+
});
30+
31+
async function getCachedToken(): Promise<string | null> {
32+
// Return cached token if it won't expire in the next 60 seconds
33+
if (_cachedToken && Date.now() < _tokenExpiry - 60_000) return _cachedToken;
34+
try {
35+
const { data: { session } } = await supabase.auth.getSession();
36+
_cachedToken = session?.access_token ?? null;
37+
_tokenExpiry = session ? session.expires_at! * 1000 : 0;
38+
} catch {
39+
_cachedToken = null;
40+
}
41+
return _cachedToken;
42+
}
43+
2044
// Inject Supabase auth token into every request
2145
api.interceptors.request.use(async (config) => {
2246
try {
23-
const { data: { session } } = await supabase.auth.getSession();
24-
if (session?.access_token) {
25-
config.headers.Authorization = `Bearer ${session.access_token}`;
26-
}
47+
const token = await getCachedToken();
48+
if (token) config.headers.Authorization = `Bearer ${token}`;
2749
} catch {
2850
// Silently proceed without token if auth check fails
2951
}

src/contexts/DemoCallContext.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
379379
.from('call_history')
380380
.select('*')
381381
.eq('user_id', user.id)
382-
.order('date', { ascending: false });
382+
.order('date', { ascending: false })
383+
.limit(50);
383384

384385
if (!historyError && historyData && historyData.length > 0) {
385386
// Map Supabase snake_case to app camelCase
@@ -582,9 +583,9 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
582583
// Initial cleanup then fetch
583584
cleanupStaleSessions().then(() => fetchActiveSessions());
584585

585-
// Periodically cleanup stale sessions every minute
586+
// Periodically cleanup stale sessions and refresh global count every minute
586587
const cleanupInterval = setInterval(() => {
587-
cleanupStaleSessions();
588+
cleanupStaleSessions().then(() => fetchActiveSessions());
588589
}, 60 * 1000);
589590

590591
// Subscribe to changes (debounced to avoid UI thrashing under high load)
@@ -597,10 +598,10 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
597598
};
598599

599600
const subscription = supabase
600-
.channel('active_sessions_changes')
601+
.channel('active_sessions_global_count')
601602
.on(
602603
'postgres_changes',
603-
{ event: '*', schema: 'public', table: 'active_sessions' },
604+
{ event: '*', schema: 'public', table: 'active_sessions', filter: `user_id=eq.${user?.id ?? ''}` },
604605
(payload) => {
605606
console.log('📡 Active sessions change:', payload.eventType);
606607
debouncedFetch();
@@ -817,7 +818,9 @@ export function DemoCallProvider({ children, initialAgentId }: { children: React
817818
}
818819
};
819820
updateToken();
820-
const interval = setInterval(updateToken, 60000); // Refresh every 60s
821+
// Jitter prevents 1M sessions all refreshing at the same wall-clock second
822+
const jitter = Math.random() * 30_000; // 0–30s random offset
823+
const interval = setInterval(updateToken, 60_000 + jitter);
821824
return () => clearInterval(interval);
822825
}, []);
823826

src/hooks/useMouseGradient.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@ export function useMouseGradient() {
99
const [mousePosition, setMousePosition] = useState<MousePosition>({ x: 0, y: 0 });
1010

1111
useEffect(() => {
12+
let rafId: number;
1213
const handleMouseMove = (e: MouseEvent) => {
13-
const x = (e.clientX / window.innerWidth) * 100;
14-
const y = (e.clientY / window.innerHeight) * 100;
15-
setMousePosition({ x, y });
14+
cancelAnimationFrame(rafId);
15+
rafId = requestAnimationFrame(() => {
16+
const x = (e.clientX / window.innerWidth) * 100;
17+
const y = (e.clientY / window.innerHeight) * 100;
18+
setMousePosition({ x, y });
19+
});
1620
};
1721

1822
window.addEventListener('mousemove', handleMouseMove);
19-
return () => window.removeEventListener('mousemove', handleMouseMove);
23+
return () => {
24+
window.removeEventListener('mousemove', handleMouseMove);
25+
cancelAnimationFrame(rafId);
26+
};
2027
}, []);
2128

2229
const gradientStyle = {

0 commit comments

Comments
 (0)