You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Big Pickle — Overall Verdict on System Design & Performance (Latency Tiering + HLD + Core Web Vitals)
Standalone verdict in the Big Pickle review series (see #1421 finance, #1433 booking). This one audits the whole platform across three categories: 1) Subsystem Latency Tiering & SLA/SLO Alignment, 2) Core High-Level Design (HLD) fundamentals, 3) Frontend Performance & Core Web Vitals. 11 dimensions, each scored 1–5 against the current dev head (369f24bc0).
Scope rule (per this audit): raw Lighthouse/CrUX/RUM numbers are not derivable from a static read — every CWV claim below is a code-level risk assessment, and the first remediation item schedules the real measurement. Performance budgets (LCP/INP budgets, TTFB SLOs) are proposed, not yet adopted.
1. Executive verdict
The money path (booking → checkout → allocation → payments → payouts) is the most disciplined subsystem in this codebase — distributed Lua-CAS locks with a provable global order, Serializable-txn retries, fail-closed crons, per-service circuit breakers, and hard DB connect/query budgets. That layer is genuinely near-production-grade. The platform layer is not: every route and every scheduled sweep executes inside one Netlify serverless function (___netlify-server-handler), there is no task/event queue (no qstash/inngest/bullmq/sqs/kafka — verified absent; middleware.ts:65 still lists a vestigial /api/inngest/ prefix), background work is a GitHub-Actions cron fleet plus a 5-minute ticker that fires 10 cleanup routes at the same function, and the landing page — the LCP page — hydrates 15 distinct client components that each import framer-motion. Combined with a documented ~24.9–31.5s cold-start event-loop stall under concurrent load (#1120/#1124, re-measured 2026-08-23) and a cross-region Supabase pooler in the request path, TTFB on dynamic routes is effectively unbounded today.
The pattern is consistent across the audit: correct, disciplined, and well-documented at the seams (locks, timeouts, CAS, ADRs) but with no queuing layer, no read/write separation, no origin TTFB contract, and a render pipeline that pays full client hydration on the public marketing path. Nothing here is a rebuild — it is a set of quick-win optimizations and a short list of architectural refactors (queue, function split, motion/queue hydration de-risking) before this scales.
2. Scorecard (11 dimensions, 1–5)
#
Category
Dimension
Score
Status / one-line evidence
1
Latency Tiering
Critical Path Isolation
3
Webhooks ack-200 + after() deferral, but deferred work shares the request function (#1446 proves the risk)
2
Latency Tiering
Asynchronous Decoupling
2
No task queue; GH-Actions crons (ADR 22: sub-hourly → ~every 100 min) + 5-min ticker into the same function
3
Latency Tiering
Fault Isolation & Degradation
3
Robust breakers/timeouts; breaker state is per-instance (close to inert) + single-function coalescing
4
Latency Tiering
Caching & Read/Write Segregation
3
Good ISR/tag layering; no read replicas, cross-region pooler, orphaned programs tag (zero revalidateTag("programs") sites)
5
HLD
Latency vs Throughput
3
Bounded/quota'd queries everywhere; one bundled function; 6-level relation include on a list read
6
HLD
Concurrency & Thread-Pool Safety
3
Money-path locks exemplary; ~25s event-loop stall needs no app code to bite; no job/read concurrency isolation
ISR-prerendered CDN landing + self-hosted Sora are strong; LCP above the fold is a hydrated framer-motion client component
9
Frontend
Interactivity (INP/TBT)
3
15 client framer-motion sections on /; 33ms counter setInterval for 2s; Intl.NumberFormat per render
10
Frontend
Visual Stability (CLS)
4
Mostly dimensioned/aspect-video; fill avatars lack sizes; raw <img>; /feed 404 avatar asset
11
Frontend
Network (TTFB)
3
Only 1 API route sets Cache-Control; dynamic routes inherit the cold-start stall; middleware hits Upstash per request
Overall: 3.2 / 5 — launch-viable, scale-blocked; the two lowest scores (async decoupling, LCP hydration) are the real debt.
3. Verified-correct posture (do NOT regress)
These were re-confirmed against current code and are considered correct by design:
Webhook ack-200 + idempotency: Razorpay returns a fast 200 and defers via after() to fit the 5s webhook timeout (app/api/webhooks/razorpay/route.ts:113,187–189); the idempotency key is built from signed material only (lines 158–166). Stripe mirrors it.
Emails are non-fatal off-path: confirmation email failures never block payment processing (lib/payments/webhooks/handlers.ts:2186–2190, 2277–2281).
Lock architecture: atom-keyed slot-booking:{profile}:{atomISO} covering [start,end), global deadlock-free acquisition order (event → consultee → slot), ownership-checked Lua PEXPIRE renewal, per-flow TTLs, full error taxonomy with retryAfter, and fail-closed acquisition (Redis down → 503, never an unlocked booking) — utils/appointmentlock.ts:266–305,441,493,583–586,671,771,859,924,980.
DB budgets that actually work through Supavisor: 3s connect / 6s query runtime, 30s connect at build, and the crucial insight that only client-side query_timeout bounds pooled traffic (Supavisor ignores statement_timeout) — lib/prisma.ts:21–68,141–149.
Fail-open vs fail-closed split: rate limits + maintenance gate fail open with a finite cache (middleware.ts), money crons fail closed (lib/cron/with-cron-lock.ts:39–44), Redis health probe cached 2s/instance (lib/redis.ts:315–332). The old single-breaker-for-everything coupling was already fixed (Stream production-readiness: everything found, bucketed by launch stage #1280).
The Netlify 25s stall mitigation is final: memory raise measured dead-and-reverted (2048 MB applied 2026-08-22 → 11/12 slow + a 500), lazy-init build reproduced the stall, so "raise memory / lazy init" must not be re-proposed (netlify.toml:18–44).
Strengths: webhooks ack-200 (app/api/webhooks/razorpay/route.ts:187); the after() continuation is the right primitive and is used exactly where the money path cannot afford to await.
No latency tier exists in the deployment model: /api/health, /api/auth, /api/checkout (money), /api/explore/*, and the /api/cleanup/* sweep all resolve to the one___netlify-server-handler (netlify.toml:34–38).
4.2 Asynchronous Decoupling — 2/5
No queue exists. No bullmq/amqp/sqs/kafka/inngest/trigger.dev/qstash in package.json. middleware.ts:65 still authed-prefixes /api/inngest/, but Inngest is not installed (the string's only repo occurrence).
The ticker is not a queue — it is a poller that re-enters the same serverless function 10× every 5 minutes, with no backpressure, no durable queue of work items, and loss tolerance only because "failed sweeps get picked up by the Actions backstop".
Consequence: event → effect latency is bounded by the sweep cadence (up to hours), and every sweep competes with user traffic on the same function. This is a correctness and latency tax.
4.3 Fault Isolation & Degradation — 3/5
Strengths: listed in §3 — the fail-open/fail-closed contract, per-service breakers, cached probes.
Deficiencies:
Breaker + health-cache state is per-instance memory in a short-lived serverless world — the Redis breaker comment itself admits it is "close to inert" because instances rarely accumulate 5 failures before dying (lib/redis.ts:115–119). Under constant new instances, the breaker effectively never trips.
ISR on the public marketing surface: /revalidate=3600 (app/page.tsx:48, with an explicit comment that force-dynamic was uncacheable-harmful, lines 27–33, 46–47), explore pages revalidate=300 (app/explore/experts/page.tsx:30).
Data-layer caching via unstable_cache with tags + the hard-won contract that "tag alone never clears Full Route Cache" (lib/data/public-cache.ts:10, purgeExpertSurfaces at 39–43, purgeReviewSurfaces at 53).
One API route is a model citizen: Cache-Control: public, s-maxage=60, stale-while-revalidate=300 (app/api/explore/recordings/route.ts:37).
Deficiencies:
No CQRS / read-replica layer. Every read — including hot public browse — traverses the pooled Supabase (ap-south-1) from Netlify (ap-southeast-1); cross-region latency is in the request path with no facade.
Orphaned cache tag:app/explore/programs/page.tsx:114,144 register tags: ["programs"], but a repo-wide grep shows zero call sites of revalidateTag("programs") (only announcements, experts, reviews are ever purged). Program counts/level aggregates can serve stale for the full 3600s window with no on-demand bust.
Offset pagination dominates: only lib/data/org-workspace.ts:256–263 uses cursor-on-id; the public directories use offset (lib/data/explore-experts.ts:338/379/408, recordings listing).
5. Category 2 — Core High-Level Design
5.1 Latency vs Throughput — 3/5
Strengths: every findMany seen is bounded (take/limit/BATCH_SIZE); a slow-query hook warns >500ms to Sentry (lib/prisma.ts:120–127); retry budgets are explicitly sized against the ~26s function ceiling (REQUEST_PATH_RETRY_CONFIG 5 attempts, CHECKOUT_WAIT_RETRY_CONFIG ~7s ceiling — utils/appointmentlock.ts:108–117).
Deficiencies:
lib/data/recordings-explore.ts nests a 6-level relation include (meetingSession → slotOfAppointment → appointment → webinar → webinarPlan → consultantProfile) on an offset-paginated list — join cost per page on the hot explore path.
392 API route.ts files all live behind the one handler bundle; there is no route-level throughput isolation or independent autoscaling of hot routes (netlify.toml:34–38).
5.2 Concurrency & Thread-Pool Safety — 3/5
Strengths: the money-path lock suite (§3) is the reference implementation: bounded pg.Pool via PG_POOL_MAX (docs say "set to 1 or 2 in serverless", lib/prisma.ts:61–67), adapter reused through globalThis.
Breaker/probe state is per-instance (see 4.3) — protection is probabilistic at scale.
5.3 Scalability & Statelessness — 4/5
Strengths: stateless serverless; sessions externalized (BetterAuth DB sessions + cookie-presence edge gate in middleware.ts, no DB hit at the edge by design); all mutable coordination lives in Redis/Postgres; horizontal scale via Netlify's autoscaling instances.
Deficiencies: the single-function bundle blocks split-function scaling; per-instance breaker/health-cache memory is lost in churn; the giant handler bundle repeatedly flirted with Netlify's function size cap (mitigated via outputFileTracingExcludes, next.config.mjs:194–210).
6. Category 3 — Frontend Performance & Core Web Vitals
All items below are code-level risk assessments — real LCP/INP/TTFB numbers must come from the measurement sweep in §7.
6.1 Loading (FCP/LCP) — 3/5
Strengths:
The LCP page is ISR-prerendered and served off the CDN with no function invocation (app/page.tsx:27–48).
Single self-hosted font via next/font with display: swap (lib/fonts.ts:12–16, with a good comment on the per-callsite re-hash trap).
next/image globally configured for AVIF+WebP (next.config.mjs:228–230); svg/webp/avif optimized assets exist under public/.
<Suspense> skeletons around the three DB-backed home sections (app/page.tsx:1,103–121).
Deficiencies:
The LCP hero is "use client" with framer-motion + an animated counter (components/home/HeroSection.tsx:4,20,47,78,89): hydration and motion JS are on the critical path above the fold.
All 15/15 landing sections import framer-motion (app/page.tsx:3–17, verified grep -rl framer-motion components/home = 15 of 18 files; 15 of 18 are "use client"). framer-motion is in the initial bundle with no lazy/tiered loading.
No priority/fetchpriority="high" on above-the-fold imagery (hero uses CSS orbs; apply to whatever becomes LCP once measured).
6.2 Interactivity (INP/TBT) — 3/5
AnimatedNumber spins a 33ms setInterval updating state for 2000ms on the landing hero (HeroSection counter, 60 steps: components/home/HeroSection.tsx:24–36) — main-thread churn that should be requestAnimationFrame-driven or off-thread.
Intl.NumberFormat is constructed per render in hooks/useCurrency.ts:132 instead of being module-cached.
Good: Stream scopes, the unified calendar, onboarding steps, and earnings tabs are next/dynamic (components/stream/StreamChatScope.tsx:3, StreamVideoScope.tsx:3, components/scheduling/SafeUnifiedCalendar.tsx:3, app/form/onboarding/page.tsx:35, app/dashboard/consultant/[consultantId]/(features)/earnings/EarningsTabs.tsx:3).
Raw <img> with eslint-disable @next/next/no-img-element at app/explore/recordings/page.tsx:53 and its [slug] page.
404 asset:app/feed/page.tsx:39,78 reference /placeholder-avatar.jpg, which does not exist in public/ (only placeholder-user.jpg, placeholder.svg) → real image miss renders a 404 in dev and prod.
6.4 Network (TTFB) — 3/5
Strengths: edge middleware rate limiting before functions run (DDoS containment, table-driven RATE_LIMIT_RULES); HSTS + CSP + security headers on every response; compression + CDN by Netlify.
Deficiencies:
Only one API route sets a Cache-Control header (app/api/explore/recordings/route.ts:37). All other public GET endpoints are uncached by default.
Middleware performs an Upstash rate-limit round-trip per matching edge request (fail-open by design, but it is an edge dependency on every guarded request).
7. Indexed backlog (new findings from this audit)
No new P0s. These are the findings this audit adds, indexed P1→P3. Severity is performance/architecture impact, not money-path impact (none of these touch the payment invariants).
#
Sev
Layer
Finding
A1
P1
observability
Dynamic-route TTFB is unbounded: the #1124 stall (~25–31.5s burst) has no origin SLO, watchdog, or alert wired to /api/health/cron heartbeats
A2
P1
architecture
No task/event queue: cron sweeps + the 5-min ticker are the only background mechanism (ADR 22 drift, #1446 starvation risk)
A3
P2
data-cache
programs tag is registered (app/explore/programs/page.tsx:114,144) but never revalidated — 0 call sites
A4
P2
caching
Only 1 of 392 API routes sets an HTTP cache header; remaining public GETs are uncacheable-by-default
A5
P2
loader
/ hydrates 15 client framer-motion sections; framer-motion is in the initial bundle on the LCP page
A6
P2
faults
Breaker + health-cache state is per-instance and "close to inert" (lib/redis.ts:115–119) — probabilistic under churn
A7
P2
HLD
Single ___netlify-server-handler bundle hosts all 392 routes — no throughput isolation, no split-function scaling
A8
P2
query
6-level relation include on an offset-paginated list (recordings explore)
33ms counter setInterval (2s) on the hero; Intl.NumberFormat per render
A13
P3
migration
unstable_cache is fully replaced in Next 16 by use cache/Cache Components — plan the migration before next major bump
8. Prioritized remediation index
Quick Wins (days, low risk, no architecture change)
Fix the /feed 404 asset — point app/feed/page.tsx:39,78 at the existing /placeholder-user.jpg (or ship the file). (A9)
Cache-header pass — extend the app/api/explore/recordings/route.ts:37 pattern to all remaining public GET endpoints. (A4)
Crank the programs tag — add a revalidateTag("programs") call on the program-count/level mutations, mirroring the purgeExpertSurfaces contract in lib/data/public-cache.ts. (A3)
Module-cache Intl.NumberFormat in hooks/useCurrency.ts:132; switch AnimatedNumber to requestAnimationFrame. (A12)
sizes on the 8 fill images; swap the two raw <img> to next/image. (A10, A11)
Sweep the vestigial /api/inngest/ middleware prefix or adopt the tool. (A2-adjacent)
Measure first — Lighthouse on /, an explore page, and checkout; record idle-20-min TTFB on a dynamic route; attach CrUX. Validates or disputes the stall on today's deploy and sets the budgets for A1.
Split the server-handler function — carve heavy/background/ticker and public GET surfaces into separate Netlify functions so a sweep or a slow explore query can't stall /api/health, /api/auth, or booking reads. (A7) Re-enumerate function names after any adapter bump (the v1-name gotcha in netlify.toml:34–38 costs a day).
De-risk the landing hydration — a shared lightweight motion wrapper or next/dynamic around the 15 sections cuts framer-motion off the initial bundle; then re-measure. (A5)
Cursor pagination + flattened joins for the deep directories; evaluate a read replica or cached facade so cross-region DB latency leaves the hot read path. (A8)
Plan unstable_cache → use cache for the Next 16 bump; keep the "tag alone never clears Full Route Cache" contract (lib/data/public-cache.ts:10). (A13)
Per the series convention: no db push against the shared Supabase project; validate changes against the seeded dev-server with mock payments and the jest suites under __tests__/ relevant to the touched module. For this audit specifically each Quick Win is verified by (a) the measurement sweep in §8.QW7, (b) a Lighthouse pass on the affected surface, and (c) the existing jest suites where cache/tag behavior is touched (e.g. purgeExpertSurfaces contract tests). The architectural items each need a written design note (ADR) before build, matching how ADR 22/27 already document the cron trade-offs this audit builds on.
Big Pickle — Overall Verdict on System Design & Performance (Latency Tiering + HLD + Core Web Vitals)
1. Executive verdict
The money path (booking → checkout → allocation → payments → payouts) is the most disciplined subsystem in this codebase — distributed Lua-CAS locks with a provable global order, Serializable-txn retries, fail-closed crons, per-service circuit breakers, and hard DB connect/query budgets. That layer is genuinely near-production-grade. The platform layer is not: every route and every scheduled sweep executes inside one Netlify serverless function (
___netlify-server-handler), there is no task/event queue (no qstash/inngest/bullmq/sqs/kafka — verified absent;middleware.ts:65still lists a vestigial/api/inngest/prefix), background work is a GitHub-Actions cron fleet plus a 5-minute ticker that fires 10 cleanup routes at the same function, and the landing page — the LCP page — hydrates 15 distinct client components that each import framer-motion. Combined with a documented ~24.9–31.5s cold-start event-loop stall under concurrent load (#1120/#1124, re-measured 2026-08-23) and a cross-region Supabase pooler in the request path, TTFB on dynamic routes is effectively unbounded today.The pattern is consistent across the audit: correct, disciplined, and well-documented at the seams (locks, timeouts, CAS, ADRs) but with no queuing layer, no read/write separation, no origin TTFB contract, and a render pipeline that pays full client hydration on the public marketing path. Nothing here is a rebuild — it is a set of quick-win optimizations and a short list of architectural refactors (queue, function split, motion/queue hydration de-risking) before this scales.
2. Scorecard (11 dimensions, 1–5)
after()deferral, but deferred work shares the request function (#1446 proves the risk)programstag (zerorevalidateTag("programs")sites)includeon a list readSoraare strong; LCP above the fold is a hydrated framer-motion client component/; 33ms countersetIntervalfor 2s;Intl.NumberFormatper renderaspect-video;fillavatars lacksizes; raw<img>;/feed404 avatar assetCache-Control; dynamic routes inherit the cold-start stall; middleware hits Upstash per requestOverall: 3.2 / 5 — launch-viable, scale-blocked; the two lowest scores (async decoupling, LCP hydration) are the real debt.
3. Verified-correct posture (do NOT regress)
These were re-confirmed against current code and are considered correct by design:
after()to fit the 5s webhook timeout (app/api/webhooks/razorpay/route.ts:113,187–189); the idempotency key is built from signed material only (lines 158–166). Stripe mirrors it.lib/payments/webhooks/handlers.ts:2186–2190, 2277–2281).slot-booking:{profile}:{atomISO}covering[start,end), global deadlock-free acquisition order (event → consultee → slot), ownership-checked LuaPEXPIRErenewal, per-flow TTLs, full error taxonomy withretryAfter, and fail-closed acquisition (Redis down → 503, never an unlocked booking) —utils/appointmentlock.ts:266–305,441,493,583–586,671,771,859,924,980.query_timeoutbounds pooled traffic (Supavisor ignoresstatement_timeout) —lib/prisma.ts:21–68,141–149.middleware.ts), money crons fail closed (lib/cron/with-cron-lock.ts:39–44), Redis health probe cached 2s/instance (lib/redis.ts:315–332). The old single-breaker-for-everything coupling was already fixed (Stream production-readiness: everything found, bucketed by launch stage #1280).headers()/session so the shell stays static (app/layout.tsx:62–69, infra: cross-region latency (Netlify us-east-2 ↔ Supabase ap-south-1) causes Postgres pooler connection timeouts #932) — this is why ISR works at all.netlify.toml:18–44).4. Category 1 — Subsystem Latency Tiering & SLA/SLO Alignment
4.1 Critical Path Isolation — 3/5
Strengths: webhooks ack-200 (
app/api/webhooks/razorpay/route.ts:187); theafter()continuation is the right primitive and is used exactly where the money path cannot afford to await.Deficiencies:
after()still runs inside the same function instance, sharing the event loop and the pool with the next request. [payments][P1 HIGH] Post-commit work in after() starves the single-connection pool: two unawaited ~39 s Novu triggers overlap the chat-channel step and it dies at the 3 s connect timeout #1446 is the live proof: two unawaited ~39s Novu triggers overlapped the chat-channel step inside the single-connection pool and it died at the 3s connect timeout. Every newafter()job is a new opportunity for this class of starvation./api/health,/api/auth,/api/checkout(money),/api/explore/*, and the/api/cleanup/*sweep all resolve to the one___netlify-server-handler(netlify.toml:34–38).4.2 Asynchronous Decoupling — 2/5
package.json.middleware.ts:65still authed-prefixes/api/inngest/, but Inngest is not installed (the string's only repo occurrence).*/5 * * * *(netlify/functions/cron-tick.mts:19). ADR 22 measured sub-hourly GH Actions cron drifting to ~once per hundred minutes (Cron architecture: measured GitHub Actions throttling breaks the sub-hourly fleet — move ~10 event-shaped jobs to QStash, keep business crons on GA #866); ADR 27 added the ticker as the workaround.4.3 Fault Isolation & Degradation — 3/5
Strengths: listed in §3 — the fail-open/fail-closed contract, per-service breakers, cached probes.
Deficiencies:
lib/redis.ts:115–119). Under constant new instances, the breaker effectively never trips.connect_timeout/setTimeoutcannot fire while the loop is blocked.4.4 Caching & Read/Write Segregation — 3/5
Strengths:
/revalidate=3600(app/page.tsx:48, with an explicit comment thatforce-dynamicwas uncacheable-harmful, lines 27–33, 46–47), explore pagesrevalidate=300(app/explore/experts/page.tsx:30).unstable_cachewith tags + the hard-won contract that "tag alone never clears Full Route Cache" (lib/data/public-cache.ts:10,purgeExpertSurfacesat 39–43,purgeReviewSurfacesat 53).Cache-Control: public, s-maxage=60, stale-while-revalidate=300(app/api/explore/recordings/route.ts:37).Deficiencies:
app/explore/programs/page.tsx:114,144registertags: ["programs"], but a repo-wide grep shows zero call sites ofrevalidateTag("programs")(onlyannouncements,experts,reviewsare ever purged). Program counts/level aggregates can serve stale for the full 3600s window with no on-demand bust.lib/data/org-workspace.ts:256–263uses cursor-on-id; the public directories use offset (lib/data/explore-experts.ts:338/379/408, recordings listing).5. Category 2 — Core High-Level Design
5.1 Latency vs Throughput — 3/5
Strengths: every
findManyseen is bounded (take/limit/BATCH_SIZE); a slow-query hook warns >500ms to Sentry (lib/prisma.ts:120–127); retry budgets are explicitly sized against the ~26s function ceiling (REQUEST_PATH_RETRY_CONFIG5 attempts,CHECKOUT_WAIT_RETRY_CONFIG~7s ceiling —utils/appointmentlock.ts:108–117).Deficiencies:
lib/data/recordings-explore.tsnests a 6-level relationinclude(meetingSession → slotOfAppointment → appointment → webinar → webinarPlan → consultantProfile) on an offset-paginated list — join cost per page on the hot explore path.route.tsfiles all live behind the one handler bundle; there is no route-level throughput isolation or independent autoscaling of hot routes (netlify.toml:34–38).5.2 Concurrency & Thread-Pool Safety — 3/5
Strengths: the money-path lock suite (§3) is the reference implementation: bounded
pg.PoolviaPG_POOL_MAX(docs say "set to 1 or 2 in serverless",lib/prisma.ts:61–67), adapter reused throughglobalThis.Deficiencies:
5.3 Scalability & Statelessness — 4/5
Strengths: stateless serverless; sessions externalized (BetterAuth DB sessions + cookie-presence edge gate in
middleware.ts, no DB hit at the edge by design); all mutable coordination lives in Redis/Postgres; horizontal scale via Netlify's autoscaling instances.Deficiencies: the single-function bundle blocks split-function scaling; per-instance breaker/health-cache memory is lost in churn; the giant handler bundle repeatedly flirted with Netlify's function size cap (mitigated via
outputFileTracingExcludes,next.config.mjs:194–210).6. Category 3 — Frontend Performance & Core Web Vitals
6.1 Loading (FCP/LCP) — 3/5
Strengths:
app/page.tsx:27–48).next/fontwithdisplay: swap(lib/fonts.ts:12–16, with a good comment on the per-callsite re-hash trap).next/imageglobally configured for AVIF+WebP (next.config.mjs:228–230);svg/webp/avifoptimized assets exist underpublic/.<Suspense>skeletons around the three DB-backed home sections (app/page.tsx:1,103–121).Deficiencies:
"use client"with framer-motion + an animated counter (components/home/HeroSection.tsx:4,20,47,78,89): hydration and motion JS are on the critical path above the fold.app/page.tsx:3–17, verifiedgrep -rl framer-motion components/home= 15 of 18 files; 15 of 18 are"use client"). framer-motion is in the initial bundle with no lazy/tiered loading.priority/fetchpriority="high"on above-the-fold imagery (hero uses CSS orbs; apply to whatever becomes LCP once measured).6.2 Interactivity (INP/TBT) — 3/5
AnimatedNumberspins a 33mssetIntervalupdating state for 2000ms on the landing hero (HeroSectioncounter, 60 steps:components/home/HeroSection.tsx:24–36) — main-thread churn that should berequestAnimationFrame-driven or off-thread.Intl.NumberFormatis constructed per render inhooks/useCurrency.ts:132instead of being module-cached.next/dynamic(components/stream/StreamChatScope.tsx:3,StreamVideoScope.tsx:3,components/scheduling/SafeUnifiedCalendar.tsx:3,app/form/onboarding/page.tsx:35,app/dashboard/consultant/[consultantId]/(features)/earnings/EarningsTabs.tsx:3).6.3 Visual Stability (CLS) — 4/5
Strengths: dimensioned
next/imagepredominates; recordings thumbnail surfaces are already insideaspect-videocontainers.Deficiencies (all low-effort):
fillnext/imageavatars/thumbnails with nosizes(wasted bytes + async-decode layout risk):app/explore/programs/plans/consultations/[consultationPlanId]/components/ConsultationDetails.tsx:138app/explore/programs/plans/subscriptions/[subscriptionPlanId]/components/SubscriptionDetails.tsx:160app/explore/programs/plans/webinars/[webinarPlanId]/components/WebinarDetails.tsx:228,280app/explore/programs/plans/classes/[classPlanId]/components/ClassDetails.tsx:266,319app/explore/experts/[consultantId]/components/ProfileHeader.tsx:41app/dashboard/consultant/[consultantId]/(features)/recordings/components/RecordingCard.tsx:110app/explore/enterprise/organisations/[orgSlug]/page.tsx:232,305<img>witheslint-disable @next/next/no-img-elementatapp/explore/recordings/page.tsx:53and its[slug]page.app/feed/page.tsx:39,78reference/placeholder-avatar.jpg, which does not exist inpublic/(onlyplaceholder-user.jpg,placeholder.svg) → real image miss renders a 404 in dev and prod.6.4 Network (TTFB) — 3/5
Strengths: edge middleware rate limiting before functions run (DDoS containment, table-driven
RATE_LIMIT_RULES); HSTS + CSP + security headers on every response; compression + CDN by Netlify.Deficiencies:
Cache-Controlheader (app/api/explore/recordings/route.ts:37). All other public GET endpoints are uncached by default.7. Indexed backlog (new findings from this audit)
No new P0s. These are the findings this audit adds, indexed P1→P3. Severity is performance/architecture impact, not money-path impact (none of these touch the payment invariants).
/api/health/cron heartbeatsprogramstag is registered (app/explore/programs/page.tsx:114,144) but never revalidated — 0 call sites/hydrates 15 client framer-motion sections; framer-motion is in the initial bundle on the LCP pagelib/redis.ts:115–119) — probabilistic under churn___netlify-server-handlerbundle hosts all 392 routes — no throughput isolation, no split-function scalingincludeon an offset-paginated list (recordings explore)/feedreferences missing/placeholder-avatar.jpg(404 asset)fillimages withoutsizes(8 sites listed in 6.3)<img>+ eslint-disable on recordings pagessetInterval(2s) on the hero;Intl.NumberFormatper renderunstable_cacheis fully replaced in Next 16 byuse cache/Cache Components — plan the migration before next major bump8. Prioritized remediation index
Quick Wins (days, low risk, no architecture change)
/feed404 asset — pointapp/feed/page.tsx:39,78at the existing/placeholder-user.jpg(or ship the file). (A9)app/api/explore/recordings/route.ts:37pattern to all remaining public GET endpoints. (A4)programstag — add arevalidateTag("programs")call on the program-count/level mutations, mirroring thepurgeExpertSurfacescontract inlib/data/public-cache.ts. (A3)Intl.NumberFormatinhooks/useCurrency.ts:132; switchAnimatedNumbertorequestAnimationFrame. (A12)sizeson the 8fillimages; swap the two raw<img>tonext/image. (A10, A11)/api/inngest/middleware prefix or adopt the tool. (A2-adjacent)/, an explore page, and checkout; record idle-20-min TTFB on a dynamic route; attach CrUX. Validates or disputes the stall on today's deploy and sets the budgets for A1.Architecture (weeks, high impact)
after()-starvation class. (A2)/api/health,/api/auth, or booking reads. (A7) Re-enumerate function names after any adapter bump (the v1-name gotcha innetlify.toml:34–38costs a day).next/dynamicaround the 15 sections cuts framer-motion off the initial bundle; then re-measure. (A5)unstable_cache→use cachefor the Next 16 bump; keep the "tag alone never clears Full Route Cache" contract (lib/data/public-cache.ts:10). (A13)/api/healthstall signals andcron:heartbeat:last; tie ticket A1 to The 3s Prisma connect budget is not enforced — connect timeout fires at t+29.8s and pins the function for 30s #1120/A newly created function instance stalls its event loop for ~24s before any application work #1124 instead of leaving TTFB unbounded.9. Verification posture
Per the series convention: no
db pushagainst the shared Supabase project; validate changes against the seeded dev-server with mock payments and the jest suites under__tests__/relevant to the touched module. For this audit specifically each Quick Win is verified by (a) the measurement sweep in §8.QW7, (b) a Lighthouse pass on the affected surface, and (c) the existing jest suites where cache/tag behavior is touched (e.g.purgeExpertSurfacescontract tests). The architectural items each need a written design note (ADR) before build, matching how ADR 22/27 already document the cron trade-offs this audit builds on.