What v0 typically generates and where the trust boundary is
v0's documentation describes turning prompts into interfaces you can deploy with one click to Vercel infrastructure. In practice the output is commonly a Next.js App Router project with React components and styling, sometimes with route handlers, server actions and database or authentication integrations added later. Check what you actually have: look for an app directory, files named route.ts, functions marked with use server, and any middleware file.
The trust boundary in Next.js is the line between server and client. The Next.js documentation states that environment variables are available only on the server unless they are prefixed with NEXT_PUBLIC_, and that prefixed values are inlined into the JavaScript sent to the browser at build time. It also notes those values are frozen at build. So a component is not a security control, a client-side check is not a security control, and any value with the public prefix is published.
The second boundary is between a request and your data. A route handler or server action is a public entry point that anyone can call directly with any body, regardless of what your form sends. Each one must authenticate the caller, authorize the specific record, and validate the input. A polished interface generated in one prompt tells you nothing about whether that happens.
The review checklist
| Area | What to verify | How to test on your own project | Pass condition |
|---|---|---|---|
| Public variables | Nothing secret uses the NEXT_PUBLIC_ prefix. | List every NEXT_PUBLIC_ name with the grep command and read each value's purpose. | Each public variable is safe to show to any visitor. |
| Secrets in the build | No secret string appears in the client build output. | Build and grep the .next static directory for secret patterns. | No matches. |
| Route handlers | Each handler checks the session and the caller's right to the record. | Call each route with curl using no session, then another user's session. | No session gives 401 and the wrong user gives 403 or 404. |
| Server actions | Each action re-checks authorization and validates its input on the server. | Read each use server function and trace the first line that checks the user. | Every mutating action checks identity before doing any work. |
| Identifiers | User ids come from the session, not from the request body or query. | Search handlers for reads of ids from the body and confirm the source. | No handler trusts a client-supplied owner id. |
| Data layer rules | If a hosted database is used, its own row rules are on. | Follow the database vendor's checklist for row-level access. | Direct calls with the public key cannot read other users' data. |
| Deployment environments | Preview and production hold separate secrets and separate data. | Compare environment variable names across environments in the hosting dashboard. | Production secrets are not reused in previews. |
| Dependencies | Installed packages are known, current and needed. | Run the audit command and review any package you did not choose. | No unaddressed high-severity advisories and no unexplained packages. |
Commands and queries to run
Run these from the root of your own repository. They find public variables, secret-shaped strings in the built client files, direct reads of user identifiers from request bodies, and vulnerable dependencies.
grep -rnE "NEXT_PUBLIC_[A-Z0-9_]+" --include="*.ts" --include="*.tsx" --include=".env*" .
npm run build
grep -rEl "sk_live_|service_role|sb_secret_|BEGIN PRIVATE KEY" .next/static
grep -rnE "(body|searchParams)[^;]*(userId|user_id|ownerId)" app
npm audit --omit=dev
git log --all -p -S"sk_live_" | head -50Test each route as an outsider
Use your own deployed or local address. For every route that reads or changes data, send the request without a session, then with a valid session that belongs to a different user than the record.
The first call should be rejected as unauthenticated. The second should be refused or return nothing, because the record is not user B's. The third must ignore or reject the supplied userId and create the record, if at all, under user B. Adjust paths to your own routes.
export BASE="http://localhost:3000"
curl -i "$BASE/api/orders/ORDER_ID_OWNED_BY_USER_A"
curl -i "$BASE/api/orders/ORDER_ID_OWNED_BY_USER_A" \
-H "Cookie: YOUR_USER_B_SESSION_COOKIE"
curl -i -X POST "$BASE/api/orders" \
-H "Content-Type: application/json" \
-H "Cookie: YOUR_USER_B_SESSION_COOKIE" \
-d '{"userId":"USER_A_ID","total":1}'Server actions, middleware and the client
Two patterns cause trouble in generated Next.js code. First, authorization implemented only in middleware or in a layout. Middleware is a useful gate, but each handler and action should still check for itself, since a route can be reached in ways you did not anticipate. Second, sensitive work placed in a client component because it was convenient, such as building a database query in the browser with a key from a public variable.
Also check what the client receives. A server component that passes a whole database record to a client component sends every field it contains, including columns the interface never displays. Select only the fields the screen needs. For forms, treat the browser-side validation as usability, and repeat it on the server with a schema.
Common mistakes with this workflow
- Adding NEXT_PUBLIC_ to a variable to make an error disappear. That publishes it in the bundle.
- Assuming a page hidden behind a login redirect protects the API route it calls.
- Trusting an id in the request body. Read identity from the session.
- Sharing one set of secrets between preview and production deployments.
- Accepting whatever packages the assistant added without reading what they do.
- Committing a local environment file. The create-next-app template ignores env files by default, so confirm yours still does.
- Verifying only in the interface. Always repeat the check with curl.
Keep it working
Repeat the greps, the build search and the route tests after any change that adds a route, an action or a variable. Run the audit command on a schedule. Pass condition: every data-changing entry point rejects unauthenticated and wrong-user calls, and no secret appears in client files.
Frequently asked questions
Are NEXT_PUBLIC_ variables safe?
Only for values you are happy to publish. Next.js inlines them into the browser JavaScript at build time. An analytics id or a public API base URL is fine. A database password, secret API key or token is not, and needs a server-only variable.
Does deploying on the vendor's platform make the app secure?
Hosting can provide transport security and isolation, but your application logic is still yours. Authorization, input validation and data rules are written in your code. Check the current hosting documentation for exactly what the platform covers.
Do I need to check code that v0 generated for the interface only?
Look mainly at what handles data: route handlers, server actions, middleware and anything reading environment variables. Pure presentational components carry less risk, but confirm they do not receive more fields than they display.
How do I check what the browser actually receives?
Build the project and search the static output for secret patterns, then open your browser's network tab and read API responses on key screens. Compare returned fields with what the screen displays and remove extras server-side.