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:

ContextHas access toRisk
Server Components / API RoutesAll env vars via process.envSafe — server-only, never sent to the browser
Client ComponentsOnly NEXT_PUBLIC_* varsExposed — inlined into the JavaScript bundle
Build time (next build)All env vars at build timeNEXT_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:

Terminal
npm i -g @secr/cli
secr login
secr init

This 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:

Terminal
# 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 production

Both 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:

Terminal
# Inject secrets and start Next.js dev server
secr run -- next dev

# Or with a custom port
secr run -- next dev -p 4000

secr 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:

package.json
{
  "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:

app/api/users/route.ts
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 });
}
app/dashboard/page.tsx
// 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/vercel

2. Create a deploy token

secr token create --name "vercel-deploy"

3. Add environment variables in Vercel

In your Vercel project: Settings → Environment Variables

VariableValueScope
SECR_TOKENsecr_tok_...All Environments
SECR_ORGmy-orgAll Environments
SECR_PROJECTmy-nextjs-appAll Environments

4. Override the build command

In Settings → General → Build & Development Settings:

Build Command
npx secr-vercel && next build

That'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 DeployVERCEL_ENVsecr Environment
Production (main branch)productionproduction
Preview (pull requests)previewpreview
Development (local)developmentdevelopment

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:

.github/workflows/ci.yml
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 build

Secrets 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:

Terminal
# Scan the whole project
secr scan

# Install a pre-commit hook to block future leaks
secr guard install

The 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:

middleware.ts
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:

Terminal
# 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:

Terminal
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 production

Ship Next.js apps without .env files

npm i -g @secr/cli

secr init

secr run -- next dev