Where the trust boundary is
Everything that reaches the browser is public: static files, client components and any variable whose name carries the framework's public prefix. In Next.js that prefix is NEXT_PUBLIC_, and Next.js inlines those values into client code. Everything else runs on the server side of Vercel, where environment variables live.
Vercel stores environment variables encrypted at rest but they are visible to anyone with access to the project. That makes team membership part of your trust boundary. For general secret-handling ideas see our environment variables guide. Below is what to verify on Vercel itself.
The checklist
| Area | What to verify | How to test on your own project | Pass condition |
|---|---|---|---|
| Environment variables | Secrets are server-only, not prefixed NEXT_PUBLIC_, and scoped to the environments that need them | Run vercel env ls; search the built output for secret prefixes | No secret in client bundles; production keys absent from Preview |
| Sensitive flag | Production and Preview secrets are marked Sensitive | Check the Sensitive tag in project settings; consider the team enforcement policy | Values cannot be read back after creation |
| Preview deployments | Deployment Protection covers preview URLs | Open a preview URL in a private window | Redirected to login or password, not the app |
| Headers | Security headers set through vercel.json or framework config | curl -I your production URL | nosniff, frame protection and a CSP present |
| Server actions | Each action re-checks authentication and ownership | Read every file marked use server | No action relies on the page having checked |
| Route handlers | Each route.ts authenticates and validates input | Call each route with no cookie and with another user's cookie | 401 or 403 in both cases |
| Logs | No tokens, personal data or full request bodies logged | Read log output for a sample request | Nothing sensitive appears |
Step 1: Environment variables and scopes
Each variable can apply to Production, Preview, Development and custom environments. Use the narrowest set. A production database key in Preview means every branch deploy, and everyone who can push a branch, touches production data. Changes only affect new deployments, so redeploy after editing.
Mark secrets Sensitive. Vercel stores those in an unreadable format and redacts them from build logs when they are long enough, though only for Production and Preview. Owners can enforce this for all new variables in team security settings.
vercel env ls
vercel env pull .env.check
grep -rnE "sk_live_|sb_secret_|service_role|BEGIN PRIVATE KEY" .next/staticStep 2: Preview deployments
Deployment Protection controls who can open your URLs. Standard Protection covers everything except production domains and is the documented recommendation for most projects. Vercel Authentication limits access to Vercel users with access, while Password Protection and Trusted IPs are paid options on some plans. Choose All Deployments if the whole app is private.
After enabling Standard Protection, generated production deployment URLs become restricted, so server code that calls itself through VERCEL_URL may need to use the requested origin instead.
Step 3: Headers
Add headers in vercel.json or your framework config. Start Content-Security-Policy in report-only mode, as Vercel's guidance suggests, then enforce once nothing breaks, and avoid unsafe-inline and unsafe-eval where you can.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Content-Security-Policy-Report-Only", "value": "default-src 'self'" }
]
}
]
}Step 4: Server actions and route handlers
Next.js documents that an exported server action is reachable by a direct POST request, not only through your UI. A check on the page does not cover the action, so authenticate inside it, then check that the user owns the record. Return only the fields the client needs. The same documentation says route.ts files deserve traditional auditing.
Put data access in a server-only module that performs authorization and returns minimal objects, and validate every argument.
"use server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
export async function deletePost(postId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
const post = await db.post.findUnique({ where: { id: postId } });
if (!post || post.authorId !== session.user.id) throw new Error("Forbidden");
await db.post.delete({ where: { id: postId } });
}Step 5: Check the live site
Confirm headers on production, and confirm a route returns an error without a session. Run these against your own deployment only.
curl -sI https://your-app.example.com | grep -iE "content-security|x-frame|x-content-type|referrer"
curl -i https://your-app.example.com/api/your-routeStep 6: Logs
Function logs are visible to teammates and often flow to third-party tools. Log identifiers and outcomes, not tokens, cookies, request bodies or health data. Search recent logs for the prefixes of your provider keys, and rotate anything you find.
Common mistakes
- Naming a secret NEXT_PUBLIC_ because the client needed it, which publishes it.
- Sharing one production database key across Production and Preview.
- Assuming a page redirect protects the server action defined on it.
- Leaving preview URLs public while they point at real data.
- Setting headers only in development config that never reaches production.
- Passing whole database rows to client components.
Keep it working
After each dependency update or AI-generated change, re-run vercel env ls, the header check and the no-session route calls. Review any new use server file for the two checks above.
Frequently asked questions
Are Vercel environment variables safe for secrets?
Yes on the server side. They are encrypted at rest but readable by people with project access unless marked Sensitive. Never expose them through a public-prefixed name, and rotate any secret that appeared in client code or logs.
Do preview deployments need protection?
If they use real data, real keys or unreleased features, yes. Enable Deployment Protection so only intended people can open them, and give previews their own test credentials.
Is a server action private because I never call it from the client?
No. Next.js states that exported actions are reachable by direct POST. Unused ones are stripped from the client bundle, but you should still verify authentication and ownership inside every action.
Where do I set a Content Security Policy?
In vercel.json headers or your framework config. Start with the report-only header, review violations, then switch to enforcement. Use nonces or hashes rather than unsafe-inline.