5 parallel domain re-sweeps against current code (last full audit was 2026-07-21/22, three days of heavy shipping since -- billing, developer/BYOK permission split, moderator scoping). Corrections and additions: - Fixed stale admin nav count (13 -> 15 sections, was missing Billing entirely). - Closed the /r/[id] followers-visibility gap that was flagged open -- it's fixed in code, found a new inconsistency in its place: a followers-only recipe never shows on the author's own profile grid. - Corrected "auto-deduct pantry on cook" -- the API logic is real and correct but both UI callers hardcode it off, so it never fires in practice. Was overstated as "Exists, one nuance." - Fixed "bulk-set-week" mislabel (it's bulk-clear only, no bulk-create). - Added several undocumented features: recipe notes, manual nutrition entry, AI conversation thread management + chat-history search, two more draft-recipe AI endpoints, the authenticated meal-plan collaboration UI (distinct from the public link), move-to-pantry button, pantry bulk-merge, public shopping-list page + print/QR, add-to-shopping-list AI tool, leftover-cron scope limitation. - Noted shopping-list collaboration now supplements polling with real push notifications on add/check events. New "Consumer-friendliness assessment" section directly answers whether Epicure has too many technical/developer-oriented features for a home-cook audience: mostly no -- API keys, webhooks, BYOK, OpenAPI docs, and self-serve developer access are all real surface but properly gated and hidden from nav by default. One genuine exception found and flagged: Settings -> AI's Model Prefs picker lets every user pick raw model IDs with zero gating -- the one place a developer-tool control was left unfenced on a consumer screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
30 KiB
Epicure Feature Audit
Code-verified inventory of what exists in the app today, by domain. Written 2026-07-21, re-audited in full 2026-07-24 (5 parallel domain sweeps re-reading actual code against the prior version, not trusting it). Keep this file updated whenever a feature is added or changes what it does (see standing rule below).
Status legend: Exists (fully working) · Partial (works but with a real limitation, noted) · Missing (confirmed absent by grep/read, not just "didn't find it").
1. Recipes & AI
AI stack: Vercel AI SDK, provider-agnostic (apps/web/lib/ai/factory.ts) — OpenAI, Anthropic, OpenRouter, Ollama (local). Key resolution order: user's own BYOK key → admin default per use-case → admin's raw provider key → env var. Structured outputs always generateObject + Zod; chat features use generateText with optional tool-calling.
⚠️ Found this pass — ungated technical UI: Settings → AI's Model Prefs section (apps/web/components/settings/model-prefs-form.tsx) lets every user, with no developer/BYOK gate at all, pick a raw provider + specific model ID (gpt-4o-mini, o3-mini, claude-opus-4-8, etc.) per use-case, spending the admin's own API budget. This is the one place in the app where a home cook can stumble into an LLM-playground-style control panel with zero fencing. See the "Consumer-friendliness" section at the end of this doc.
| Feature | Status | Description | Key files |
|---|---|---|---|
| Recipe CRUD | Exists | Create/get/list/update/delete; update snapshots the prior version first (recipeSnapshots) |
apps/web/app/api/v1/recipes/** |
| Fork / duplicate | Exists | Same backend action for both — UI just swaps the label based on ownership | apps/web/app/api/v1/recipes/[id]/fork/route.ts |
| Version history | Exists | GET /recipes/[id]/versions, diff-able snapshots |
apps/web/app/api/v1/recipes/[id]/versions/** |
| Import from URL | Exists | Fetches a page (SSRF-checked), strips markup, extracts a structured recipe via AI. Returns the extraction to the client — doesn't self-persist, caller POSTs it to create | apps/web/app/api/v1/ai/import-url |
| Import from photo | Exists | Two-stage vision→text; recognizes a photographed page/handwritten recipe, self-persists as a private recipe | apps/web/app/api/v1/ai/import-photo, lib/ai/features/{recognize-photo,generate-recipe-from-recognition}.ts |
| Generate from idea/title | Exists | Text-only generation from just a title | apps/web/app/api/v1/ai/generate-from-idea |
| Recipe ideas (browsing) | Exists | 6 idea cards from an optional prompt, not persisted | apps/web/app/api/v1/ai/recipe-ideas |
| Adapt (exclude ingredients / constraints) | Exists | Persists as new recipe + variation link, unless AI reports unchanged | apps/web/app/api/v1/ai/adapt/[id] |
| Suggest variations | Exists | Ideas only, not persisted; gated by tier feature flag recipe_variations |
apps/web/app/api/v1/ai/variations/[id] |
| Meal / drink pairings | Exists | Gated by tier feature flags meal_pairing / drink_pairing |
apps/web/app/api/v1/ai/{pairings,drinks}/[id] |
| Translate recipe | Exists | Saves as a new private recipe, keeps original quantities/units/timers | apps/web/app/api/v1/ai/translate/[id] |
| Batch-cook generator | Exists | One recipe, multiple dishes, per-dish fridge/freezer guidance | apps/web/app/api/v1/ai/batch-cook/generate |
| Nutrition estimation | Exists | Hybrid AI+USDA (see section 2's nutrition row), cached on the recipe row (nutritionData), author-triggered. A separate nutritionManual flag lets the author hand-enter facts at create/edit time instead, skipping estimation entirely — undocumented until this pass. |
apps/web/app/api/v1/recipes/[id]/nutrition, packages/db/src/schema/recipes.ts (nutritionManual) |
| Recipe notes | Exists | Private, per-user free-text note attached to any recipe (not shared with other viewers) | apps/web/app/api/v1/recipes/[id]/notes |
| AI conversation management | Exists | List/create/rename/delete threads for recipe-chat/cooking-chat, plus a separate full-text search + bulk-delete over past chat messages — a full thread-management layer around the chat feature, not just the chat itself | apps/web/app/api/v1/ai/conversations/**, apps/web/app/api/v1/ai/chat-history |
| Draft-recipe AI helpers | Exists | Two additional, non-persisting endpoints: generate (freeform prompt → recipe, used by the "describe" tab in the AI-generate dialog) and regenerate (revise an in-progress unsaved draft with a free-text instruction) — distinct from the persisting generate-from-idea |
apps/web/app/api/v1/ai/{generate,regenerate} |
| Scale servings | Exists | Model-driven, not naive linear math (handles "1 egg" etc.) | apps/web/app/api/v1/ai/scale |
| Substitute ingredient | Exists | apps/web/app/api/v1/ai/substitute |
|
| Recipe chat / cooking chat | Exists | Recipe-grounded Q&A, and a general assistant with tool-calling (createRecipe, addToShoppingList) |
apps/web/app/api/v1/ai/{recipe-chat,cooking-chat} |
| Themed meal generator | Exists | Multi-course meal into a collection | apps/web/app/api/v1/ai/generate-meal |
| Recipe media: photos | Exists | S3-compatible storage, counts against tier storage limit | packages/db/src/schema/recipes.ts (recipePhotos) |
| Recipe video | Missing | No video field/upload path anywhere in schema or components | — |
| Collections | Exists | Shared/collaborative (collection_members, viewer/editor roles), fork-a-collection, explore page, reorder |
apps/web/app/api/v1/collections/** |
| Tags, difficulty, dietary labels | Exists | Dietary schema has 7 tags; search only exposes 4 of them as filters (vegan/vegetarian/glutenFree/dairyFree) |
packages/db/src/schema/recipes.ts |
| Visibility (private/unlisted/public/followers) | Exists | /r/[id]'s followers-only gap (previously flagged here) is closed — it now explicitly checks viewerId === authorId || isFollowing(...) before allowing a followers-only recipe through. New inconsistency found this pass: a followers-only recipe shows up in a follower's following-feed, but never on the author's own profile grid (u/[username]/page.tsx only queries visibility = "public" for the grid) — see "Real gaps" below. |
apps/web/app/r/[id]/page.tsx, apps/web/app/(app)/u/[username]/page.tsx |
| Print views | Exists | Recipe, collection, meal plan, shopping list, pantry all have print routes | apps/web/app/print/** |
| Markdown export | Exists | Single recipe, bulk (up to 100), meal plan | apps/web/lib/markdown/** |
| Cooking mode | Exists | Step timers (multiple parallel), voice control (Web Speech API, EN/FR commands), wake-lock, batch-cook step grouping | apps/web/components/cooking-mode/cooking-mode.tsx |
| Search | Exists | Title/description/ingredient/tag substring match + filters (difficulty, type, max time, dietary, exact tags) | apps/web/app/api/v1/search |
| "What can I cook" (pantry-matched) | Exists | Deterministic ingredient-overlap scorer against the user's own recipes, not AI | apps/web/lib/pantry-match.ts, /recipes/can-cook |
| Explore / trending | Exists | Purely favorite-count-driven (7-day window) + recency — no editorial curation | apps/web/app/(app)/explore |
2. Meal planning, pantry, shopping, nutrition
| Feature | Status | Description | Key files |
|---|---|---|---|
| Weekly calendar meal plan | Exists | Mon–Sun grid, manual CRUD; the "bulk" entries endpoint is bulk-clear only (a day or the whole week) — there's no bulk-create, entries are always added one at a time via POST /entries |
apps/web/app/(app)/meal-plan, apps/web/app/api/v1/meal-plans/[weekStart]/** |
| AI weekly meal-plan generator | Exists | Pantry-aware, dietary-aware, nudged by nutrition goals; one real recipe per entry; entries can also link to a specific batchDish (reflected in ICS export and the calendar page) |
apps/web/app/api/v1/ai/meal-plan/generate |
| Meal plan sharing | Exists | Two separate paths: (1) authenticated member collaboration — meal_plan_members viewer/editor roles, day-to-day editing UI at /meal-plan/shared/[mealPlanId] (this is where collaborators actually work, not the public link); (2) anonymous read-only public link (isPublic flag, view-only — no public-editable mode) at /m/[id], with a client-generated QR code. Entry changes fire a meal_plan.updated webhook event. |
apps/web/lib/meal-plan-access.ts, apps/web/app/(app)/meal-plan/shared/[mealPlanId]/page.tsx, apps/web/app/m/[id]/page.tsx |
| Calendar export (.ics) | Exists | Real ICS builder with per-meal-type wall-clock times | apps/web/app/api/v1/meal-plans/[weekStart]/export/ics |
| Weekly nutrition rollup | Exists | apps/web/app/api/v1/meal-plans/[weekStart]/nutrition |
|
| Meal plan print + Markdown export | Exists | Undocumented until this pass despite section 1 mentioning print/markdown generally | /print/meal-plan, apps/web/lib/markdown/meal-plan.ts |
| Multiple shopping lists | Exists | apps/web/app/api/v1/shopping-lists/** |
|
| Generate list from meal-plan week | Exists | Merges duplicate ingredients, flags items already covered by pantry (never silently drops) | apps/web/lib/pantry-shopping-match.ts |
| Add to list from a single recipe | Exists | Separate path from meal-plan-based generation; also reachable via the cooking-chat AI tool ("add to shopping list") | apps/web/components/recipe/add-to-shopping-list-button.tsx, apps/web/lib/ai/tools/add-to-shopping-list-tool.ts |
| Shared/collaborative lists | Exists | Member roles and an anonymous public link that can grant full anonymous edit rights (Google-Docs-style), not just read — isPublic + publicEditable; disabling isPublic force-revokes publicEditable. Also has its own print view with a QR code, and a public page distinct from the meal-plan one. |
apps/web/lib/shopping-list-access.ts, apps/web/app/s/[id]/page.tsx, /print/shopping-list/[id] |
| List collaboration | Partial, upgraded | Still short-polling (~4s interval) for state sync, but item-add and item-checked events now also fire real Web Push notifications (VAPID) as a supplement — not purely polling anymore. Uncheck/rename/reorder still don't push. | apps/web/components/meal-plan/shopping-list-view.tsx, apps/web/lib/shopping-list-notify.ts |
| Move checked items to pantry | Exists | Undocumented until this pass — button on the shopping-list view moves checked-off items into pantry stock | shopping-list-view.tsx → /api/v1/pantry/bulk |
| Aisle categorization | Exists | Heuristic auto-assign + bulk re-categorize, plus inline custom-category creation and full drag-and-drop reordering (richer UI than previously documented) | apps/web/lib/grocery-categories.ts |
| Grocery delivery integration | Partial / stub | Generic export payload works (integrator-shaped — no visible in-app UI consumer besides the Instacart adapter); Instacart adapter is an explicit stub (requires a partnership agreement Epicure doesn't have — returns 501 if unconfigured, throws if "configured") | apps/web/lib/grocery-providers/instacart.ts |
| Other delivery/price integrations (DoorDash, Kroger, Walmart, live pricing) | Missing | Confirmed absent by repo-wide search | — |
| Pantry manual CRUD | Exists | Includes a bulk case-insensitive name+unit merge endpoint (used by the scan-confirm flow), undocumented until this pass | apps/web/app/api/v1/pantry/** |
| Auto-deduct pantry on cook | API-only, dead in the UI | The API logic is correct (name+unit matching, defaults deductFromPantry: true) — but both real UI callers (batch-cook-dishes.tsx, meal-planner.tsx's mark-cooked flow) hardcode deductFromPantry: false. A home cook using the app today never triggers auto-deduction; it's only reachable via a direct API/API-key client. Previously documented as "Exists, one nuance," which overstated real-world behavior. |
apps/web/app/api/v1/recipes/[id]/cooked/route.ts, apps/web/components/recipe/batch-cook-dishes.tsx, apps/web/components/meal-plan/meal-planner.tsx |
| Expiring-soon tracking | Exists | With "use it up" recipe suggestions. The leftover-expiry push/email cron only covers batch-cook dishes — plain pantry items only get an in-app badge, no reminder. | apps/web/components/pantry/expiring-soon-suggestions.tsx, apps/web/app/api/internal/cron/leftover-reminders/route.ts |
| Barcode scan (pantry) | Exists | Real Open Food Facts API lookup by UPC/EAN | apps/web/app/api/v1/pantry/scan/barcode |
| Photo scan (pantry) | Exists | Vision-model based | apps/web/app/api/v1/pantry/scan/photo |
| Nutrition goals | Exists | apps/web/app/api/v1/users/me/nutrition-goals |
|
| Nutrition diary | Exists | Single-day diary view, plus a Trend tab (7/30/90-day calorie line chart + daily macro averages) — the same endpoint switches modes via a range query param |
apps/web/app/api/v1/users/me/nutrition-diary, apps/web/components/nutrition/nutrition-trend.tsx |
| USDA nutrition-facts database | Exists | estimateNutrition now looks up each ingredient in USDA FoodData Central (SR Legacy + Survey (FNDDS) datasets) after converting its quantity/unit to grams, sums the matches, and only falls back to AI estimation for ingredients that couldn't be converted (count-based units like "2 cloves") or had no USDA match — including every ingredient, transparently, when USDA_API_KEY isn't configured. Barcode scan (pantry) still only IDs product names via Open Food Facts, unrelated to this. |
apps/web/lib/usda.ts, apps/web/lib/ingredient-grams.ts, apps/web/lib/ai/features/estimate-nutrition.ts |
| Batch cooking | Exists | Shared prep steps grouped by which dish they serve, per-dish fridge/freezer countdown independent of the whole-batch pantry deduction | apps/web/components/recipe/batch-cook-*.tsx |
3. Social, messaging, notifications
| Feature | Status | Description | Key files |
|---|---|---|---|
| Follow / unfollow | Exists | One-way, no approval step | apps/web/app/api/v1/users/[username]/follow |
| Block | Exists | Blocks messaging both ways. Comment-hiding is one-directional only — hides comments from someone you've blocked, but not your own comments from someone who blocked you (not necessarily wrong, just undocumented directionality). | apps/web/lib/blocks.ts |
| Private accounts | Exists | Hides recipe grid from non-followers; excluded from "for you" unless already followed | apps/web/app/(app)/u/[username]/page.tsx |
| Profile page | Exists | Cooking-history streak stats, "cooked it" photo gallery (owner-visible) | apps/web/app/(app)/u/[username] |
| Ratings/reviews, comments (1-level threaded), reactions, @mentions | Exists | packages/db/src/schema/social.ts |
|
| Favorites | Exists | Drives "for you" ranking and trending | apps/web/app/api/v1/recipes/[id]/favorite |
| Direct messaging | Exists, 1:1 by design | No group messaging (deliberate — unique-pair constraint at schema level) | packages/db/src/schema/messaging.ts |
| Read receipts | Partial | Thread-level lastReadAt only — no per-message delivery/seen state |
packages/db/src/schema/messaging.ts |
| Following feed | Exists | apps/web/app/api/v1/feed |
|
| "For You" personalized feed | Exists | Tag/dietary preference profile from favorites + high ratings | apps/web/lib/for-you-ranking.ts |
| Trending feed / explore | Exists | Favorite-count driven, no editorial curation, no separate "featured" surface. Trending/explore/for-you all additionally exclude private-account authors even when the specific recipe is public — a non-obvious interaction worth knowing about when debugging "why isn't my public recipe showing up." |
apps/web/app/api/v1/feed/trending |
| Notification categories | Exists | 8 categories, independent push + email toggle each, plus email-only weekly digest | packages/db/src/schema/users.ts (userNotificationPrefs) |
| Push notifications | Exists | Web Push + VAPID, end-to-end, real trigger call sites (not just plumbing) | apps/web/lib/push.ts |
| Push display + click-through | Exists | sw.js previously had no push listener at all — push messages never displayed. Now has both push (calls showNotification) and notificationclick (focuses an existing tab on the payload's url, or opens one) |
apps/web/public/sw.js |
| Email notifications + weekly digest cron | Exists | apps/web/lib/notifications.ts, apps/web/app/api/internal/cron/weekly-digest |
|
| Reports/moderation | Exists | Report → admin webhook → admin queue → resolve (reviewed/dismissed) + audit log. Supports three target types — recipe, comment, and user (reporting a person directly, not previously called out) | apps/web/app/api/v1/reports, apps/web/app/api/v1/admin/reports |
| Moderator role | Exists | Moderators get /admin/reports (view/resolve) and /admin/recipes (view + unpublish takedown); every other /admin/* page redirects them out via requireFullAdminPage(). Comment deletion already allowed moderator before this. |
apps/web/lib/require-admin-page.ts, apps/web/app/admin/layout.tsx, apps/web/app/api/v1/admin/recipes/[id]/route.ts |
4. Admin, billing, ops, integrations
| Feature | Status | Description | Key files |
|---|---|---|---|
| Tiers (free/pro/family) + limits | Exists | Recipe count, AI calls/month, storage, public-recipe count; TOCTOU-safe via advisory lock + transaction | apps/web/lib/tiers.ts |
| Stripe webhook (checkout/cancel) | Exists | Signature-verified, replay-protected, event-deduped — production quality | apps/web/app/api/webhooks/stripe/route.ts |
| Stripe checkout + self-serve billing portal (2026-07-23) | Exists | POST /api/v1/billing/checkout creates a subscription Checkout Session (promotion codes enabled); POST /api/v1/billing/portal opens Stripe's hosted Customer Portal for self-serve cancel/upgrade/card-update. Webhook route rewritten with the real stripe SDK (stripe.webhooks.constructEvent, replacing the hand-rolled HMAC verifier) and now handles the full event set: checkout.session.completed, customer.subscription.{updated,deleted}, invoice.{payment_failed,paid} — past_due deliberately doesn't downgrade tier (Stripe retries the card first). /settings/billing shows plan cards, usage, and a "Manage billing" button; /admin/billing shows connection status, subscriber counts, past-due list, recent billing audit events. Cancel/downgrade is at period end (Stripe Portal default); no trial period. Not yet built: family-group multi-user sharing (Family tier is purchasable solo, but the plan's per-account member invite/join/tier-resolution piece is deliberately deferred — see plans/STRIPE_PLAN.md §1a, flagged there as the most novel/error-prone piece, intentionally shipped after solo billing is proven). |
apps/web/lib/stripe.ts, apps/web/app/api/webhooks/stripe/route.ts, apps/web/app/api/v1/billing/**, apps/web/app/(app)/settings/billing/page.tsx, apps/web/app/admin/billing/page.tsx |
| Admin dashboard | Exists | 15 sections (was 13, missing Billing until this pass): overview, insights/analytics, users, invites, recipe moderation, reports, support, tier limits, billing, webhooks, audit logs, storage, AI config, site settings, changelog | apps/web/app/admin/layout.tsx (adminNav) |
| Feature flags (per-tier) vs feature prefs (per-user cosmetic) | Exists, two distinct systems | Flags gate 3 AI capabilities by tier; prefs let users hide 7 nav sections, no billing implication | apps/web/lib/{feature-flags,feature-prefs}.ts |
| Developer permission (2026-07-22, split 2026-07-23) | Exists | Two separate, orthogonal permissions — not one. users.isDeveloper gates webhooks + self-serve API keys: admin-toggled always, and self-serve toggleable by the user themselves once on a paid tier (no added fee) via PATCH /api/v1/users/me/developer-access; free-tier users still need an admin grant. users.isByokEnabled gates BYOK separately, admin-only, no self-serve — routing real AI spend through Epicure on the user's own key warrants a manual check-in. Previously all three (webhooks/API keys/BYOK) shared one flag with zero self-serve path. Existing users with a webhook/API key were already grandfathered for isDeveloper; a second migration grandfathered existing BYOK users into isByokEnabled specifically. |
apps/web/lib/permissions.ts (hasDeveloperAccess, hasByokAccess, canSelfServeDeveloperAccess), apps/web/lib/api-auth.ts (requireDeveloper, requireByok) |
| User webhooks (personal automation) | Exists | 7 events, Zapier-style, requires developer access | apps/web/lib/webhooks.ts |
| Admin ops webhooks (site-wide) | Exists | 3 events (signup, ticket, report) — admin-only, unrelated to developer access | apps/web/lib/admin-webhooks.ts |
| Support tickets ↔ Gitea two-way sync | Exists, verified real | Outbound (create/comment/close mirror to Gitea issue) + inbound (signed webhook receiver, dedup, loop-safe) | apps/web/lib/gitea.ts, apps/web/app/api/webhooks/gitea/route.ts |
| Self-serve API keys | Exists | Up to 10 per user, scoped full/read, SHA-256 hashed at rest, requires developer access | apps/web/app/api/v1/api-keys |
| Public OpenAPI spec + docs page | Exists | Scalar-rendered at /docs, spec at /api/v1/openapi.json |
apps/web/lib/openapi.ts |
| i18n | Partial | Only en and fr — no other locales |
apps/web/messages/*.json |
| 2FA | Exists | TOTP + backup codes + OTP, passwordless-account-compatible | Better Auth twoFactor plugin |
| Social login | Exists | Google always, GitHub/Discord/generic OIDC if configured | apps/web/lib/auth/server.ts |
| Rate limiting | Exists | Two layers: Better Auth's Redis-backed built-in limiter (tighter on 2FA verify endpoints) + a general sliding-window limiter for AI/API routes | apps/web/lib/rate-limit.ts |
| Invite-gated signup | Exists | Signups can be globally disabled, invite-only fallback; first-ever user auto-promoted to admin | — |
5. PWA, offline, mobile
| Feature | Status | Description | Key files |
|---|---|---|---|
| Installability | Exists | Manifest + SVG icons (no raster/PNG fallback), Chromium beforeinstallprompt button, iOS Safari gets a static "tap Share" instructional banner (no programmatic install API exists on Safari — Apple restriction, not fixable) |
apps/web/app/manifest.ts, apps/web/components/pwa/install-prompt.tsx |
| Service worker | Exists | Cache-first for /cook pages, network-first-with-cache-populate elsewhere, /api/* bypassed |
apps/web/public/sw.js |
| Background Sync | Exists | Chromium via native Background Sync API; Safari/Firefox (no support at all) via an online-event fallback reading the same IndexedDB queue |
apps/web/lib/offline-queue.ts, apps/web/public/sw.js |
| Offline save-for-later | Exists | Per-recipe explicit pin + IndexedDB-tracked list shown on the /offline fallback page |
apps/web/lib/offline-db.ts, apps/web/components/recipe/save-offline-button.tsx |
| Offline scope | Partial | Recipe viewing + one write path (mark-cooked). Shopping lists and meal plan have no offline/queue coverage | — |
| Push (client + server) | Exists end-to-end | Real subscribe/unsubscribe with correct state sync, VAPID, multiple triggers wired | apps/web/lib/push.ts |
| Voice control | Exists | Web Speech API in cooking mode (this was previously assumed missing — it isn't) | apps/web/components/cooking-mode/cooking-mode.tsx |
| Barcode scanning | Exists (pantry only) | BarcodeDetector isn't used, but a manual-entry-triggered Open Food Facts API lookup covers the same use case |
apps/web/app/api/v1/pantry/scan/barcode |
| OS Share Target (share a URL into Epicure from another app) | Exists (Chromium/Android only) | Manifest share_target → /recipes?title&text&url; extracts the shared link from either url or a URL substring in text (senders vary), auto-opens the existing import-from-URL dialog pre-filled and auto-imports. Safari has no Share Target API at all — same Apple-restriction story as install prompts. |
apps/web/app/manifest.ts, apps/web/app/(app)/recipes/page.tsx, apps/web/components/recipe/url-import-dialog.tsx |
| Home-screen widgets, Siri/Assistant shortcuts | Missing, expected — N/A for a pure PWA without a native shell | — | |
| Native mobile app (React Native/Capacitor/Expo) | Missing | Confirmed PWA-only, no native project anywhere in the monorepo | — |
Real gaps (confirmed absent, worth considering)
Ranked roughly by likely value:
Stripe checkout + billing portal— closed 2026-07-23 (solo Pro/Family subscribe/cancel/manage; family multi-user sharing still deferred, see the note in the table above).- Recipe video — no support at all.
Nutrition trend/history view— closed 2026-07-22 (7/30/90-day calorie chart + macro averages).USDA/nutrition-database lookup— closed 2026-07-22 (per-ingredient, gram-convertible ingredients only; AI fills the rest).OS Share Target— closed 2026-07-21 (Chromium/Android only, no Safari equivalent exists).Push click-through handling— closed 2026-07-21 (push/notificationclicklisteners added tosw.js).Anonymous public link for meal plans— closed 2026-07-21 (read-only, with a QR code).- Grocery delivery/live pricing beyond the Instacart stub (which needs a partnership agreement to go live).
- Offline coverage for shopping lists / meal plan — currently recipe-viewing + mark-cooked only.
Moderator-scoped admin routes— closed 2026-07-21 (reports + recipe-unpublish).- Followers-only recipes invisible on the author's own profile grid (2026-07-24) —
u/[username]/page.tsxonly queriesvisibility = "public"for the grid, so a followers-only recipe shows up in a follower's feed but never on the profile it belongs to. Real bug, not a design choice. - Auto-deduct pantry on cook is dead in the UI (2026-07-24) — correct, tested API logic that literally nothing in the product surfaces; both UI callers hardcode it off. Either wire it up (a real feature a home cook would want) or stop pretending it's a shipped feature.
- Model Prefs picker is ungated (2026-07-24) — see "Consumer-friendliness" below; the one clear case of a technical control exposed with no fencing at all.
Consumer-friendliness assessment — "is Epicure too tech-savvy?" (2026-07-24)
Asked directly this pass: has Epicure accumulated too many technical/developer-oriented features for its actual audience (home cooks)? Verdict from all 5 domain audits: mostly no, one real exception.
The one real offender: Settings → AI's Model Prefs picker (model-prefs-form.tsx) — every user, gated by nothing, can select a raw provider + model ID (gpt-4o-mini, o3-mini, claude-opus-4-8...) per AI use-case. A home cook has no reason to know what "o3-mini (reasoning)" means or why they'd pick it over the default. This is the single clearest case of a developer-tool control bolted onto a consumer screen with zero fencing. Worth fixing: hide it behind developer/BYOK access (or just remove the picker for non-technical users and let the admin's configured default silently apply), since right now it's reachable by literally everyone.
Everything else technical is already properly fenced, and mostly invisible by default:
- API keys, both webhook systems (user + admin-ops), BYOK, public OpenAPI/Scalar docs, self-serve developer-access toggle — real engineering surface (auth gating, rate limiting, encryption-at-rest, SSRF validation, delivery/redelivery, replay protection), but every one of them is hidden from the Settings nav by default and only appears once a user has developer access (paid tier + opt-in) or BYOK (admin-only). A casual user never sees these nav entries at all. The audience is third-party integrators / automation hobbyists / self-hosters with their own AI keys — genuinely orthogonal to "save and share recipes," but it doesn't clutter the product because it's opt-in-gated, not default-visible.
- Admin billing complexity, feature-flag matrix, Gitea sync, admin ops webhooks — pure back-office/staff tooling. A home cook never has any path to these screens at all (admin-only). Legitimate SaaS-business infrastructure, zero consumer-facing weight.
- Moderator console — same story, staff-only.
- Every consumer-facing domain checked clean: recipe/meal-plan/pantry/shopping/social UI copy uses plain language throughout (checked directly — no "service worker," "IndexedDB," "webhook," "API," or similar jargon leaks into any user-facing string, including the newer offline/PWA surfaces). Cooking mode, batch cooking, collections, search, notifications — all read as ordinary consumer app features, not power-user tooling.
Net take: the codebase has a lot of technical depth relative to "recipe app," but almost all of it is correctly fenced off from the people it's not for. The fencing itself (developer-access self-serve, BYOK admin-gate, admin-only sections) is well-designed. The Model Prefs picker is the one place that fencing was skipped — fix that one thing and the "too tech-savvy" concern is essentially resolved.
Deliberate design choices (not gaps)
- Direct messaging is 1:1 only — enforced at the schema level, not a missing feature.
- Feature toggles (
userFeaturePrefs) are cosmetic nav-hiding only, never access control — separate from tier-based feature flags. - No native app — PWA-only is the stated architecture, covered well by the offline/install/push work already shipped.
Standing rule
Every time a feature ships, update this file in the same change — add/update its row, move it out of "Real gaps" if it closes one. Bug fixes to existing features don't need an entry unless they change what the feature does.