Configuration and domains
Keep secrets server-side
Store sensitive configuration in Vercel while preventing framework public prefixes, logs, build output, and client responses from exposing it.
Vercel encrypts environment variables at rest. That protects them in Vercel’s storage. It does nothing once your own code sends the value to the browser or writes it to a log. Keeping a secret secret is a data-flow problem, and it’s yours.
The public prefix
In Next.js, any variable whose name starts with NEXT_PUBLIC_ is inlined into the client bundle at build time. That’s on purpose. It exists for values the browser must know, like an analytics site id.
So the first rule is naming. NEXT_PUBLIC_API_KEY is not a secret, whatever the value is. A real secret is RESEND_API_KEY, no prefix, read only in server code.
Read secrets only on the server
Server components, route handlers and server actions can read process.env.RESEND_API_KEY. Client components cannot, and should not try. The safe pattern is to use the credential on the server and return the result of using it, never the credential itself:
// app/api/newsletter/route.ts
export async function POST(request: Request) {
const { email } = await request.json()
const res = await fetch('https://api.resend.com/contacts', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` },
body: JSON.stringify({ email })
})
return Response.json({ ok: res.ok })
}
The browser gets {"ok":true}. It never sees the key.
The leaks that don’t look like leaks
The server-only rule is broken in ways that look innocent. A server component that passes a whole config object as a prop to a client component serializes it into the HTML. An error message that includes the request headers prints the Authorization header. A console.log(process.env) while debugging ends up in the build or runtime log, where every team member can read it.
Never log headers, tokens, request bodies or the environment object. Log a boolean or a length if you need proof.
When a secret leaks
If a value shows up in a log or a bundle, rotate it. Create a new credential at the provider, update Vercel, redeploy, and then revoke the old one. Rotation is complete only after the revoke.
Use a different credential per environment, with the smallest permissions that environment needs. And think about rollback: an older deployment may still expect the credential you just revoked. That’s a reason to fix forward, not to restore it.
Try this on your own project: add a fake secret to Preview, read it in a server route that returns only {"configured":true}, then search the page source, the network responses and the logs for the value. You should find nothing.
Lesson completed