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.
1. Executive Summary
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:
- Critical: OpenAI API key is exposed in the browser build via
VITE_OPENAI_API_KEY. This must be moved to a Supabase Edge Function immediately.
- Critical: No way to edit or delete inventory items. Users can only add items, never correct mistakes or remove them.
- Critical: The
household_invites table does not exist in any migration, so the Invite Member flow will always fail with a database error.
- High: Budget page is not reachable from the navigation bar. Users can only reach it by typing the URL directly.
- High: No pagination on any data list. With hundreds of inventory items, performance will degrade.
2. CRUD Matrix
inventory_items
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
| Operation | Status | Notes |
| 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
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
/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.
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.
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.
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.
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
| Route | In Nav? | Reachable From | Exit Path | Notes |
/ |
N/A |
Direct URL |
Sign In / Sign Up links |
Public landing page |
/signin |
N/A |
Landing page, redirect on unauth |
Sign Up link, auth redirect |
OK |
/signup |
N/A |
Sign In page |
Sign In link |
OK |
/create-household |
N/A |
Redirect when no household |
Redirect to dashboard on create |
OK |
/dashboard |
Yes |
Sidebar, bottom tab |
All nav items |
OK |
/inventory |
Yes |
Sidebar, bottom tab |
All nav items |
OK |
/inventory/add |
No |
FAB on inventory, dashboard button, product search |
Back button to /inventory |
OK |
/scan |
Yes |
Sidebar, bottom tab |
All nav items |
OK |
/scan/barcode |
No |
Link on /scan page |
Back to /scan, nav items |
OK |
/recipes |
Yes |
Sidebar, bottom tab |
All nav items |
OK |
/recipes/new |
No |
"New recipe" button on recipes page |
Back link to /recipes |
OK |
/recipes/:id |
No |
Recipe card click |
Back link to /recipes |
OK |
/recipes/:id/edit |
No |
Edit button on recipe detail |
Back link to recipe detail |
OK |
/product-search |
Yes |
Sidebar (Shop), Inventory "Find at Woolies" |
Back button, nav items |
OK |
/budget |
No |
Only by typing URL directly |
Nav items (once loaded) |
Not discoverable. Major gap. |
/settings |
Yes |
Sidebar (Household), bottom tab |
All nav items |
OK |
/settings/invite |
No |
"Invite member" button on settings |
Nav items (no back button on InviteMember) |
Missing back button to /settings |
5. Error State Coverage
| Screen | Loading | Error | Empty | Offline | Unauth |
| 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
.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:
| ID | Gap | Effort | Impact |
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 |