VibeAudit

VibeAudit · Claude Fable 5.1 reads a whole repo and grades it for launch. Free quick scan, $19 deep audit. Scan your own repo →

58/100
NOT READY
90+ ship it
70–89 fix first
<70 not ready

This is the Kiranism Next.js 16 + shadcn admin dashboard starter: Clerk auth, Sentry, TanStack Query/Table/Form, with all product/user data served from an in-memory faker mock. The UI and auth wiring are solid, but the backend surface is not launch-safe: the /api/products and /api/users route handlers accept unauthenticated, unvalidated GET/POST/PUT/DELETE and write to a shared in-memory store, and the middleware protects nothing outside the /dashboard layout. Before real users arrive, protect and validate the API routes, replace the mock store with a real data layer, and fix the Sentry env-flag bug that silently disables error tracking when you follow the example env file.

Next.js 16.2 (App Router, proxy.ts)React 19.2TypeScript 5.7Clerk 7 (auth, organizations, billing)Sentry 10TanStack React Query 5TanStack Table 8TanStack Form + Zod 4nuqsZustandTailwind CSS v4shadcn/ui on Base UIRechartsBun / Docker (standalone output)@faker-js/faker in-memory mock API

365 files reviewed · claude-fable-5-1 · deep audit

Findings (15)

What's wrong

src/proxy.ts (line 5) exports a bare clerkMiddleware() with no createRouteMatcher/auth.protect(), and the comment says protection lives only in the /dashboard layout. The route handlers in src/app/api/products/route.ts (GET line 21, POST line 41), src/app/api/products/[id]/route.ts (PUT line 23, DELETE line 35), src/app/api/users/route.ts (POST line 41) and src/app/api/users/[id]/route.ts (PUT line 12, DELETE line 24) never call auth() or check userId/orgId. There is also no CSRF protection or rate limiting on these JSON endpoints.

Impact

Anyone on the internet can curl POST/PUT/DELETE against your API and create, overwrite or delete records shared by every signed-in user. Today that is the mock store; the moment you follow the template's documented path and swap in Prisma/Drizzle/BFF calls, this becomes an unauthenticated write path to your production database.

Fix

Protect API routes in the middleware and re-check in each handler: // src/proxy.ts import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; const isProtected = createRouteMatcher(['/dashboard(.*)', '/api(.*)']); export default clerkMiddleware(async (auth, req) => { if (isProtected(req)) await auth.protect(); }); // in each route handler const { userId, orgId } = await auth(); if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); Add an org/ownership check (IDOR) once records belong to tenants, and rate-limit mutation endpoints (e.g. @upstash/ratelimit) before launch.

Paste into Cursor / Claude Code
In src/proxy.ts, replace the bare `clerkMiddleware()` with `clerkMiddleware(async (auth, req) => { if (isProtected(req)) await auth.protect(); })` where `isProtected = createRouteMatcher(['/dashboard(.*)', '/api(.*)'])` imported from '@clerk/nextjs/server'. Then in every handler in src/app/api/products/route.ts, src/app/api/products/[id]/route.ts, src/app/api/users/route.ts and src/app/api/users/[id]/route.ts, add `const { userId } = await auth();` (import auth from '@clerk/nextjs/server') at the top and return `NextResponse.json({ error: 'Unauthorized' }, { status: 401 })` when userId is missing. Reason: these route handlers are currently reachable by anonymous users and perform writes.
What's wrong

PUT in src/app/api/products/[id]/route.ts (line 25-26) and src/app/api/users/[id]/route.ts (line 14-15), and POST in both route.ts files (line 42-43), do `const body = await request.json()` and hand it directly to updateProduct/createProduct/updateUser/createUser. In src/constants/mock-api.ts updateProduct spreads `...data` into the record (line 213-217), so a body like {"id":1,"created_at":"x","photo_url":"javascript:..."} overwrites protected fields. Malformed JSON throws and surfaces as a 500. The Zod schemas that exist (src/features/products/schemas/product.ts, src/features/users/schemas/user.ts) are only applied client-side.

Impact

Clients can inject arbitrary fields, corrupt ids and timestamps, store oversized or hostile strings, and crash the handler with bad JSON. With a real ORM this is the classic mass-assignment hole (e.g. setting role or org_id on a user).

Fix

Define a server-side payload schema and parse before touching the store: const productPayload = z.object({ name: z.string().min(2).max(200), category: z.string().min(1), price: z.number().min(0), description: z.string().min(10).max(2000) }).strict(); export async function PUT(request, { params }) { const { id } = await params; const parsed = productPayload.safeParse(await request.json().catch(() => null)); if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); const data = await fakeProducts.updateProduct(Number(id), parsed.data); ... } Also validate that Number(id) is a positive integer.

