Appearance
Data Access Strategy
The single hard rule
supabase.from() is NEVER called from any client-side file — components, hooks, services, pages, anywhere in src/. No exceptions.
This keeps table names and schema off the network wire:
Browser Network tab sees:
✅ /rest/v1/rpc/get_nifty50_market_mood ← function name only, no schema
✅ /functions/v1/trade-planner-save ← EF name only, no table or secret
❌ /rest/v1/trade_planner_plans ← table name exposed — NEVERTwo complementary patterns
RPC (supabase.rpc()) | Edge Function (supabase.functions.invoke()) | |
|---|---|---|
| Operation | SELECT only | INSERT / UPDATE / DELETE / enriched SELECT |
| Secrets | No | Yes (service role, Cloudflare, Razorpay…) |
| External HTTP | No | Yes |
| Multi-step / webhook / cron | No | Yes |
| Latency | Low (Postgres direct) | Higher (Deno cold start) |
| Client location | hooks/use{Feature}.js only | services/{feature}Service.js only |
Decision rule: pure SQL read + no secrets + no external HTTP → RPC. Everything else → Edge Function.
RPC hook pattern (primary read pattern)
js
// hooks/useFeatureData.js
import { useState, useEffect, useRef, useCallback } from 'react';
import { supabase } from '@/lib/supabaseClient';
import { getFromCache, setToCache } from '@/lib/cache';
const CACHE_KEY = 'feature_data'; // module-level constant — never inline
const CACHE_TTL = 2; // minutes
const POLL_MS = 5 * 60 * 1000;
export const useFeatureData = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const hasLoaded = useRef(false); // loading=true only for the FIRST fetch
const fetchData = useCallback(async () => {
const cached = getFromCache(CACHE_KEY, CACHE_TTL);
if (cached) {
setData(cached);
if (!hasLoaded.current) { setLoading(false); hasLoaded.current = true; }
return;
}
const { data: rpcData, error: rpcError } = await supabase.rpc('get_feature_data');
if (rpcError) setError(rpcError.message);
else { setToCache(CACHE_KEY, rpcData); setData(rpcData); setError(null); }
if (!hasLoaded.current) { setLoading(false); hasLoaded.current = true; }
}, []);
useEffect(() => { fetchData(); }, [fetchData]);
return { data, loading, error, refetch: fetchData };
};One hook per RPC. Always export refetch. Add supabase.channel() for frequently-INSERTed tables and visibility polling for live/intraday data.
Edge Function service pattern (all writes + secret reads)
js
// services/featureService.js
import { supabase } from '@/lib/supabaseClient';
import { EDGE_FN } from '../config/featureConfig'; // never inline EF name strings
export const featureService = {
async save(payload) {
const { data, error } = await supabase.functions.invoke(EDGE_FN.SAVE, { body: payload });
if (error) throw error;
return data;
},
};Cache TTL guidelines
| Data type | TTL |
|---|---|
| Live intraday / OI chain | 1–2 min |
| Tab summaries / breadth | 1 min |
| Historical OHLC | 60 min |
| User profile / settings | 5 min |
| Static reference / expiry maps | 24 hr |
Placement rules (absolute)
| What | Where | Never |
|---|---|---|
supabase.rpc() | hooks/use{Feature}.js | components, services, pages, JSX |
supabase.functions.invoke() | services/{feature}Service.js | components, hooks, pages, JSX |
supabase.from() | nowhere in src/ | absolutely forbidden |
Canonical reference implementation: Market Mood.