Data Layer
Convex hook wrappers, response flattening, and reactivity patterns.
The barrel
src/hooks/convex/index.ts re-exports per-domain files: use-dashboard, use-loyalty (the big one), use-users, use-orders, use-products, use-content, use-notifications, use-ratings, use-duit-raya, use-webhooks, use-webhook-events (delivery pipeline rows, not subscriptions), use-beauty, use-support-messages, use-audit-log, and the R2 trio (use-r2-url, use-r2-files, use-r2-upload). types.ts exports Doc<'table'> / Id<'table'> aliases and Enriched* response types (the backend transforms _id → id).
Hook shape: thin adapters returning { data, isLoading: response === undefined }; mutations are often returned directly.
Response flattening
The backend returns mixed shapes — hooks defensively handle all of them:
- Wrapped envelope
{ success, data, message }→ unwrap (seeuseUserDetail) - Direct document objects → use as-is
- Direct payloads with no envelope (e.g.
dashboardGetUserStatsreturns stats directly)
use-loyalty.ts also carries field normalizers mapping backend field names to frontend form names. When the backend response shape of an endpoint changes, fix it in the hook, not the component.
Reactivity rules
- Server state is Convex subscriptions — never manually refetch.
- Conditional queries pass
'skip':useQuery(api.x.y, ready ? args : 'skip'). - Search inputs must use
useDebounce(~300 ms).useDeferredValueis banned for Convex args — it doesn't prevent subscription churn. - Memoize callbacks passed to children; use skip-initial-mount refs for side effects.
use-auth.tsadditionally guards the transient null: a profile that regresses tonullmid-session is treated as loading (5 s timeout), because Better Auth session updates can briefly get ahead of the Convex auth token.
ID and casing conventions
_id= Convex doc ID (aliasedid);userId= Better Auth ID. User pages navigate byuserId.- Statuses are lowercase on the wire — hooks
.toLowerCase()before mutation calls. Role casing is lowercase everywhere (normalizeRole()for unknown input).