Paste into Cursor / Claude Code
Add server-side Zod validation to all four API route files (src/app/api/products/route.ts, src/app/api/products/[id]/route.ts, src/app/api/users/route.ts, src/app/api/users/[id]/route.ts). Create a `.strict()` Zod object for the product payload (name, category, price >= 0, description) and the user payload (first_name, last_name, email, phone, role, status) and use `safeParse(await request.json().catch(() => null))` in each POST/PUT; return 400 with the flattened error on failure and only pass `parsed.data` to the store. Also validate `Number(id)` is a positive integer and return 400 otherwise. Reason: the handlers currently pass unvalidated bodies into a spread (`...data`) that allows overwriting id/created_at and crashes on malformed JSON.
What's wrong

src/features/products/api/service.ts and src/features/users/api/service.ts call fakeProducts/fakeUsers, which hold records in a module-level array initialized at import (mock-api.ts line 246, mock-api-users.ts line 191). createProduct assigns `id: this.records.length + 1` (line 182; users line 73): after deleting any record, length shrinks and the next create reuses an existing id, so getProductById/updateProduct/deleteProduct (which use findIndex on id) hit the wrong row. @faker-js/faker is a devDependency imported by runtime server code.

Impact

Every product/user a paying customer creates is lost on redeploy or cold start, different serverless instances show different data, and edits/deletes silently target the wrong record after the first deletion. If you ever install with --omit=dev the server crashes importing faker.

Fix

Before launch, replace the function bodies in the two service.ts files with real persistence (Prisma/Drizzle/Supabase or your backend), scoped by orgId. If you must keep the mock for a staging demo, generate ids with `Math.max(0, ...this.records.map(r => r.id)) + 1` or crypto.randomUUID() and move @faker-js/faker to dependencies.

Paste into Cursor / Claude Code
In src/constants/mock-api.ts (createProduct) and src/constants/mock-api-users.ts (createUser), replace `id: this.records.length + 1` (and the matching photo_url computation) with a monotonic id: `const nextId = this.records.reduce((m, r) => Math.max(m, r.id), 0) + 1`. Then plan the real fix: implement src/features/products/api/service.ts and src/features/users/api/service.ts against a real database, filtering by the caller's Clerk orgId. Reason: the in-memory store loses data on restart and reuses ids after deletes, corrupting update/delete targets.

Quick wins

  • · Change the three `!process.env.NEXT_PUBLIC_SENTRY_DISABLED` checks to `!== 'true'` and fix env.example.txt (5 minutes, restores error tracking).
  • · Delete every `delay()` call in overview slot pages and mock-api files.
  • · Add `const { userId } = await auth(); if (!userId) return 401` to the top of all eight API route handlers.
  • · Set `sendDefaultPii: false` and a production `tracesSampleRate` of 0.1 in both Sentry init files.
  • · Fix the dead `/examples/authentication` link and placeholder 'Logo'/'Random Dude' copy on the auth pages.
  • · Guard `emailAddresses[0]` with optional chaining in user-avatar-profile.tsx and user-nav.tsx.
  • · Add `NEXT_PUBLIC_APP_URL` to env.example.txt so OG/Twitter images resolve to absolute URLs.
  • · Commit bun.lock so Docker `--frozen-lockfile` builds are reproducible.

What's already good

  • · Dashboard segment is gated server-side with `auth.protect()` and Clerk middleware is wired for every request, so the UI itself is not reachable anonymously.
  • · No secrets in the repo; all keys come from env, and .gitignore/.dockerignore exclude .env files and .clerk.
  • · Sort params are validated with Zod and column-id allowlists (src/lib/parsers.ts) before reaching the data layer, and JSON.parse is wrapped in try/catch.
  • · Clean service-layer separation (types → service → queries → mutations) with query-key factories makes the backend swap localized to two files.
  • · Error boundaries exist per overview slot and globally, all reporting to Sentry; auth and dashboard routes are noindex.
  • · Docker images run as a non-root user with standalone output; bunfig minimumReleaseAge adds supply-chain hardening.
  • · Forms use shared Zod schemas and accessible field components (aria-invalid, aria-describedby) throughout.

Do this first

  1. Protect and validate the API: add route matching in src/proxy.ts, `auth()` checks and Zod body/pagination validation in all /api route handlers (F1, F2, F4).
  2. Replace the in-memory faker store with a real, org-scoped data layer in the two service.ts files, and remove all demo delays (F3, F9, F12).
  3. Fix the observability configuration: Sentry env-flag comparison, sendDefaultPii, sampling (F7, F8).
  4. Move plan/org entitlement checks to the server for exclusive and billing pages (F5, F6).
  5. Fix the product edit flow (image requirement, category options, badge icon) and the email-address crash (F11, F10).
  6. Clean up launch-visible embarrassments: auth page link/copy, text-filter hydration bug, commit the lockfile (F13, F14, F15).
VibeAudit badge
Add the badge to your README
[![VibeAudit](https://vibeaudit.sh/api/badge/fx09shadcndash)](https://vibeaudit.sh/a/fx09shadcndash)

Fixed things? Re-audit.

Run a new scan on the updated repo. Use a 5-pack key or pay per audit.

Public repo URL, no signup. Private repo? Sign in with GitHub and pick it — read-only, nothing stored except the report.