UI components
shadcn/ui initialized with Base UI (not Radix)
Decision: npx shadcn@latest init -d chose base-nova style with Base UI primitives (not Radix). This means:
- Components use
renderprop for polymorphism, notasChild - Imports come from
@base-ui/react/*, not@radix-ui/react-* - Example:
<SidebarMenuButton render={<Link href="/" />}>instead of<SidebarMenuButton asChild><Link href="/" /></SidebarMenuButton>
Next.js conventions
proxy.ts uses proxy export (Next.js 16)
Decision: Next.js 16 renamed middleware.ts to proxy.ts. The exported function must be named proxy (not middleware). The file lives at src/proxy.ts.
Access control
RLS access control model
Decision: Supabase RLS is the primary access control layer. Every household-scoped table has policies enforcing household_id in (select get_user_household_ids()).
Authenticated client actions (createClient() from @supabase/ssr): Safe by default. RLS prevents cross-household reads/writes. No additional ownership checks needed.
Admin client actions (createAdminClient() from service role key): Bypass RLS entirely. Used only where deeply nested RLS queries fail (shopping list chain: items → lists → plans → cycles → households). Every admin client action MUST:
- Call
requireHousehold()to verify the user is authenticated with a household - Explicitly verify the target resource belongs to the user's household before operating
API routes: Must verify household membership since they don't use requireHousehold() middleware. The proxy handles auth session refresh but route handlers must check membership explicitly.
Data model
Post-meal feedback uses recipe_bookmarks (not a new table)
Decision: For the post-meal feedback loop (#148), feedback is stored in the existing recipe_bookmarks table using bookmark_type='tried' + rating (1–5) rather than creating a new table.
Rationale:
recipe_bookmarksalready hasrating,note, andbookmark_type='tried'— all unused but purpose-built for this- RLS policies already exist
- The ranking engine already processes bookmarks — extending is simpler than adding a new data source
- Added
meal_plan_entry_idFK to link feedback to specific plan entries (not just recipe globally) - Plan-level skip tracking goes on
recipe_history.skipped(boolean) since it's about the plan, not the recipe
Feature design
Pantry MVP is names-only, manual management, highlight-not-remove
Decision: For the pantry/inventory feature (#165), MVP design decisions:
- Names + optional quantity text only. No expiry dates, no barcode scanning, no unit conversion. Keep it simple.
- Manual management only. No auto-depletion when cooking. Users add/remove items themselves.
- Shopping list shows "in pantry" badge but does NOT auto-remove matched items. Families may want to buy more of something they already have.
- Ranking engine boost based on ingredient match percentage: 75%+ → +3, 50–74% → +1, <30% → -1.
- Cook-level nav only. Pantry management is a cook task, not a household-wide page.
- Uses same normalization as
recipe_ingredients:name.toLowerCase().trim().replace(/\s+/g, " ")
Migration
Neon + Drizzle + NextAuth migration planned
Decision: Dinly is migrating off Supabase DB to Neon (Postgres) + Drizzle ORM + NextAuth v5. Auth will use NextAuth's Email provider (magic links via Resend or Brevo) since dinly uses magic links, not email/password.
Migration constraint: All household-scoped queries currently rely on Supabase RLS for implicit isolation. With Drizzle, every query that touches a household-scoped table needs an explicit .where(eq(table.householdId, householdId)). The migration cannot be considered safe until every such query has been audited.
Drizzle transaction gotcha (from scholexis): return inside a db.transaction() callback does not propagate — ownership checks must happen before entering the transaction, not inside it. Pattern: fetch and verify ownership, then open transaction to write.
Drizzle FK index gotcha (from scholexis migration): Every FK column in the Drizzle schema needs an explicit index() declaration — Drizzle does not infer indexes from foreign key constraints. Failing to add them means table scans on every join. When writing the dinly Drizzle schema, add index() for every FK column (household_id, recipe_id, weekly_cycle_id, etc.). Dinly's existing composite indexes on recipe_history and weekly_candidates must be replicated explicitly in the schema file.
Scout findings
Scout run 6 — findings and exclusions
Context: Scout run after completing #309, #311, #312, #313 (window.confirm replacements, calendar tests, not-found pages, admin AI page update).
Included: 4 grindable issues (silent failures in feedback-section and shopping-list, cadence-nudge tests, wrong revalidatePath in shopping actions) + 1 needs-prep (AI suggestions UX gaps).
Excluded: @ai-sdk/anthropic ^3.0.64 version check — the package follows its own versioning independent of the ai package; no evidence of incompatibility in the working build.
Excluded: handleCopy clipboard error handling — clipboard failures are expected behavior in restricted environments; not worth an error state since it's a convenience action.
Last updated 2026-05-19 · Owner Paul Welty