Skip to content

Recipicity — Complete Architecture Review

Date: 2026-02-25 Last Updated: 2026-02-25 (immediate fixes applied) Scope: Full stack — API, Frontend, Admin, Database, Security, Infrastructure Reviewers: 5 specialist agents (Backend Architect, Security Auditor, Frontend Engineer, Database Specialist, Comprehensive Reviewer)


Executive Summary

Recipicity is a well-built social recipe platform with strong security fundamentals (parameterized queries, JWT validation, BFF proxy, security headers, audit logging) and an impressively lean frontend (only 5 runtime deps). However, four systemic issues threaten production scalability:

  1. N+1 query epidemic — up to 100 extra DB queries per request on key endpoints
  2. Zero transactions — multi-step writes risk data inconsistency on partial failure
  3. ~~Redis underutilization — Redis exists but is barely used for caching; rate limiting is per-container in-memory~~ Partially fixed — rate limiting now Redis-backed (H6)
  4. Unnecessary client rendering — Homepage and 18+ protected pages ship as client components when server rendering would improve LCP and reduce bundle size

Total findings across all reviews: 70+

Severity Count Key Themes
Critical ~~7~~ 2 remaining ~~Privilege escalation~~, N+1 queries, no transactions, ~~homepage LCP~~, ~~memory leak in AI search~~, ~~stored XSS in JSON-LD~~, ~~no service layer~~
High ~~14~~ 10 remaining SSRF, ~~root containers~~, missing auth checks, no caching, no loading states, accessibility, ~~duplicate PrismaClient~~, UsageService query fan-out
Medium ~~25+~~ 24 remaining Validation gaps, ~~code duplication~~, CSRF, missing indexes, inconsistent patterns
Low 20+ Naming, minor a11y, pagination at scale, cosmetic inconsistencies

CRITICAL Findings (Fix Immediately)

~~C1. Privilege Escalation — Legacy Import Trusts Client userId~~ FIXED

Files: recipeImport.routes.ts:946 | Flagged by: Backend, Security, Comprehensive Risk: Any authenticated user can create recipes under ANY other user's account.

// FIXED: userId now derived from authenticated token
const { url, saveAsDraft = true } = req.body;
const userId = req.user?.userId;
Status: :white_check_mark: Fixed 2026-02-25. Deployed to staging.

C2. N+1 Query Epidemic

