What a v0 app ships with
- A Next.js App Router project, usually deployed to Vercel from GitHub.
- NEXT_PUBLIC_ environment variables inlined into the browser bundle; everything else stays on the server.
- Route handlers under /app/api that any website can call unless they say otherwise.
- Vercel hosting with sensible defaults but no security headers until next.config.js asks for them.
The five things that go wrong
- A secret with a NEXT_PUBLIC_ prefix. NEXT_PUBLIC_ is the switch that copies a value into the browser. A database URL, a service-role key, or an OpenAI key with that prefix is public the moment the build runs.
- Route handlers that echo any Origin. Copied CORS code that returns the request's Origin with credentials allowed lets any site call your API as the signed-in visitor.
- No security headers. Vercel does not add Content-Security-Policy, HSTS, or frame protection on its own; a headers() block in next.config.js does.
- Supabase or Neon straight from a client component. A query in a "use client" file runs in the browser with whatever key it was given. Row Level Security has to cover it, or the query has to move to a server component or route handler.
- Source maps uploaded by an error-tracking plugin. Sentry and friends upload maps for readable stack traces; the wrong setting also serves them to everyone.
How to fix them
Send security headers from next.config.js
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};Keep the key in a route handler, not the page
// app/api/ask/route.ts (OPENAI_API_KEY without NEXT_PUBLIC_ stays on the server)
export async function POST(req: Request) {
const r = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" },
body: await req.text(),
});
return new Response(await r.text(), { status: r.status, headers: { "Content-Type": "application/json" } });
}Answer CORS from a fixed list
const allowed = new Set(["https://your-app.com"]);
export async function GET(req: Request) {
const origin = req.headers.get("origin") || "";
const headers = new Headers({ "Content-Type": "application/json" });
if (allowed.has(origin)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
}
return new Response(JSON.stringify({ ok: true }), { headers });
}
Every finding in a scan comes with the exact lines for your host and framework, opened for the one it detected.
Questions people ask
- Which variables are public in Next.js?
- Only the ones starting with NEXT_PUBLIC_. Everything else is readable only in server components, route handlers, and server actions. The scan reports NEXT_PUBLIC_ names that sound like secrets, names only.
- Does the scanner see my server components?
- No, and neither does anyone else. It sees the HTML they render and the JavaScript the browser loads, which is where leaks live.
- Vercel already gives me HTTPS. What is missing?
- The certificate, yes. The headers that tell browsers to insist on it, to refuse framing, and to limit where scripts load from are yours to add.
Check your v0 app now
Free, read-only, ten seconds. Then, if you want it watched: weekly re-scans, uptime and error alerts, code checks on the repo.
Scan your app free