Skip to content

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 — NEVER

Two complementary patterns

RPC (supabase.rpc())Edge Function (supabase.functions.invoke())
OperationSELECT onlyINSERT / UPDATE / DELETE / enriched SELECT
SecretsNoYes (service role, Cloudflare, Razorpay…)
External HTTPNoYes
Multi-step / webhook / cronNoYes
LatencyLow (Postgres direct)Higher (Deno cold start)
Client locationhooks/use{Feature}.js onlyservices/{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 typeTTL
Live intraday / OI chain1–2 min
Tab summaries / breadth1 min
Historical OHLC60 min
User profile / settings5 min
Static reference / expiry maps24 hr

Placement rules (absolute)

WhatWhereNever
supabase.rpc()hooks/use{Feature}.jscomponents, services, pages, JSX
supabase.functions.invoke()services/{feature}Service.jscomponents, hooks, pages, JSX
supabase.from()nowhere in src/absolutely forbidden

Canonical reference implementation: Market Mood.