All Reports

Full App Audit Report

Comprehensive QA audit of the Pantry React app, covering CRUD completeness, navigation, error handling, security, mobile UX, data integrity, and performance.

Date: 01 Apr 2026 Branch: sprint-5-build Auditor: QA/Product Team

1. Executive Summary

3
Critical
8
High
11
Medium
7
Low
29
Total Gaps

The app has a solid foundation: authentication, RLS, household scoping, receipt scanning, recipe matching, and depletion tracking are all in place. However, several critical gaps remain before the app is production-ready:

2. CRUD Matrix

inventory_items

OperationStatusNotes
Create Done AddItem.jsx, ReceiptScan (OCR), BarcodeScan, ProductSearch flow, recurring item "Stock now"
Read Done Inventory.jsx with search, category filter, depletion badges, expiry display
Update Partial Can set/edit baseline stock minimum only. Cannot edit item name, quantity, unit, category, brand, expiry, or notes
Delete Missing No delete button or swipe-to-delete anywhere. RLS policy exists (is_household_manager) but no UI

recipes

OperationStatusNotes
Create Done RecipeCreate.jsx with ingredient rows, validation
Read Done Recipes list with search, RecipeDetail with pantry match check
Update Done RecipeCreate.jsx in edit mode via /recipes/:id/edit
Delete Done RecipeDetail.jsx with confirmation dialog

household_members

OperationStatusNotes
Create (Invite) Missing InviteMember.jsx exists but writes to household_invites table which has no migration. Will always fail with a DB error.
Read Done Settings.jsx shows members via get_household_members_with_profiles RPC
Update (Role) Done Settings.jsx context menu: change role between manager/member
Delete (Remove) Done Settings.jsx with confirmation dialog

purchase_events

OperationStatusNotes
Create Partial Created behind the scenes by receipt scan and depletion trigger. No direct manual log entry UI.
Read Missing No UI to view purchase history for an item or household
Update N/A Append-only log by design
Delete N/A Append-only log; no DELETE policy defined

recurring_items

OperationStatusNotes
Create Done Settings.jsx inline form with name, quantity, unit, frequency, due date
Read Done Settings.jsx list with overdue highlighting
Update Done Settings.jsx edit mode via context menu
Delete Done Settings.jsx with confirmation

budget_periods

OperationStatusNotes
Create Done Budget.jsx "Set a monthly budget" button
Read Done Budget.jsx with month navigation, progress bar, receipt list
Update Done Budget.jsx edit button on existing budget
Delete Missing No way to delete a budget period once created

receipts

OperationStatusNotes
Create Done ReceiptScan.jsx via OCR
Read Done Budget.jsx shows receipts per month with expandable item detail
Update Missing No way to correct receipt total, store name, or items after creation
Delete Missing No way to delete a receipt (e.g. duplicate scan)

3. Gap List

