This example shows two dimensions, the First move section, and the Dropped-during-verification section in full to demonstrate shape, tone, grounding, and the verify pass for a web app. A real report contains all seven dimensions with up to 5 items each.
The "project" here is a fictional B2B customer-analytics dashboard called Cohort — Next.js 14 App Router, React Server Components, Postgres via Prisma, Stripe for billing, deployed on Vercel.
The dimensions covered here (Reliability, Security) are different from the iOS example so a reader sees the platform-specific lenses in action.
Platform: web (inferred —
package.jsonlistsnext@14.2,react@18.3,@prisma/client;next.config.mjspresent) Surveyed: 2026-05-06 Coverage: full —app/,lib/, server actions, middleware, env config, dependency manifests Attractiveness anchor: Linear (user-supplied)
- Location:
lib/api/client.ts:34 - Proof:
// lib/api/client.ts:32-40 — quote covers entire fetcher; no signal, no retry wrapper. export async function fetchAnalytics<T>(path: string, init?: RequestInit): Promise<T> { const res = await fetch(`${BASE_URL}${path}`, { ...init, headers: { ...defaultHeaders, ...init?.headers }, }); if (!res.ok) throw new Error(`Analytics ${res.status}`); return res.json(); }
- Verified:
grep -n "AbortSignal\|AbortController\|withRetry\|attempts" lib/api/client.ts→# (no matches) - Do: Wrap in a
withRetry(fn, { attempts: 3, backoff: 'exponential' })helper and add a 10-secondAbortSignal.timeout(10_000). Retry on 502/503/504 and network errors only — not on 4xx. - Why: Vercel logs show ~2% of analytics requests time out at 30 s waiting for a response that already failed upstream. Users see a hung loading state instead of a recoverable error.
- Effort: S · Impact: M
- Location:
app/api/webhooks/stripe/route.ts:18 - Proof:
// route.ts:16-26 — quote covers entire POST handler; no event_id lookup. export async function POST(req: Request) { const sig = req.headers.get('stripe-signature')!; const body = await req.text(); const event = stripe.webhooks.constructEvent(body, sig, WEBHOOK_SECRET); await processEvent(event); // ← direct dispatch, no dedupe return Response.json({ received: true }); }
- Verified:
grep -rn "webhook_events\|event_id\|onConflict.*event" app/api/webhooks/ prisma/schema.prisma→# (no matches) - Do: Add a
webhook_eventstable with a unique constraint on(provider, event_id)and short-circuit duplicates with200 OKbefore any side effects. - Why: Stripe retries webhooks for up to 3 days. Without idempotency a delayed retry can double-credit a customer or re-fire downstream emails — both real incidents this quarter.
- Effort: S · Impact: L
- Location:
app/(dashboard)/segments/page.tsx:12 - Proof:
// page.tsx:10-18 — quote covers the data fetch; no cache wrapper. export default async function SegmentsPage({ searchParams }) { const segments = await prisma.segment.findMany({ // ← runs every request where: { orgId: getOrgId() }, orderBy: { updatedAt: 'desc' }, }); return <SegmentsTable segments={segments} />; }
- Verified:
grep -rn "unstable_cache\|revalidateTag" app/(dashboard)/segments/ app/actions/segments.ts→# (no matches) - Do: Wrap with
unstable_cachekeyed by(orgId, filterHash)with a 5-minute TTL, and invalidate viarevalidateTag('segments')from the four mutation paths inapp/actions/segments.ts. - Why: Cold visits feel slow (~800 ms p50) and the upstream Postgres query runs harder than needed. Cache invalidation is well-defined here (only four mutation paths), so this won't drift.
- Effort: M · Impact: M
- Location:
app/actions/import.ts:47 - Proof:
// import.ts:45-55 — quote covers entire import action; one big transaction. export async function importCsv(file: File) { const rows = await parseCsv(file); await prisma.$transaction( // ← all-or-nothing rows.map(r => prisma.customer.create({ data: r })) ); return { ok: true }; }
- Verified:
grep -n "skippedRows\|errorCsv\|per-row" app/actions/import.ts components/Import*→# (no matches) - Do: Switch to row-level transactions, return a per-row result map, and surface a "1,234 imported, 12 skipped" summary plus a downloadable error CSV.
- Why: Users with messy spreadsheets currently can't import anything until every row is perfect. #1 support ticket and the Reliability gap that costs the most onboarding revenue.
- Effort: M · Impact: L
- Location:
lib/query/keys.ts:1 - Proof:
// keys.ts:1-9 — quote covers the entire export; no version prefix. export const queryKeys = { segments: (orgId: string) => ['segments', orgId] as const, customers: (orgId: string, q?: string) => ['customers', orgId, q] as const, events: (id: string) => ['events', id] as const, };
- Verified:
grep -n "__v\|CACHE_VERSION\|persistVersion" lib/query/→# (no matches) - Do: Prefix every key with a single
__v1_constant and bump on shape changes. - Why: Two production bugs in the last quarter traced to this. One-line change that closes the entire class.
- Effort: S · Impact: M
- Location:
lib/db.ts:7 - Proof:
// lib/db.ts:5-11 — quote covers the client construction. import { PrismaClient } from '@prisma/client'; export const prisma = new PrismaClient({ datasources: { db: { url: process.env.NEXT_PUBLIC_DATABASE_URL! }, // ← bundles to client }, });
- Verified:
next build && grep -c "DATABASE_URL" .next/static/chunks/*.js→12(credential leaks into 12 client chunks) - Do: Rename to
DATABASE_URL, ensure the import path runs server-side only (move intolib/server/db.tsand re-export withimport 'server-only'at the top), and verify the bundle-grep returns0post-fix. - Why: This is a credential leak. The DB is currently behind IP allowlisting that masks the impact, but security depth-in-defense should never depend on a single layer that wasn't designed as the last line.
- Effort: S · Impact: L
- Location:
next.config.mjsand a newmiddleware.ts - Proof:
// next.config.mjs (entire file) — no headers() function, no CSP. /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, experimental: { ppr: true } }; export default nextConfig;
- Verified:
grep -rn "Content-Security-Policy\|nonce" next.config.mjs middleware.ts app/2>/dev/null →# (no matches; middleware.ts does not exist) - Do: Start with a report-only policy (
default-src 'self'; script-src 'self' 'nonce-...' https://js.stripe.com; report-uri /api/csp) and enforce after two weeks of clean reports. - Why: Mitigates an entire class of XSS that the team has been lucky to avoid. Free win for a SaaS app handling customer data.
- Effort: M · Impact: M
- Location:
lib/auth/storage.ts:14 - Proof:
// lib/auth/storage.ts:12-20 — JWT stored client-side, readable from any script. export function setSession(token: string) { localStorage.setItem(SESSION_KEY, token); // ← reachable from any JS } export function getSession() { return localStorage.getItem(SESSION_KEY); }
- Verified:
grep -rn "Set-Cookie\|cookies()\|HttpOnly" app/actions/auth.ts lib/auth/→# (no matches) - Do: Switch to HttpOnly, Secure, SameSite=Lax cookies set by the server-side login action via Next's
cookies()helper; rework the API client to omit the bearer header and rely on cookies. - Why:
localStorageis reachable from any script that runs in the page; an XSS in any third-party widget steals every session. Cookies move the secret out of JS reach. - Effort: M · Impact: L
- Location:
package.json,.github/workflows/ci.yml - Proof:
// `pnpm audit --prod --audit-level=high` output (excerpt): 18 vulnerabilities (12 moderate, 3 high, 3 low) found high: cookie@<0.7.0 (CVE-2024-47764) high: ws@>=8.0.0 <8.17.1 (CVE-2024-37890) high: serialize-javascript@<6.0.2 (CVE-2024-11831) - Verified:
grep -n "audit" .github/workflows/ci.yml→# (no matches; no audit gate in CI) - Do: Drop unused packages where possible, pin or override unavoidable transitive deps via
pnpm.overrides, and addpnpm audit --prod --audit-level=highas a CI gate. - Why: Two of the three highs are in active dependency trees. The CI gate prevents new ones from sliding in unnoticed during the next dependency bump.
- Effort: S · Impact: M
- Location:
app/(admin)/layout.tsx:8 - Proof:
// (admin)/layout.tsx:6-14 — auth check happens AFTER the bundle ships. export default async function AdminLayout({ children }) { const user = await getCurrentUser(); if (!user?.isAdmin) redirect('/'); // ← runs client-side after JS loads return <AdminShell>{children}</AdminShell>; }
- Verified:
grep -n "matcher\|/admin" middleware.ts 2>/dev/null→# (no middleware.ts file at repo root) - Do: Add an edge middleware check (
middleware.tswithmatcher: ['/admin/:path*']) that returns 404 before the bundle is served, so the admin chunks are never downloaded by non-admin users. - Why: Reduces leaked surface area. Today, code-splitting hides the worst of it, but the admin chunks remain discoverable via a network-tab pull — and they reveal feature names the team hasn't announced.
- Effort: S · Impact: M
(Performance, Functionality, Stability, Usability, Attractiveness/Sexiness — each up to 5 items in a real report, following the same per-item shape above.)
Move the Postgres connection string out of NEXT_PUBLIC_* (Security)
Of the items here, this is the only one that is a credential leak in production. The IP allowlist masks the impact today, but security depth-in-defense should never depend on a single layer that wasn't designed as the last line — if the allowlist is widened (for a contractor, a partner, a CI runner) the leak becomes immediately exploitable. The fix is one rename, one import-path adjustment, and a bundle-grep verification. Under a day's work, removes a class of "we got lucky" from the threat model, and the team can ship it on the next deploy without coordinating with anyone outside engineering.
- "Add CSRF protection to server actions" — anti-existence grep
grep -rn "next-csrf\|getCsrfToken\|originValidation" app/ lib/returned hits fromlib/auth/csrf.ts. Next.js server actions already enforce same-origin via the framework's built-in origin check; manual CSRF is redundant. - "Use parameterized queries to prevent SQL injection" — every DB call goes through
prisma.*which parameterizes by default; no rawprisma.$queryRawcalls in the repo (grep -rn "\\\$queryRaw\\|raw(" app/ lib/returned# (no matches)). Category error. - "Enable HTTP/2 server push for critical CSS" — HTTP/2 push has been deprecated by every major browser (Chrome removed it in M106); the proposed fix is for a feature that no longer exists. Not actionable.
- "Move bcrypt to argon2 for password hashing" — Citation failed:
grep -rn "bcrypt\|argon2" lib/auth/returned# (no matches). Cohort uses a third-party auth provider (Auth0) for credentials; the app doesn't hash passwords itself, so the proposed migration doesn't apply. - "Add helmet.js for security headers" —
helmetis an Express middleware; this is a Next.js App Router app where security headers are set vianext.config.mjsheaders()(covered by SEC #2 above). Wrong tool for the framework.
- "Add an open-source observability pipeline" — observability is a real gap, but didn't land in any one dimension's top 5; opening as a separate spike rather than padding here.
- "Migrate Pages Router fragments to App Router" — partially complete; the audit reflects current state, not in-flight work.
- "Add SOC 2 Type 1 evidence collection" — process work, not codebase work; out of scope for an elevate audit.