Enforces DB-first architecture rules for Supabase frontend code. Use when writing hooks, queries, or components that interact with the database. Triggers on mentions of Supabase, RLS, hooks, or database operations.
NO DIRECT SUPABASE QUERIES WITHOUT HOOKS
Every database interaction MUST go through a custom hook from src/hooks/. Period.
This is not negotiable, has no exceptions, and applies to all contexts:
Why: Hooks encapsulate RLS expectations, error handling, and TypeScript contracts. Direct queries bypass these guarantees.
The frontend is a strict client of the Supabase DB. It reads what the DB authorizes, executes writes best-effort, and handles refusals gracefully. The DB is the single source of truth for all authorization and business logic.
service_roleuser.role, hasPermission, checkAccess, or permission matricessubscriptions, consent_events, admin_audit_log) — use Edge FunctionsIf you see ANY of these patterns in frontend code, STOP immediately and refactor:
// ❌ Direct Supabase query without hook
const { data } = await supabase.from('tasks').select()
// ❌ Client-side authorization logic
if (user.role === 'admin') { ... }
if (account.status === 'subscriber' && canEdit) { ... }
// ❌ Business logic calculations
const remainingQuota = MAX_TASKS - userTasks.length
const canCreate = currentEpoch < maxEpoch
// ❌ service_role usage
const supabase = createClient(url, SERVICE_ROLE_KEY)
// ❌ RLS bypass attempts
.select('*, secret_field') // "I'll just not display it"
Action: Replace with appropriate hook from src/hooks/ or create a new one if missing.
accounts.status usageaccounts.status (free | subscriber | admin) is read for display only:
It is never used as authorization. If a button is visible by mistake and the user clicks, the DB refuses and the frontend handles it.
Before writing ANY database interaction code, ask yourself:
Is there already a hook for this?
src/hooks/ firstsrc/hooks/CLAUDE.md for inventoryDoes this operation require authorization?
Am I trying to enforce a rule?
Am I calculating business values?
Only proceed if all 4 questions pass.
1. Client attempts Supabase operation
2. DB evaluates RLS policy
3. If authorized → operation succeeds
4. If refused → error returned to client
5. Frontend displays context-appropriate state:
- Édition: explicit non-technical message
- Tableau: neutral screen (NEVER technical)
validated_at rulesession_validations.validated_at is audit-only. Never use it in any frontend logic (sorting, filtering, duration calculation, display).
Visitor is local-only — no DB row exists. No accounts.status = 'visitor'. Data in IndexedDB only. Only allowed DB call: read published bank cards.
| Excuse | Why it's wrong | Correct approach |
|---|---|---|
| "It's just a quick read, no need for a hook" | Bypasses error handling, TypeScript contract, RLS expectations | Always use hook. Create one if missing (5 min) |
"I'll check account.status first, then query" | Frontend check ≠ authorization. User can bypass. | Let RLS refuse. Handle error gracefully. |
| "This field is read-only, safe to select" | RLS may hide it. Direct select breaks contract. | Use hook with explicit TypeScript return type. |
"I need service_role for admin features" | Admin = user with admin status. Still uses RLS. | Admin RLS policies grant access. Never bypass. |
| "Calculating quota client-side is faster" | Creates race conditions, inconsistent state. | DB calculates via RLS or function. Frontend displays. |
| "Just this once for prototyping" | Prototype becomes production. Technical debt. | Prototype with hooks. Refactor effort = same. |
Remember: Every shortcut creates a future bug, security hole, or architectural violation. DB-first is non-negotiable because it's the ONLY pattern that scales safely.
For complete contract details and edge cases, see FRONTEND_CONTRACT.md: