Appearance
Data Access Strategy
This is the single most important rule in the codebase.
supabase.from() is banned in src/. It keeps table names and schema off the network wire. Every data path is one of exactly two shapes.
The decision rule
Pure SQL read + no secrets + no external HTTP → RPC. Everything else → Edge Function.
Reads → TanStack Query + RPC
useQuerycalls a portable service that wrapssupabase.rpc('get_…').supabase.rpc()appears only in hooks/services, never in components/pages/JSX.- RPCs are
SECURITY DEFINER+SET search_path = public+REVOKE ALL FROM PUBLIC/GRANT EXECUTE. - Standard hook shape:
{ data, isLoading, error, refetch }, per-featurequeryKey,staleTimeby data type. - Use
row_to_json(table.*)in RPCs for forward-compat (don't project bare column lists that leak schema shape).
Writes / secrets / external HTTP / multi-step → Edge Function
useMutationcallsservices/{feature}Service.ts→supabase.functions.invoke(EDGE_FN.X).supabase.functions.invoke()appears only in services.- Invalidate queries on success so reads refetch.
- EF names come from a per-feature
EDGE_FNconfig map — never inline strings.
ts
// constants/edgeFunctions.ts (per feature)
export const EDGE_FN = {
MANAGE_PROFILE: 'manage-profile',
VALIDATE_INPUT: 'validate-user-input',
} as const
// services/profileService.ts
import { EDGE_FN } from '../constants/edgeFunctions'
const { data, error } = await supabase.functions.invoke(EDGE_FN.MANAGE_PROFILE, { body })Defense in depth
RLS is not the primary write gate — it is defense-in-depth. The Edge Function is the primary write-enforcement layer. Both are always required: an EF validates + authorizes + mutates, and RLS still guards the table underneath.
[TRANSITIONAL] — the migration in progress
- ~56 files still call
from()directly. Being migrated feature-by-feature with parity verification. Do not add newfrom()calls. - Two caching layers coexist: TanStack Query and
src/lib/cacheUtils.js. Converge on TanStack Query and retirecacheUtils.js. - A "never calls
from()" compliance test is part of the target test model (see Testing Strategy).
Why this matters
| Benefit | How the rule delivers it |
|---|---|
| Security | Table names/schema never appear on the wire; RLS + EF enforce access twice. |
| Portability | Services/hooks are DOM-free → the future Expo app reuses them unchanged. |
| Performance | Composite RPCs avoid ≥3 parallel read RPCs saturating the pool on multi-section screens. |
| Refactor safety | row_to_json projections are forward-compatible; callers don't break on column changes. |