Skip to content

Recipicity Code Review — April 5, 2026

Reviewer: Claude (automated analysis) Codebase: reelsea/Recipicity (monorepo: apps/api, apps/web, packages/shared) Branch: main (staging) Previous review: March 27, 2026 (69 findings; 41 fixed, 6 deferred, 7 partial)


Summary

Category Critical High Medium Low Info Total
Security -- 3 4 3 4 14
Performance 3 6 11 8 -- 28
Architecture 1 7 11 3 3 25
Total 4 16 26 14 7 67

Key takeaway

The most impactful single fix is extracting Playwright/Chromium out of the API production image (appears as both PERF-1 and ARCH-1). This cuts the container from ~1.3 GB to ~200 MB and removes an unnecessary attack surface.


Security (14 findings)

HIGH

ID Finding Impact
SEC-1 No JWT revocation on user logout. BFF clears the cookie but the JWT remains valid for its full 24-hour TTL. An attacker who exfiltrates a token can use it long after the user logs out. Token replay for up to 24 hours after logout.
SEC-2 GET /bff/auth/get-token exposes raw JWT to JavaScript. This endpoint defeats the purpose of httpOnly cookies. Any XSS vulnerability can exfiltrate the token. Full session hijack via XSS.
SEC-3 Rate limiter bypass via X-Forwarded-For spoofing. trust proxy = 1 allows an attacker behind a single proxy to spoof an internal IP and skip rate limiting entirely. Brute-force and abuse bypass.

Remediation: SEC-1 requires a Redis-backed JWT denylist checked on every authenticated request. SEC-2 should be restricted or removed entirely. SEC-3 needs explicit trusted proxy CIDR configuration instead of a numeric hop count.

MEDIUM

ID Finding Impact
SEC-4 CSRF bypass via missing Origin header. Requests with no Origin header bypass CSRF checks entirely. Cross-site state-changing requests from non-browser clients or crafted requests.
SEC-5 Google mobile OAuth skips idToken verification. Uses accessToken to call UserInfo endpoint instead of verifying the signed idToken. Token substitution attack if accessToken is leaked.
SEC-6 Unvalidated imageType in upload path. No allowlist on imageType parameter; user can set an arbitrary MinIO key prefix. Path traversal within the MinIO bucket; overwrite of unrelated objects.
SEC-7 CSP contains unsafe-inline for script-src on API routes. Weakens XSS mitigation on API-served HTML (Swagger UI, error pages).

LOW

ID Finding Impact
SEC-8 Admin token not validated against sessions table. Stale admin sessions cannot be force-revoked server-side.
SEC-9 SSRF protection is pre-DNS string-match only. DNS rebinding or redirect-based SSRF is not blocked.
SEC-10 E2E test credentials committed to repository. Credential exposure if repo becomes public.

INFO

ID Finding
SEC-11 HTML sanitization runs before Joi validation (ordering concern, no exploit identified).
SEC-12 Swagger UI has no auth guard and no NODE_ENV check.
SEC-13 MinIO connections use no TLS (acceptable within Docker Swarm overlay network).
SEC-14 Next.js version should be verified against recent CVE advisories.

Positive security findings

The codebase demonstrates solid security practices in many areas:

  • Separate JWT signing secrets with 32-character minimum length
  • bcrypt cost factor 12
  • Account lockout after 5 failed attempts within 15 minutes
  • Password-change invalidates existing tokens
  • OAuth CSRF nonce backed by Redis with TTL
  • Magic-byte file type validation on uploads
  • Parameterized queries throughout (Prisma)
  • Docker Secrets with _FILE env-var pattern in production
  • SSRF blocklist for private IP ranges
  • Error handler never leaks stack traces to clients
  • Admin audit logging on all state-changing operations
  • HSTS with preload directive
  • Helmet.js with sensible defaults
  • Redis-backed rate limiting on all public endpoints

Performance (28 findings)

CRITICAL

ID Finding Impact
PERF-1 API Docker image uses Playwright/Chromium as production base (~1.3 GB vs ~200 MB with node:20-slim). Playwright is only needed for recipe URL import fallback when structured data is unavailable. 6x image size, slower deploys, higher memory baseline, larger attack surface.
PERF-2 FeatureFlagService executes 2-4 DB queries per isFeatureEnabled call with zero caching. Called on every AI request and the feature-flags page load (up to 24 DB queries per page). Unnecessary database load on every feature-gated path.
PERF-3 Notification N+1 in notifyFamilyMealAdded. Fetches notification preferences one-by-one per family member. Linear DB round-trips scaling with family size.

HIGH

ID Finding Impact
PERF-4 Search runs two redundant query paths (Prisma ORM + raw SQL) simultaneously. JSONB ingredients search is an unindexed full-table scan. Double query cost; full scan on every ingredient search.
PERF-5 maxTime filter has no index on the computed expression COALESCE(prep_time, 0) + COALESCE(cook_time, 0). Sequential scan for time-filtered recipe queries.
PERF-6 Recipe detail page makes a separate rating aggregation query instead of using the existing batchAverageRatings helper. Extra DB round-trip on every recipe view.
PERF-7 Notification service makes 2-3 sequential user lookups per social event instead of Promise.all. Serialized I/O adds latency to social actions.
PERF-8 Recipe cache key uses JSON.stringify on an object with unstable key ordering, causing cache misses on identical queries. Effective cache-hit rate near zero for complex queries.
PERF-9 Following feed loads all followed-user IDs into Node.js memory for an IN(...) clause instead of using a DB join. Memory pressure and query-plan degradation at scale.

MEDIUM

ID Finding
PERF-10 Feature flags endpoint uncached (24 DB queries per page load).
PERF-11 Admin dashboard runs unbounded COUNT(*) without caching.
PERF-12 Admin database export uses unbounded findMany() on all tables (OOM risk at scale).
PERF-13 Analytics config fetched via head script on every page load; should be a build-time env var.
PERF-14 No AVIF format support in Next.js image config (30% compression gain missed).
PERF-15 AdSense script makes a runtime API call in <head> on every page.
PERF-16 Gzip only, no Brotli compression (15-25% better compression available).
PERF-17 Inconsistent HTTP cache headers across public endpoints.
PERF-18 Custom Redis rate limiter is dead code (redundant with express-rate-limit).
PERF-19 Compression middleware positioned suboptimally in middleware chain.
PERF-20 Image carousel sizes prop incorrect for recipe detail layout.

LOW

ID Finding
PERF-21 Tag saving performs N sequential upserts instead of a batch operation.
PERF-22 Slug generation uses a busy-wait retry loop.
PERF-23 Popular recipes query runs an unnecessary COUNT alongside the data query.
PERF-24 Home page is entirely client-rendered (no SSR or SSG).
PERF-25 No generateStaticParams for popular recipe pages.
PERF-26 email included in USER_PUBLIC_SELECT (leaks PII to public endpoints).
PERF-27 10-second max-age on all public pages (too short for static content, too long for dynamic).
PERF-28 Rate limiter at 100 req/15min may be too restrictive for feed pagination.

Architecture (25 findings)

CRITICAL

ID Finding Impact
ARCH-1 Same as PERF-1. API production image is 1.3 GB due to Playwright base. Extract recipe-scraping into a dedicated sidecar service. Deployment speed, resource usage, separation of concerns.

HIGH

ID Finding Impact
ARCH-2 Shared package (@recipicity/shared) is entirely unused. Types are duplicated byte-for-byte between apps/api and apps/web. Monorepo value unrealized; drift risk between duplicated types.
ARCH-3 34 of 51 route files bypass the service layer and query Prisma directly (collections: 19, meal plans: 15, families: 11 direct Prisma calls). Business logic scattered across route handlers; untestable without HTTP.
ARCH-4 Services import from routes (inverted dependency). aiClient imports from routes/admin/ai; emailTemplate imports from routes/unsubscribe. Circular dependency risk; violates layered architecture.
ARCH-5 ingredients and instructions typed as unknown[] throughout. No compile-time type safety on core domain objects. Runtime errors on malformed data; no editor autocompletion.
ARCH-6 328 console.* calls in route layer bypass the pino structured logger. Logs are unstructured, unsearchable, and missing request context.
ARCH-7 Single points of failure in infrastructure: no PostgreSQL read replica, no Redis HA (Sentinel/Cluster), MinIO single-node. Any node failure takes down all environments.
ARCH-8 Dev server holds SSH deploy authority over production. A compromise of the dev server grants production deployment access. Blast radius of a dev-server compromise includes production.

MEDIUM