Critical Blocks core use case or exposes data High Major missing feature Medium UX gap or partial implementation Low Polish item or edge case
SEC-001 Critical OpenAI API key exposed in browser build
VITE_OPENAI_API_KEY in .env is prefixed with VITE_, meaning it is bundled into the client-side JavaScript. Anyone can open DevTools and extract the key. Used in src/lib/photoProductIdentifier.js to call OpenAI directly from the browser.
Fix: Move the OpenAI call to a Supabase Edge Function (like scan-receipt already does). Remove the VITE_OPENAI_API_KEY env var entirely.
CRUD-001 Critical Cannot edit or delete inventory items
Users can add items via multiple paths (manual, receipt scan, barcode, product search, recurring restock), but there is no way to correct an item's name, adjust its quantity, change its category, update its expiry date, or delete it. The only edit available is setting the baseline stock minimum. This means typos, incorrect quantities from OCR, and duplicate items cannot be fixed.
Fix: Add an edit item screen (or inline edit on Inventory page) and a delete action with confirmation. RLS policies for update and delete already exist.
CRUD-002 Critical Invite Member flow is broken: missing household_invites table
InviteMember.jsx inserts into household_invites, but no migration creates this table. The invite flow will always fail with a Supabase error. This blocks the core household sharing feature.
Fix: Create a migration for household_invites with columns for household_id, invited_email, role, invited_by, status, created_at. Add RLS policies. Alternatively, redesign to add members directly if the invite-accept flow is not needed yet.
NAV-001 High Budget page not in navigation
The /budget route exists in App.jsx, and Budget.jsx is a fully built page, but "Budget" is not listed in the NAV_ITEMS array in AuthLayout.jsx. Users cannot discover or reach it from the sidebar or bottom tabs.
Fix: Add Budget to NAV_ITEMS with an appropriate icon, between Recipes and Scan (or similar logical position).
PERF-001 High No pagination on inventory, recipes, or receipts
All data fetching uses select('*') with no .limit() or .range(). Dashboard fetches all inventory items. Inventory page fetches all items. Recipes page fetches all recipes. With hundreds of items, this will cause slow load times and high memory usage on mobile devices.
Fix: Add cursor-based or offset pagination (e.g. 50 items per page) with a "Load more" button or infinite scroll.
CRUD-003 High Cannot delete receipts
If a user accidentally scans the same receipt twice or scans the wrong image, there is no way to delete the receipt. This inflates the budget spending totals with no correction path.
Fix: Add a delete button on the expanded receipt row in Budget.jsx with a confirmation dialog.
CRUD-004 High Cannot edit receipts
OCR is imperfect. Users cannot correct the store name, total amount, or individual line items after a receipt is saved. The expanded receipt view in Budget.jsx is read-only.
Fix: Allow inline editing of receipt total and store name. Item-level edits are lower priority.
CRUD-005 High No purchase history view
purchase_events is logged by triggers but there is no UI to view purchase history. Users cannot see when they last bought an item, price trends, or frequency data.
Fix: Add a purchase history section to an item detail/edit screen, or a dedicated history page.
UX-001 High Dashboard "Buy" and "Restock?" buttons are no-ops
The Stock Alerts section on Dashboard has "Buy" and "Restock?" buttons with onClick={() => {}} (empty handlers). These buttons are visible and clickable but do nothing, which will confuse users.
Fix: Wire these to either navigate to AddItem with prefilled data, or directly create a purchase event + update quantity.
DATA-001 High No unique constraint on budget_periods per household/month
The budget_periods table has no unique constraint on (household_id, period_start, period_end). Budget.jsx uses .maybeSingle(), but rapid clicks or race conditions could create duplicate budget records for the same month. The code would then only show one, with the other silently orphaned.
Fix: Add a unique constraint: UNIQUE(household_id, period_start, period_end). Use upsert in the frontend.
UX-002 Medium No sort options on inventory
Inventory supports search and category filter, but there is no way to sort by name, quantity, expiry date, or date added. Items are always ordered by created_at DESC.
Fix: Add a sort dropdown (Name A-Z, Expiry soonest, Quantity low-high, Recently added).
UX-003 Medium No way to mark item as "out of stock" or adjust quantity
There is no quick action to set an item's quantity to 0, decrement it, or adjust it. The only quantity change happens at creation time.
Fix: Add +/- buttons or a quick "Mark empty" action on each inventory card.
MOBILE-001 Medium Dashboard stat cards and content grid not responsive
Dashboard uses gridTemplateColumns: 'repeat(3, 1fr)' for stat cards and '1fr 1fr' for the content grid. On a 320px screen, this creates very narrow columns. No media query or responsive breakpoint is applied.
Fix: Use CSS media queries or repeat(auto-fit, minmax(...)) to stack on small screens.
MOBILE-002 Medium Recurring items row crowded on small screens
Each recurring item row in Settings.jsx has: name, meta text, frequency badge, "Stock now" button, and a three-dot menu button, all in a single flex row. On narrow screens (under 375px), these elements will overlap or wrap awkwardly.
Fix: Stack the action buttons below the name/meta on mobile, or use a two-line layout.
DATA-002 Medium category_id FK on inventory_items has no ON DELETE handling
inventory_items.category_id references categories(id) with no ON DELETE clause (defaults to RESTRICT). Deleting a category will fail if any item references it. The app provides no UI to manage categories, so orphaned or undeletable categories are likely.
Fix: Add ON DELETE SET NULL to the foreign key, or add a category management UI.
DATA-003 Medium No index on inventory_items.barcode
Barcode lookups query inventory_items by barcode value, but there is no index on this column. As the table grows, barcode lookups will become slower.
Fix: Add CREATE INDEX idx_inventory_items_barcode ON inventory_items(barcode).
DATA-004 Medium No index on receipts.purchase_date
Budget.jsx filters receipts by purchase_date range using .gte() and .lte(), but there is no index on this column. With many receipts, this range scan will be slow.
Fix: Add CREATE INDEX idx_receipts_purchase_date ON receipts(purchase_date).
UX-004 Medium No global error boundary
If a React component throws during render, the entire app crashes with a white screen. There is no ErrorBoundary component wrapping the route tree.
Fix: Add a React ErrorBoundary at the App level that shows a friendly "Something went wrong" screen with a reload button.
UX-005 Medium No offline detection or network error handling
When the network drops or Supabase is unreachable, API calls fail silently or show raw error messages. There is no offline banner, toast, or graceful degradation.
Fix: Add a global online/offline listener that shows a banner. Wrap Supabase calls with retry logic or queue.
CRUD-006 Medium Cannot delete budget period
Once a monthly budget is set, there is no way to delete it. Users can only edit the amount. If a budget was set for the wrong month, it stays forever.
Fix: Add a delete option in the budget edit form.
UX-006 Medium No 404 page
Navigating to an undefined route (e.g. /foo) renders a blank page. There is no catch-all route or 404 component in App.jsx.
Fix: Add a <Route path="*"> element with a "Page not found" component.
UX-007 Low No export or share shopping list
Low/out-of-stock items are displayed on the dashboard and inventory, but there is no way to export them as a shopping list (copy to clipboard, share via WhatsApp, or print).
Fix: Add a "Copy shopping list" button on the Stock Alerts card that formats low items as text.
UX-008 Low No notification system for low stock or expiring items
The app shows alerts on the dashboard when items are low or expiring, but there is no push notification, email, or WhatsApp alert. Users must open the app to see alerts.
Fix: Consider a scheduled Supabase Edge Function that sends daily digest emails or WhatsApp messages for overdue recurring items and low stock.
UX-009 Low Landing page V2 route exists but is not linked
/v2 renders LandingPageV2 but is not discoverable. Leftover from a design iteration.
Fix: Remove the route if not needed, or redirect / to the preferred version.
DATA-005 Low household_members.user_id has no ON DELETE CASCADE
If a Supabase auth user is deleted (e.g. GDPR request), the household_members row referencing that user will become orphaned, since the FK defaults to RESTRICT (blocking the auth user deletion) or NO ACTION.
Fix: Add ON DELETE CASCADE to the user_id foreign key on household_members.
PERF-002 Low Dashboard fetches baseline_stock for all items
Dashboard fetches all baseline_stock rows for the household, even though it only needs items where quantity < minimum_quantity. This could be a single query with a join instead of two full-table fetches.
Fix: Use an RPC or a joined query to fetch only items that are below their baseline.
MOBILE-003 Low AddItem form row layout can clip on very small screens
The quantity/unit row and date fields use display: flex; gap: 16 with no wrap. On screens under 320px, the fields may overflow horizontally.
Fix: Add flexWrap: 'wrap' to the row styles.
UX-010 Low "Add missing to shopping list" button is permanently disabled
RecipeDetail.jsx shows a disabled "Add missing to shopping list" button with "Coming soon" text after a pantry check. This is a visible but non-functional feature stub.
Fix: Either implement the feature or remove the button to avoid confusion.

4. Navigation Map

5. Error State Coverage

ScreenLoadingErrorEmptyOfflineUnauth
Dashboard Handled Handled Handled Missing Handled
Inventory Handled Handled Handled Missing Handled
AddItem N/A Handled N/A Missing Handled
ReceiptScan Handled Handled N/A Missing Handled
BarcodeScan Handled Handled N/A Missing Handled
Recipes Handled Handled Handled Missing Handled
RecipeDetail Handled Handled Handled Missing Handled
RecipeCreate Handled Handled N/A Missing Handled
Budget Handled Handled Handled Missing Handled
Settings Handled Handled Handled Missing Handled
ProductSearch Handled Handled Handled Missing Handled
InviteMember Handled Handled N/A Missing Handled

Every page handles loading, error, and empty states well with skeleton loaders, retry buttons, and friendly empty state messages. The consistent gap is offline/network detection, which is missing app-wide. Authentication is handled globally via ProtectedRoute.

6. Security Flags

Critical OpenAI API key in browser bundle
.env contains VITE_OPENAI_API_KEY=sk-proj-2LLHV9L.... Any VITE_ prefixed variable is embedded in the Vite build output and visible to anyone who opens the app. This key grants access to the OpenAI API and could be used to generate arbitrary completions at the project owner's expense.

Used in: src/lib/photoProductIdentifier.js:11, which calls the OpenAI Chat Completions API directly from the browser.
Also exposed: VITE_OPENAI_VISION_MODEL=gpt-4o (not sensitive, but confirms the API key is active).
RLS Coverage: Complete
All tables have RLS enabled with appropriate policies. Helper functions (is_household_member, is_household_editor, is_household_manager, get_user_household_ids) use SECURITY DEFINER to avoid recursion. This is well-designed.
Supabase Anon Key: Expected
VITE_SUPABASE_ANON_KEY is a publishable key by design. With RLS enabled on all tables, this is safe and expected. The anon key only grants access that RLS policies allow.
No Other Secrets Found
No hardcoded API keys, passwords, or tokens found in source files outside of .env. Auth tokens are obtained via Supabase session, not hardcoded.

7. Quick Wins

Gaps that can be addressed in under one hour each:

IDGapEffortImpact
NAV-001 Add Budget to navigation 5 min Unlocks an entire page for all users
UX-001 Wire "Buy"/"Restock?" buttons on dashboard 20 min Removes dead click targets, improves core flow
UX-006 Add 404 page 10 min Prevents blank screen on bad URLs
DATA-003 Add barcode index 1 min Faster barcode lookups
DATA-004 Add receipts.purchase_date index 1 min Faster budget page loads
DATA-001 Add unique constraint on budget_periods 5 min Prevents duplicate budgets
UX-004 Add React ErrorBoundary 15 min Prevents white-screen crashes
MOBILE-003 Add flexWrap to AddItem form rows 2 min Fixes overflow on very small screens
UX-009 Remove or redirect /v2 route 2 min Cleanup