Skip to content

Adding a Feature ​

STALE — the paths and the reference feature are both wrong (2026-08-08)

This recipe targets the retired Vite SPA: tiers/{tier}/features/{name} at the repo root, routes/index.jsx mounted from src/routes/index.jsx, and digital-menu as the feature to imitate. All of it is legacy/. digital-menu is additionally excluded from R1 and slated for an R2 re-home onto the Goods archetype, so it is the last thing to copy — it predates the data-access rule and is the single largest supabase.from() user in the repo.

Current shape: decide the app first (apps/mobile for the merchant product, apps/web for public + admin), then src/tiers/{tier}/features/{name}/{screens,hooks,utils}/ + an index.ts barrel

  • a mandatory README.md. reminders is the reference feature. Data logic goes in packages/ so it is written once. See tier system and data access.

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}_summary RPCs (SECURITY DEFINER, SET search_path = public, REVOKE ALL FROM PUBLIC → GRANT 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.