Framework Guide
Next.js + Vercel Secrets Management
Next.js blurs the line between server and client. A single NEXT_PUBLIC_ prefix decides whether a secret ships to every browser or stays on the server. This guide shows you how to manage that boundary securely — from local development to production on Vercel.
Why Next.js Secrets Are Tricky
Next.js has three distinct runtime contexts, and each handles environment variables differently:
| Context | Has access to | Risk |
|---|---|---|
| Server Components / API Routes | All env vars via process.env | Safe — server-only, never sent to the browser |
| Client Components | Only NEXT_PUBLIC_* vars | Exposed — inlined into the JavaScript bundle |
| Build time (next build) | All env vars at build time | NEXT_PUBLIC_* values are baked into static HTML/JS |
The common mistakes: putting a database URL in a NEXT_PUBLIC_ variable, sharing a single .env.local across the entire team via Slack, or forgetting that Vercel preview deploys use production secrets by default. secr solves all three.
Step 1: Set Up Your Project
Install the CLI and initialize secr in your Next.js project:
npm i -g @secr/cli
secr login
secr initThis creates a .secr.json config linking your repo to a secr project. Three default environments are created: development, staging, and production.
Step 2: Import Your Existing .env
If you already have a .env.local file, import it in one command:
# Import all vars into the development environment
secr set --from-env .env.local --env development
# Import production values separately
secr set --from-env .env.production --env productionBoth NEXT_PUBLIC_ prefixed and server-only variables are stored together — secr handles them identically since the prefix is part of the key name. Next.js decides what gets exposed based on the prefix at build time.
Step 3: Local Development
Replace your .env.local workflow with zero-disk injection:
# Inject secrets and start Next.js dev server
secr run -- next dev
# Or with a custom port
secr run -- next dev -p 4000secr run pulls secrets from the development environment and injects them into the process — no .env.local file written to disk. Both NEXT_PUBLIC_* and server-only variables are available.
Tip: Add a script to your package.json so the whole team uses the same command:
{
"scripts": {
"dev": "secr run -- next dev",
"dev:staging": "secr run --env staging -- next dev"
}
}Step 4: Server Components & API Routes
In the App Router, Server Components and Route Handlers run on the server. Access secrets via process.env as usual — secr injects them before the process starts:
import { NextResponse } from "next/server";
export async function GET() {
// Server-only — never exposed to the client
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.STRIPE_SECRET_KEY;
// ... use secrets safely
return NextResponse.json({ ok: true });
}// Server Component (default in App Router)
export default async function DashboardPage() {
// Safe: this runs on the server
const data = await fetch("https://api.example.com", {
headers: { Authorization: `Bearer ${process.env.API_SECRET}` },
});
return <div>{/* render server data */}</div>;
}Rule of thumb: Only use the NEXT_PUBLIC_ prefix for values that are truly public — analytics IDs, feature flags, or API base URLs. Database credentials, API keys, and signing secrets should never have the prefix.
Step 5: Deploy to Vercel
The @secr/vercel package pulls secrets before next build runs, writing them to .env.local where Next.js picks them up automatically.
1. Install the integration
npm install @secr/vercel2. Create a deploy token
secr token create --name "vercel-deploy"3. Add environment variables in Vercel
In your Vercel project: Settings → Environment Variables
| Variable | Value | Scope |
|---|---|---|
| SECR_TOKEN | secr_tok_... | All Environments |
| SECR_ORG | my-org | All Environments |
| SECR_PROJECT | my-nextjs-app | All Environments |
4. Override the build command
In Settings → General → Build & Development Settings:
npx secr-vercel && next buildThat's it. On every deploy, secr pulls the correct secrets for the environment and writes them to .env.local before Next.js builds.
Preview Environments
Vercel creates preview deployments for every pull request. By default, @secr/vercel maps Vercel's VERCEL_ENV to a matching secr environment:
| Vercel Deploy | VERCEL_ENV | secr Environment |
|---|---|---|
| Production (main branch) | production | production |
| Preview (pull requests) | preview | preview |
| Development (local) | development | development |
To map preview deploys to your staging environment instead, set SECR_ENVIRONMENT=staging scoped to the Preview environment in Vercel's settings.
CI: Run Tests with Secrets
Use the secr GitHub Action to inject secrets into your CI pipeline before running tests or linting:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Pull secrets
uses: secr-dev/secr-action@v1
with:
token: ${{ secrets.SECR_TOKEN }}
org: my-org
project: my-nextjs-app
environment: staging
- run: npm ci
- run: npm test
- run: npm run buildSecrets are exported as environment variables for the duration of the job. Values are automatically masked in GitHub Actions logs.
Scan for Leaked Secrets
Next.js projects tend to accumulate .env.local, .env.production, and .env.development.local files. Even if gitignored, they sit on disk unencrypted. Scan your project to check:
# Scan the whole project
secr scan
# Install a pre-commit hook to block future leaks
secr guard installThe scanner detects 20+ secret patterns — AWS keys, Stripe keys, database URLs, OpenAI tokens, and more. Once you're using secr run for local dev and @secr/vercel for builds, you can delete those .env.* files entirely.
Complete Workflow
Putting it all together, here's the full Next.js + Vercel workflow with secr:
Local dev
secr run -- next dev injects secrets without writing .env files. Every developer pulls the same values.
Vercel builds
npx secr-vercel writes .env.local before next build. Environment auto-detected from VERCEL_ENV.
CI pipeline
The secr GitHub Action exports secrets as env vars for tests. Masked in logs automatically.
Secret scanning
secr guard blocks commits containing secrets. secr scan audits the codebase on demand.
Common Next.js Patterns
Middleware with secrets
Next.js Middleware runs at the edge. Secrets are available via process.env since they're set at build time:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const apiKey = process.env.INTERNAL_API_KEY;
// Validate incoming requests against your API key
// ...
return NextResponse.next();
}Separate public and private vars
Use secr's secret descriptions to document which variables are client-exposed:
# Public: safe to expose in the browser
secr set NEXT_PUBLIC_POSTHOG_KEY "phc_abc123" \
--description "PostHog analytics key (client-safe)"
# Private: server-only
secr set DATABASE_URL "postgresql://..." \
--description "Production database (server-only, never prefix with NEXT_PUBLIC_)"Templates for required variables
Define the secrets every environment must have. Catch missing variables before they cause runtime errors:
secr template add DATABASE_URL
secr template add NEXT_PUBLIC_APP_URL
secr template add STRIPE_SECRET_KEY
secr template add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
# Validate before deploying
secr template validate --env productionShip Next.js apps without .env files
npm i -g @secr/cli
secr init
secr run -- next dev