Files: users.ts:377-388, users.ts:470-485, tags.ts:223-281, recipes.ts:319-323 | Flagged by: Backend, Database, Comprehensive - User search (50 users): 100 extra queries per request - Tag recipe listing (12 recipes, auth'd): 48 extra queries per request - Recipe listing: fetches ALL individual rating rows instead of aggregating - Profile view: 3 extra count queries each

Fix: Use Prisma _count includes or single raw SQL with subqueries. Batch user-specific lookups (likes, bookmarks, ratings) using findMany with recipeId: { in: recipeIds }. Effort: 1-2 days. Impact: 10-50x reduction in DB queries.

C3. Zero Transaction Usage

Files: bookmarks.ts:176-206, recipes.ts:673-743 | Flagged by: Database, Backend, Comprehensive Zero uses of prisma.$transaction() in entire codebase. Bookmark + collection sync, recipe + tag creation can leave data inconsistent on partial failure. Effort: 1 day.

C4. Homepage Is a Client Component (LCP Bottleneck)

Files: home-content.tsx:1 | Flagged by: Frontend Entire homepage forced into client bundle because of 2 conditional CTA links. Hero image uses CSS background-image to external Unsplash URL, bypassing Next.js Image optimization. Fix: Split into server component + tiny <HomeCTA /> client island. Replace CSS background with <Image priority>. Effort: 2 hours.

~~C5. AI Ingredient Search Loads All Recipes Into Memory~~ FIXED

File: aiRecipeGeneration.service.ts:223-244 | Flagged by: Comprehensive prisma.recipe.findMany() with no take limit fetches EVERY public recipe into Node.js memory for in-process filtering. Will OOM the container at scale. Status: :white_check_mark: Fixed 2026-02-25. Added take: 500 limit with orderBy: createdAt desc.

~~C6. Stored XSS via JSON-LD Script Injection~~ FIXED

File: json-ld.tsx:3 | Flagged by: Comprehensive, Frontend JSON.stringify(data) does not escape </script>. A recipe title containing </script><script>alert(1)</script> breaks out of the JSON-LD script block. Status: :white_check_mark: Fixed 2026-02-25. Added .replace(/<\/script/gi, '<\\/script') after stringify.

~~C7. Multiple new PrismaClient() Instances (Bypass Singleton)~~ FIXED

Files: admin/users.ts:8, nutrition.ts:14 | Flagged by: Comprehensive These files create their own PrismaClient instead of importing the singleton. Each opens a separate connection pool, bypasses the slug auto-generation extension, and increases PgBouncer pressure. Status: :white_check_mark: Fixed 2026-02-25. Both files now import from lib/database.ts.


HIGH Findings

# Finding Source Status
~~H1~~ ~~Docker API container runs as root (no USER directive)~~ Security :white_check_mark: FIXED
H2 SSRF via recipe import — DNS rebinding + IPv6 bypass Security Open
H3 optionalAuthMiddleware doesn't check active/banned status Backend Open
~~H4~~ ~~Admin logout broken — bcrypt hashing produces different hash each time~~ Security :white_check_mark: FIXED
H5 No Redis caching on expensive queries (listings, popular, cuisines) Database Open
~~H6~~ ~~Rate limiting per-container not shared (in-memory, not Redis)~~ Database+Security :white_check_mark: FIXED
H7 No loading.tsx skeletons on 18 of 20 routes Frontend Open
H8 No next: { revalidate } on any server-side fetch Frontend Open
H9 Dropdown menus fail WCAG AA keyboard accessibility Frontend Open
H10 Zero next/dynamic lazy loading for modals/heavy components Frontend Open
~~H11~~ ~~getUserId helper duplicated 6+ times (4 different implementations)~~ Backend :white_check_mark: FIXED
H12 Feature check boilerplate duplicated 5+ times in import routes Backend Open
H13 UsageService runs 15 DB queries per usage summary call Comprehensive Open
~~H14~~ ~~unhandledRejection calls exit(0) — Swarm sees clean exit, may not restart~~ Comprehensive :white_check_mark: FIXED

MEDIUM Findings (Summarized)

# Finding Source
~~M1~~ ~~Unauthenticated data-deletion status endpoint leaks emails~~ Security
M2 No CSRF token; null-origin CORS bypass Security
M3 Regex HTML sanitizer (bypassable with svg/img) Security
M4 Duplicate comment endpoints (social.ts + comments.ts) Backend
M5 Inline route handlers in server.ts (subscription plans, 120 lines) Backend
M6 No validation on profile update (schema exists but unused) Backend
M7 Notification settings accepts arbitrary fields (userId injection) Backend
M8 Email preferences accepts arbitrary JSON Backend
M9 CUID validation middleware exists but never applied Backend
M10 Missing indexes: User (active, oauth), GroceryList (userId), Report Database
M11 Collections list eager-loads ALL items + recipes + authors Database
M12 Duplicate notification preference storage (3 separate locations) Database
M13 MinIO client initialized twice (upload.ts + users.ts) Backend
M14 18 protected pages unnecessarily fully client-rendered Frontend
M15 AuthProvider re-fetches user on every page navigation (no cache) Frontend
M16 Notification polling continues in background tabs Frontend
M17 Inconsistent error response shapes across endpoints Backend
M18 Missing rate limiting on upload and tags routes Backend
M19 Admin audit middleware logs entire request bodies (GDPR concern) Backend
M20 No nested error boundaries in frontend Frontend
M21 Silent error swallowing (empty catch blocks, no monitoring) Frontend
M22 CSP allows unsafe-inline and unsafe-eval in scripts Frontend
M23 Cookie consent modal lacks focus trap + ARIA dialog Frontend
M24 Recipe search uses ILIKE on JSONB (full table scan, no GIN index) Database
M25 Analytics reachable at both /api/analytics AND /api/admin/analytics Comprehensive
M26 OAuth code stored in resetToken column — concurrent login/reset clobber Comprehensive
M27 Admin backup exports to container filesystem (lost on deploy, incomplete) Comprehensive

Cross-Cutting Themes

1. Caching Gap Spans the Entire Stack

  • Backend: No Redis query caching (H5)
  • Frontend: No revalidate on server fetches (H8)
  • Infrastructure: Rate limiting per-container (H6)

A unified caching strategy (Redis for API, ISR for public pages, Redis-backed rate limiting) would address all three simultaneously.

2. Security Tooling Built But Not Wired Up

Joi validation schemas exist but several endpoints don't use them (M6). validateObjectId middleware exists but is never applied (M9). ~~RedisManager.rateLimit() exists but rate limiter uses in-memory store.~~ (Fixed: H6) Pattern: security tools are built, just not connected.

3. DRY Violations Create Maintenance Risk

~~6+ copies of getUserId~~ (Fixed: H11 — consolidated into auth-helpers.ts), 5 copies of feature check boilerplate, 3 copies of asyncHandler, 4 copies of email preferences parsing, 2 MinIO client initializations. Each duplicate is a place where fixes can be missed.

4. Frontend Performance vs. Server Components

The recipe detail page demonstrates the correct pattern (server component with client islands). But the homepage and all 18 protected pages don't follow it, missing streaming SSR, reducing LCP, and inflating the JS bundle.


Prioritized Action Plan

Immediate — Fix This Week :white_check_mark: COMPLETED

All 10 immediate items fixed and deployed to staging on 2026-02-25.

# Action Status
1 Fix req.body.userIdreq.user?.userId in recipeImport (C1) :white_check_mark: Done
2 Fix JSON-LD XSS by escaping </script> (C6) :white_check_mark: Done
3 Import singleton PrismaClient in admin/users.ts + nutrition.ts (C7) :white_check_mark: Done
4 Add USER appuser to API Dockerfile (H1) :white_check_mark: Done
5 Fix admin logout bcrypt → SHA-256 lookup (H4) :white_check_mark: Done
6 Add auth to data-deletion status endpoint (M1) :white_check_mark: Done
7 Add take: 500 limit to AI ingredient search (C5) :white_check_mark: Done
8 Fix exit(0)exit(1) in unhandledRejection (H14) :white_check_mark: Done
9 Switch rate limiter to Redis store (H6) :white_check_mark: Done
10 Consolidate getUserId/getAdminId into shared auth-helpers.ts (H11) :white_check_mark: Done

Short-Term — Next Sprint (~5-7 days)

# Action Effort
11 Fix N+1 queries in users.ts, tags.ts, recipes.ts (C2) 1-2 days
12 Wrap multi-step writes in $transaction (C3) 1 day
13 Add Redis caching for expensive endpoints (H5) 1 day
14 Convert HomeContent to server component + Image priority (C4) 2 hours
15 Add next: { revalidate } to public server fetches (H8) 2 hours
16 Add loading.tsx skeletons for major routes (H7) 1 day
17 Lazy-load modals with next/dynamic (H10) 2 hours
18 Extract requireFeature() middleware (H12) 1 hour

Medium-Term — This Quarter

# Action Effort
19 Add missing DB indexes (User, GroceryList, Report) (M10) 1 hour
20 Denormalize averageRating/ratingCount on Recipe model 1 day
21 SSRF hardening — resolve-time IP check, IPv6 blocking (H2) 1 day
22 Replace regex sanitizer with DOMPurify (M3) 2 hours
23 Keyboard navigation for dropdown menus (H9) 1 day
24 CSRF tokens on state-changing endpoints (M2) 2 days
25 Convert protected pages to server+client split (M14) 3 days
26 Standardize error response format across all endpoints (M17) 2 days

Long-Term — Strategic

# Action Effort
27 Extract service layer (RecipeService, UserService, etc.) 2-3 sprints
28 PostgreSQL full-text search (tsvector) for recipe search 1 week
29 Cursor-based pagination for high-traffic endpoints 3 days
30 Consolidate notification preferences into single model 2 days

What's Working Well (Preserve These)

  1. Parameterized queries everywhere — Zero SQL injection risk. No $queryRawUnsafe usage.
  2. BFF proxy architecture — httpOnly cookies, CSRF origin check, path allowlist. Production-grade.
  3. 5 runtime dependencies — Incredibly lean frontend. No state management library bloat.
  4. Schema.org Recipe JSON-LD — Full structured data with ingredients, instructions, nutrition, ratings.
  5. Recipe detail page pattern — Server component with client islands is the ideal architecture.
  6. JWT secret validation at startup — Enforces 32-char minimum, prevents admin/user secret reuse.
  7. Docker secrets in production — No hardcoded secrets in source code.
  8. Account lockout + anti-enumeration — Progressive lockout, generic error messages.
  9. Recipe model has 7 well-chosen indexes — Composite indexes on common query patterns.
  10. Optimistic UI updates — Like/bookmark with proper rollback on failure.
  11. Admin auth separation — Separate JWT secret, MFA support, audit logging.
  12. Privacy compliance — GDPR/CCPA, cookie consent with granular categories, data export/deletion.

Full individual reports: