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
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). |
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).