Decisions· Dinly· Last updated 2026-05-19

Product decisions log

Architectural and product decisions with rationale. When making new decisions, check here for precedents.

UI components

shadcn/ui initialized with Base UI (not Radix)

Date: 2026-03-18· Tags: shadcn, base-ui, radix, components, polymorphism

Decision: npx shadcn@latest init -d chose base-nova style with Base UI primitives (not Radix). This means:

  • Components use render prop for polymorphism, not asChild
  • 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)

Date: 2026-03-18· Tags: next.js, middleware, proxy, naming

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

Date: 2026-03-19· Tags: supabase, rls, security, household-scoping, admin-client

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:

  1. Call requireHousehold() to verify the user is authenticated with a household
  2. 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)

Date: 2026-03-22· Tags: data-model, feedback, recipe_bookmarks, ranking-engine

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_bookmarks already has rating, note, and bookmark_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_id FK 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

Date: 2026-03-22· Tags: pantry, mvp, shopping-list, ranking-engine, normalization

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

Date: 2026-04-09· Tags: neon, drizzle, nextauth, supabase, migration, rls, transactions, indexes

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

Date: 2026-03-26· Tags: scout, issue-triage, exclusions, ai-sdk, clipboard

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