Skip to content

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}_initYYYYMMDDHHMMSS_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}_summary RPCs (SECURITY DEFINER, SET search_path = public, REVOKE ALL FROM PUBLICGRANT EXECUTE). Project row_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.jsx and mount it from src/routes/index.jsx (follow tiers/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.