ID Finding
ARCH-9 Admin users not in Prisma schema; all admin queries use raw SQL.
ARCH-10 Error handler uses console.error instead of pino.
ARCH-11 Route mounting order is fragile; no automated test for route conflicts.
ARCH-12 Turborepo shared package has no build script; tsc is never invoked.
ARCH-13 Auth state duplicated outside TanStack Query (redundant React context).
ARCH-14 GA Measurement ID hardcoded in source instead of environment variable.
ARCH-15 Slug generation has a TOCTOU race condition (check-then-insert without unique constraint enforcement).
ARCH-16 Analytics dashboard runs 15+ separate COUNT queries instead of a single aggregation.
ARCH-17 Admin auth middleware has no Redis caching (DB hit on every admin request).
ARCH-18 No database migration step in the build/deploy pipeline.
ARCH-19 BFF pattern is narrow but correctly implemented (info, not a finding).

LOW

ID Finding
ARCH-20 Dead code: 3 unused source files.
ARCH-21 MFA service scaffolded but never activated; dead code in production bundle.
ARCH-22 any type used in core service signatures.

INFO

ID Finding
ARCH-23 Rate limiters are per-IP only; AI endpoints need per-user limiting.
ARCH-24 No job queue for long-running import operations (blocks Express worker).
ARCH-25 Monorepo CI not configured (Turborepo cache unused in deployment).

Prioritized Remediation Roadmap

Sprint 1 -- Quick Wins (1-2 days)

# Task Findings Addressed Estimate
1 Wire @recipicity/shared into both apps; delete duplicated types ARCH-2 1 hour
2 Move route-imported helpers to lib/; fix inverted dependencies ARCH-4 1 hour
3 Replace console.* with pino in errorHandler + adminAuth ARCH-6, ARCH-10 2 hours
4 Delete 3 dead-code files ARCH-20 15 min
5 Add Redis caching to admin auth middleware ARCH-17 1 hour
6 Add per-user key to AI rate limiters ARCH-23 30 min
7 Add imageType allowlist in upload route SEC-6 15 min
8 Cache FeatureFlagService results in Redis (60s TTL) PERF-2, PERF-10 2 hours
9 Fix unstable cache keys (sort keys + hash) PERF-8 30 min
10 Rotate E2E credentials; add to .gitignore SEC-10 30 min

Sprint 2 -- High Impact (1-2 weeks)

# Task Findings Addressed Estimate
1 Extract Playwright into a scraper sidecar service PERF-1, ARCH-1 2-3 days
2 Consolidate search to single query path; add GIN index on ingredients PERF-4 1 day
3 Add expression index for totalTime filter PERF-5 1 hour
4 Batch notification queries (fix N+1 patterns) PERF-3, PERF-7 1 day
5 Extract service layer for collections, meal plans, families ARCH-3 2-3 days
6 Implement Redis-backed JWT denylist for logout SEC-1 1 day
7 Restrict or remove /bff/auth/get-token endpoint SEC-2 2 hours
8 Harden trust proxy configuration with explicit CIDR SEC-3 1 hour

Sprint 3 -- Architectural (ongoing)

# Task Findings Addressed Estimate
1 Establish unit test baseline (auth, recipe, usage services) ARCH-3 1 week
2 Introduce BullMQ job queue for imports ARCH-24 1 week
3 Add AdminUser model to Prisma schema ARCH-9 2 days
4 Automate DB migration in deploy pipeline ARCH-18 1 day
5 Define Ingredient / Instruction types with Zod validation ARCH-5 2 days
6 Add AVIF support + fix image sizes in Next.js config PERF-14, PERF-20 1 hour
7 Replace remaining console.* calls with pino (328 sites) ARCH-6 1 day
8 Configure PostgreSQL read replica for analytics queries ARCH-7 2 days

Appendix: Review Methodology

  • Static analysis: Manual code review of all files in apps/api/src/, apps/web/src/, and packages/shared/.
  • Dependency audit: npm audit output reviewed; no critical vulnerabilities in direct dependencies.
  • Docker analysis: docker image inspect and Dockerfile review for all service images.
  • Runtime observation: Staging environment (staging.recipicity.com) exercised for cache behavior and query patterns.

Previous review: March 27, 2026 (69 findings; 41 fixed in code, 6 deferred, 7 partial).