Appearance
Adding a Feature
The end-to-end recipe. digital-menu is the reference feature — imitate it.
0. Decide the tier(s)
A feature with a dashboard and a public page owns both halves in parallel dirs; shared data-model types go in the shared core.
1. Scaffold the structure
tiers/{tier}/features/{name}/
pages/ components/ hooks/ services/
utils/ tests/ types/ schemas/ constants/Add constants/edgeFunctions.ts with the feature's EDGE_FN map even if empty for now.
2. Data model first (DB)
- Create tables via
npx supabase migration new {name}_init—YYYYMMDDHHMMSS_name.sql. - RLS on every table. Owner policy
auth.uid() = user_id; public read only where intended. - Append-only tables (analytics/logs) get no UPDATE/DELETE policy.
- Add
get_{entity}/get_{feature}_summaryRPCs (SECURITY DEFINER,SET search_path = public,REVOKE ALL FROM PUBLIC→GRANT EXECUTE). Projectrow_to_json(...). - Promote the migration to both Supabase projects. See Migrations.
3. Reads — hook + RPC
ts
// hooks/useThing.ts
export function useThing(id: string) {
return useQuery({
queryKey: ['thing', id],
queryFn: () => thingService.get(id), // wraps supabase.rpc('get_thing', { id })
staleTime: 60_000,
})
}Never call supabase.rpc() from a component — only from hooks/services.
4. Writes — service + EF
ts
// services/thingService.ts
import { EDGE_FN } from '../constants/edgeFunctions'
export const thingService = {
save: (body) => supabase.functions.invoke(EDGE_FN.MANAGE_THING, { body }),
}ts
// components — useMutation, invalidate on success
const m = useMutation({
mutationFn: thingService.save,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['thing'] }),
})If the write needs a secret / external HTTP / multi-step, it must be an Edge Function — see Adding an Edge Function.
5. UI
- Import UI primitives from your tier's
components/ui/(which wrap the global ones). - Apple-style soft corners (
rounded-3xl/rounded-2xl), zero hard-coded colors,cn()+isLight.
6. Routing
- User features: nest under
/dashboard/*in the user tier. - Public features: export
routes/index.jsxand mount it fromsrc/routes/index.jsx(followtiers/public/features/digital-menu/routes).
7. Tests (co-located tests/)
See Testing Strategy. First component test = "mounts without error" using the real component (never mock the component or its icon imports).
8. Done
Run the Definition of Done checklist, log a QRS-### for any debt found, update this portal, and verify in-app — not just via tests.