guias·12 min de lectura

Variables de entorno en NextJS y Vercel: guía segura

guía completa sobre variables de entorno en NextJS y Vercel. Aprende a configurar .env, NEXT_PUBLIC_, validación con Zod y mejores prácticas de seguridad para proteger tus secrets.

Variables de entorno en NextJS y Vercel: guía segura

Las variables de entorno en NextJS y Vercel son el mecanismo para configurar tu aplicación sin hardcodear valores que cambian entre entornos. Claves de API, URLs de bases de datos, tokens de autenticación: todo lo que tu aplicación necesita para funcionar pero que no debe vivir en el código fuente.

El problema es que un error en la configuración puede exponer credenciales sensibles en el navegador del usuario, o hacer que tu aplicación falle en producción porque una variable que existe en tu .env.local no esta configurada en Vercel. Esta guía cubre como evitar ambos escenarios.

por qué las variables de entorno son críticas

Consideremos un ejemplo concreto. Tu aplicación usa Stripe para procesar pagos:

typescript
// Si hardcodeas la clave en el código:
const stripe = new Stripe('sk_live_abc123xyz789...')
// Problema: cualquiera con acceso al repo ve tu clave
// Problema: no puedes cambiarla sin modificar código
// Problema: la misma clave se usa en desarrollo y producción
typescript
// Con variables de entorno:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// La clave vive fuera del código
// Puedes tener valores diferentes por entorno
// Nadie la ve en el repositorio

Las variables de entorno resuelven tres problemas fundamentales:

  1. Seguridad: Las credenciales no viven en el código fuente
  2. Flexibilidad: Valores diferentes para desarrollo, staging y producción
  3. Portabilidad: El mismo código funciona en cualquier entorno

Archivos .env en NextJS: cuál usar y cuando

NextJS soporta múltiples archivos .env, cada uno con un propósito específico. El orden de prioridad determina cuál sobreescribe a cuál.

Orden de carga y prioridad

plaintext
Prioridad alta (sobreescribe a los demas)
  ↓  .env.local
  ↓  .env.development.local  (solo en npm run dev)
  ↓  .env.production.local   (solo en npm run build / npm run start)
  ↓  .env.development        (solo en npm run dev)
  ↓  .env.production         (solo en npm run build / npm run start)
  ↓  .env
Prioridad baja (base)

.env -- El archivo base

bash
# .env
# Valores por defecto que aplican a todos los entornos
# PUEDE subirse a git (no debe contener secretos)
 
NEXT_PUBLIC_SITE_NAME=Mi Sitio
NEXT_PUBLIC_DEFAULT_LOCALE=es
NODE_ENV=development

Subir a git: Si, pero solo con valores no sensibles. Sirve como documentación de que variables necesita la aplicación.

.env.local -- Tus valores reales

bash
# .env.local
# Valores reales para tu entorno de desarrollo
# NUNCA subir a git
 
DATABASE_URL=postgresql://localhost:5432/mi-db
STRIPE_SECRET_KEY=sk_test_abc123...
RESEND_API_KEY=re_test_xyz789...
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Subir a git: Nunca. NextJS ya incluye .env.local en el .gitignore por defecto. Verifica que tu .gitignore lo tenga:

bash
# .gitignore
.env*.local

.env.production -- Valores para producción

bash
# .env.production
# Valores especificos para el build de producción
# Puede subirse a git si no tiene secretos
 
NEXT_PUBLIC_SITE_URL=https://tudominio.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

Subir a git: Depende. Si solo tiene variables NEXT_PUBLIC_ sin información sensible, puede subirse. Si tiene secretos, no.

.env.example -- Template para tu equipo

bash
# .env.example
# Template para que otros desarrolladores sepan que variables necesitan
# DEBE subirse a git
 
DATABASE_URL=postgresql://localhost:5432/mi-db
STRIPE_SECRET_KEY=sk_test_... # Obtener en dashboard.stripe.com
RESEND_API_KEY=re_...         # Obtener en resend.com/api-keys
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Este archivo no es leido por NextJS. Es documentación para tu equipo.

Buena práctica

Siempre incluye un .env.example en tu repositorio. Cuando alguien clona el proyecto, copia este archivo como .env.local y rellena los valores reales.

Resumen visual

ArchivoContiene secretosSubir a gitCuando se carga
.envNoSiSiempre
.env.localSiNoSiempre (sobreescribe .env)
.env.developmentNoSiSolo npm run dev
.env.productionNo (idealmente)DependeSolo npm run build
.env.exampleNo (template)SiNunca (es para humanos)

NEXT_PUBLIC_: que expone y que no

Esta es la decision más importante que vas a tomar con variables de entorno en NextJS. El prefijo NEXT_PUBLIC_ determina si una variable es accesible en el navegador del usuario.

Variables SIN el prefijo (privadas)

typescript
// Solo accesibles en el servidor
process.env.DATABASE_URL        // Server Components, API routes, middleware
process.env.STRIPE_SECRET_KEY   // Server Components, API routes, middleware
process.env.RESEND_API_KEY      // Server Components, API routes, middleware

Estas variables nunca llegan al bundle de JavaScript del cliente. Si intentas accederlas desde un Client Component, obtienes undefined.

Variables CON el prefijo (publicas)

typescript
// Accesibles en servidor Y en el navegador
process.env.NEXT_PUBLIC_SITE_URL    // Disponible en todos lados
process.env.NEXT_PUBLIC_GA_ID       // Disponible en todos lados
process.env.NEXT_PUBLIC_STRIPE_PK   // Disponible en todos lados

Estas variables se incrustan en el bundle de JavaScript durante el build. Cualquier persona puede verlas abriendo DevTools > Sources en el navegador.

El error más peligroso

typescript
// NUNCA hagas esto:
NEXT_PUBLIC_DATABASE_URL=postgresql://user:password@host/db
NEXT_PUBLIC_STRIPE_SECRET=sk_live_abc123...
NEXT_PUBLIC_JWT_SECRET=mi-secreto-super-seguro
 
// Cualquier usuario puede ver estos valores en el navegador
// Un atacante puede conectarse a tu base de datos directamente
// Tu clave de Stripe le permite hacer cobros a nombre tuyo
Regla de oro

Si una variable contiene una credencial, token, password o clave secreta, NUNCA le pongas el prefijo NEXT_PUBLIC_. La única excepción son las claves publicas diseñadas para estar en el frontend, como la publishable key de Stripe (pk_).

guía rápida: que lleva NEXT_PUBLIC_ y que no

VariableNEXT_PUBLIC_Razon
DATABASE_URLNoCredencial de base de datos
STRIPE_SECRET_KEYNoPermite cobrar dinero
RESEND_API_KEYNoPermite enviar emails
JWT_SECRETNoFirma tokens de autenticación
SITE_URLSiInformación pública
GA_MEASUREMENT_IDSiGoogle lo requiere en el frontend
STRIPE_PUBLISHABLE_KEYSiDiseñado para estar en el frontend
SENTRY_DSNSiEl DSN es público por diseño

Acceder a variables en Server Components vs Client Components

NextJS 16 con App Router distingue claramente entre código que corre en el servidor y código que corre en el cliente. Esto afecta directamente como accedes a las variables de entorno. Si necesitas repasar la diferencia, revisa la guía de Server Components vs Client Components.

Server Components (por defecto)

En Server Components, todas las variables de entorno estan disponibles:

typescript
// app/page.tsx (Server Component por defecto)
export default async function HomePage() {
  // Ambas funcionan en el servidor
  const siteUrl = process.env.NEXT_PUBLIC_SITE_URL
  const dbUrl = process.env.DATABASE_URL
 
  const data = await fetch(`${process.env.API_BASE_URL}/posts`)
  const posts = await data.json()
 
  return (
    <div>
      <h1>Bienvenido</h1>
      <p>Sitio: {siteUrl}</p>
      {/* dbUrl NUNCA se envia al cliente */}
    </div>
  )
}

Client Components

En Client Components, solo las variables con NEXT_PUBLIC_ estan disponibles:

typescript
'use client'
 
export function Analytics() {
  // Funciona: tiene NEXT_PUBLIC_
  const gaId = process.env.NEXT_PUBLIC_GA_ID
 
  // undefined: NO tiene NEXT_PUBLIC_
  const dbUrl = process.env.DATABASE_URL // undefined
 
  return <script data-ga-id={gaId} />
}

API Routes y Route Handlers

En el servidor, todas las variables estan disponibles:

typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server'
 
export async function GET() {
  // Todas las variables disponibles
  const dbUrl = process.env.DATABASE_URL
  const apiKey = process.env.EXTERNAL_API_KEY
 
  // Usar para conectar a servicios
  const users = await fetchUsersFromDB(dbUrl!)
 
  return NextResponse.json(users)
}

Middleware

El middleware también corre en el servidor:

typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const apiKey = process.env.AUTH_API_KEY // Disponible
 
  // Verificar token, redirigir, etc.
  return NextResponse.next()
}

Patron para pasar datos del servidor al cliente

Cuando un Client Component necesita datos que dependen de una variable privada, obten los datos en el servidor y pasalos como props:

typescript
// app/dashboard/page.tsx (Server Component)
export default async function DashboardPage() {
  // Obtener datos usando variable privada en el servidor
  const response = await fetch(`${process.env.API_BASE_URL}/stats`, {
    headers: {
      Authorization: `Bearer ${process.env.API_SECRET_TOKEN}`,
    },
  })
  const stats = await response.json()
 
  // Pasar solo los datos (no la variable) al Client Component
  return <DashboardChart data={stats} />
}
typescript
// components/DashboardChart.tsx (Client Component)
'use client'
 
interface DashboardChartProps {
  data: StatsData
}
 
export function DashboardChart({ data }: DashboardChartProps) {
  // Recibe los datos, no las credenciales
  return <div>{/* renderizar chart con data */}</div>
}
Principio de mínimo privilegio

Nunca pases más información de la necesaria al cliente. El Server Component obtiene los datos usando credenciales privadas y envia al cliente solo el resultado.

Configurar variables en Vercel

Cuando haces deploy de tu proyecto en Vercel, las variables de .env.local no existen en el servidor. Necesitas configurarlas en el dashboard de Vercel.

Agregar variables desde el dashboard

1

Ve a tu proyecto en Vercel > Settings > Environment Variables

aquí puedes agregar, editar y eliminar variables.

2

Agrega cada variable con su valor

Escribe el nombre (Key) y el valor (Value) de cada variable.

3

Selecciona los entornos

Para cada variable, elige donde debe estar disponible:

  • Production: el deploy de tu rama principal
  • Preview: los deployments de pull requests
  • Development: para usar con vercel dev en local
4

Haz redeploy para que las variables tomen efecto

Las variables nuevas no aplican retroactivamente. Necesitas un nuevo deploy.

Agregar variables desde el CLI

Si prefieres la terminal:

bash
# Instalar Vercel CLI
npm install -g vercel
 
# Vincular tu proyecto
vercel link
 
# Agregar variable para producción
vercel env add DATABASE_URL production
# Te pedira el valor interactivamente
 
# Agregar variable para todos los entornos
vercel env add NEXT_PUBLIC_SITE_URL production preview development
 
# Listar variables configuradas
vercel env ls
 
# Descargar variables a .env.local (para desarrollo)
vercel env pull .env.local
vercel env pull

El comando vercel env pull es muy útil cuando empiezas en un proyecto existente. Descarga todas las variables de entorno de development a tu .env.local automáticamente.

Valores diferentes por entorno

Es común necesitar valores distintos para producción y preview:

bash
# Production
DATABASE_URL=postgresql://prod-host:5432/prod-db
NEXT_PUBLIC_SITE_URL=https://tudominio.com
STRIPE_SECRET_KEY=sk_live_...
 
# Preview
DATABASE_URL=postgresql://staging-host:5432/staging-db
NEXT_PUBLIC_SITE_URL=https://staging.tudominio.com
STRIPE_SECRET_KEY=sk_test_...

Esto te permite probar cambios en preview sin afectar la base de datos de producción.

Variables sensibles en Vercel

Vercel trata las variables de entorno como sensibles por defecto:

  • Los valores se encriptan en reposo
  • Solo son accesibles durante el build y runtime
  • No aparecen en los logs de build (a menos que tu código las imprima)
  • Los miembros del equipo con rol Viewer no pueden ver los valores
typescript
// CUIDADO: esto expone la variable en los build logs
console.log('DB URL:', process.env.DATABASE_URL) // No hagas esto
 
// Mejor: solo loguea que la variable existe
console.log('DB URL configurada:', !!process.env.DATABASE_URL) // true/false

Validar variables de entorno con Zod

El peor momento para descubrir que falta una variable de entorno es cuando un usuario reporta un error en producción. La solución es validar todas las variables al inicio de la aplicación.

Patron básico con Zod

Si no conoces Zod, revisa la guía completa de Zod para validación. aquí lo aplicamos a variables de entorno:

typescript
// lib/env.ts
import { z } from 'zod'
 
const envSchema = z.object({
  // Variables del servidor (privadas)
  DATABASE_URL: z.string().url('DATABASE_URL debe ser una URL válida'),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_', 'STRIPE_SECRET_KEY debe empezar con sk_'),
  RESEND_API_KEY: z.string().min(1, 'RESEND_API_KEY es requerida'),
  REVALIDATION_SECRET: z.string().min(16, 'REVALIDATION_SECRET debe tener al menos 16 caracteres'),
 
  // Variables publicas
  NEXT_PUBLIC_SITE_URL: z.string().url('NEXT_PUBLIC_SITE_URL debe ser una URL válida'),
  NEXT_PUBLIC_GA_ID: z.string().optional(),
  NEXT_PUBLIC_STRIPE_PK: z.string().startsWith('pk_', 'NEXT_PUBLIC_STRIPE_PK debe empezar con pk_'),
 
  // Variables del sistema
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
})
 
// Validar al cargar el módulo
export const env = envSchema.parse(process.env)
 
// Tipo inferido automáticamente
export type Env = z.infer<typeof envSchema>

Usar las variables validadas

typescript
// En cualquier Server Component o API route:
import { env } from '@/lib/env'
 
export default async function Page() {
  // TypeScript sabe que env.DATABASE_URL es string (no undefined)
  const db = connectToDatabase(env.DATABASE_URL)
 
  // env.NEXT_PUBLIC_GA_ID puede ser string | undefined
  // porque lo marcamos como .optional()
 
  return <div>...</div>
}

Patron t3-env (avanzado)

El patron usado por create-t3-app separa las variables del servidor y del cliente explicitamente:

typescript
// lib/env.ts
import { z } from 'zod'
 
// Schema para variables del servidor
const serverSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  RESEND_API_KEY: z.string().min(1),
  NODE_ENV: z.enum(['development', 'production', 'test']),
})
 
// Schema para variables del cliente
const clientSchema = z.object({
  NEXT_PUBLIC_SITE_URL: z.string().url(),
  NEXT_PUBLIC_GA_ID: z.string().optional(),
  NEXT_PUBLIC_STRIPE_PK: z.string().startsWith('pk_'),
})
 
// Validar variables del servidor (solo corre en el servidor)
function validateServerEnv() {
  const parsed = serverSchema.safeParse(process.env)
 
  if (!parsed.success) {
    console.error(
      'Variables de entorno del servidor invalidas:',
      parsed.error.flatten().fieldErrors
    )
    throw new Error('Variables de entorno del servidor invalidas')
  }
 
  return parsed.data
}
 
// Validar variables del cliente
function validateClientEnv() {
  const parsed = clientSchema.safeParse({
    NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
    NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
    NEXT_PUBLIC_STRIPE_PK: process.env.NEXT_PUBLIC_STRIPE_PK,
  })
 
  if (!parsed.success) {
    console.error(
      'Variables de entorno del cliente invalidas:',
      parsed.error.flatten().fieldErrors
    )
    throw new Error('Variables de entorno del cliente invalidas')
  }
 
  return parsed.data
}
 
// Exportar variables validadas
export const serverEnv = validateServerEnv()
export const clientEnv = validateClientEnv()

Separar imports por contexto

typescript
// En Server Components y API routes:
import { serverEnv, clientEnv } from '@/lib/env'
 
const dbConnection = connectDB(serverEnv.DATABASE_URL)
const siteUrl = clientEnv.NEXT_PUBLIC_SITE_URL
 
// En Client Components:
import { clientEnv } from '@/lib/env'
 
// Solo accede a variables del cliente
const siteUrl = clientEnv.NEXT_PUBLIC_SITE_URL
Ventaja principal

Con este patron, si alguien agrega una variable de entorno nueva pero olvida configurarla en Vercel, la aplicación falla inmediatamente durante el build con un mensaje descriptivo en vez de fallar silenciosamente en producción.

Validación durante el build

Para que la validación ocurra durante next build y no solo en runtime:

typescript
// next.config.ts
import type { NextConfig } from 'next'
 
// Importar para que se ejecute durante el build
import './lib/env'
 
const nextConfig: NextConfig = {
  // tu configuración
}
 
export default nextConfig

Ahora si falta alguna variable, el build de Vercel falla con un mensaje claro antes de desplegar.

Errores comunes y como solucionarlos

Error 1: Variable undefined en el cliente

typescript
'use client'
 
export function PayButton() {
  // Esto siempre es undefined
  const apiKey = process.env.STRIPE_SECRET_KEY
  console.log(apiKey) // undefined
}

Causa: La variable no tiene el prefijo NEXT_PUBLIC_.

Solución: Si es una variable pública, agrega el prefijo. Si es secreta, accede desde el servidor:

typescript
// Opción A: Si es seguro exponerla (ej. publishable key)
const pk = process.env.NEXT_PUBLIC_STRIPE_PK // Funciona en cliente
 
// Opción B: Si es secreta, usa un Server Component o API route
// app/api/create-payment/route.ts
export async function POST() {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) // Servidor
  const session = await stripe.checkout.sessions.create({ ... })
  return NextResponse.json({ url: session.url })
}

Error 2: Variable existe en local pero no en Vercel

bash
# Build log en Vercel:
Error: DATABASE_URL is not defined
 
# Pero en local funciona perfecto

Causa: La variable esta en tu .env.local pero no esta configurada en el dashboard de Vercel.

Solución:

bash
# Verificar que variables necesitas
cat .env.local | grep -v "^#" | grep -v "^$"
 
# Agregar cada una en Vercel
# Dashboard > Settings > Environment Variables
# O desde la CLI:
vercel env add DATABASE_URL production

Error 3: Exponer secrets en el frontend

typescript
// alguien hizo esto en .env.local:
// NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password123@prod-host/db
 
// Y en el código:
const db = connect(process.env.NEXT_PUBLIC_DATABASE_URL!)
// PROBLEMA: la URL de la base de datos con password es visible en el navegador

Causa: Alguien uso NEXT_PUBLIC_ en una variable que debería ser privada.

Solución: Renombra la variable quitando el prefijo y accede solo desde el servidor:

bash
# Antes (MAL)
NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password123@host/db
 
# después (BIEN)
DATABASE_URL=postgresql://admin:password123@host/db

Si la variable ya estuvo expuesta con NEXT_PUBLIC_, cambia la credencial inmediatamente. El valor anterior ya fue expuesto en builds anteriores.

Error 4: Variables de entorno en el build vs runtime

typescript
// Este valor se fija durante el build y NO cambia
const url = process.env.NEXT_PUBLIC_API_URL
// Si haces build con API_URL=https://staging.api.com
// y luego cambias a https://prod.api.com en Vercel,
// las páginas estáticas siguen usando staging

Causa: NEXT_PUBLIC_ variables se incrustan durante next build. Cambiarlas en Vercel requiere un nuevo build.

Solución: después de cambiar una variable NEXT_PUBLIC_ en Vercel, haz un redeploy:

bash
# Desde el dashboard: Deployments > Redeploy
# O desde CLI:
vercel --prod

Error 5: TypeScript no reconoce process.env

typescript
// TypeScript dice: Type 'string | undefined' is not assignable to type 'string'
const dbUrl: string = process.env.DATABASE_URL // Error de tipo

Solución A: Aserción de no-null (rápido pero no seguro):

typescript
const dbUrl = process.env.DATABASE_URL!

Solución B: Validación con Zod (recomendado):

typescript
import { env } from '@/lib/env' // Validado al inicio
const dbUrl = env.DATABASE_URL // TypeScript sabe qué es string

Solución C: Ampliar los tipos de process.env:

typescript
// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string
    STRIPE_SECRET_KEY: string
    NEXT_PUBLIC_SITE_URL: string
    NODE_ENV: 'development' | 'production' | 'test'
  }
}
Ampliación de tipos

La opción C (archivo de declaración) le dice a TypeScript que las variables existen, pero no válida en runtime que realmente tengan un valor. Combina con la validación de Zod para tener seguridad completa.

Mejores prácticas de seguridad

1. Nunca commitear archivos .env con secretos

Verifica tu .gitignore:

bash
# .gitignore
.env*.local
.env.production
.env.development

Si ya commiteaste un archivo .env con secretos:

bash
# 1. Eliminar del tracking de git (mantiene el archivo local)
git rm --cached .env.local
 
# 2. Agregar a .gitignore
echo ".env*.local" >> .gitignore
 
# 3. Commitear el cambio
git add .gitignore
git commit -m "Remover .env.local del tracking"
 
# 4. IMPORTANTE: Cambiar TODAS las credenciales
# Los valores anteriores ya estan en el historial de git
El historial de git recuerda

Eliminar un archivo de git no borra su historial. Si pusheaste secretos a GitHub, cámbialos inmediatamente.

Verifica si tu sitio expone archivos sensibles

Escáner de archivos sensibles gratuito -- Escanea tu URL y revisa si /.env, /.git/HEAD y otros archivos de configuración son accesibles públicamente.

2. Crear un .env.example completo

bash
# .env.example
# Copia este archivo como .env.local y rellena los valores
 
# Base de datos (formato: postgresql://user:password@host:port/database)
DATABASE_URL=
 
# Stripe (obtener en https://dashboard.stripe.com/apikeys)
STRIPE_SECRET_KEY=sk_test_
NEXT_PUBLIC_STRIPE_PK=pk_test_
 
# Email - Resend (obtener en https://resend.com/api-keys)
RESEND_API_KEY=re_
 
# Sitio
NEXT_PUBLIC_SITE_URL=http://localhost:3000
 
# Analytics (opcional)
NEXT_PUBLIC_GA_ID=

3. Rotar credenciales periodicamente

bash
# Checklist trimestral:
# [ ] Rotar API keys de servicios externos
# [ ] Verificar que no hay credenciales en el código
# [ ] Revisar permisos de variables en Vercel
# [ ] Confirmar que .env.local no esta en git

4. Principio de mínimo privilegio

bash
# MAL: una sola API key con permisos totales
STRIPE_SECRET_KEY=sk_live_full_access_key
 
# BIEN: keys con permisos especificos
STRIPE_SECRET_KEY=sk_live_restricted_key  # Solo lectura de pagos
STRIPE_WEBHOOK_SECRET=whsec_...           # Solo para webhooks

5. Variables diferentes por entorno

bash
# Development: usa servicios de prueba
STRIPE_SECRET_KEY=sk_test_...
DATABASE_URL=postgresql://localhost:5432/dev-db
 
# Preview: usa staging
STRIPE_SECRET_KEY=sk_test_...
DATABASE_URL=postgresql://staging-host:5432/staging-db
 
# Production: usa servicios reales
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgresql://prod-host:5432/prod-db

Nunca uses credenciales de producción en desarrollo. Un error en un script local puede afectar datos reales.

6. Validar al inicio, no en runtime

typescript
// MAL: Descubrir que falta la variable cuando un usuario la necesita
export async function GET() {
  const apiKey = process.env.EXTERNAL_API_KEY
  if (!apiKey) {
    return new Response('API key not configured', { status: 500 })
    // El usuario ve un error. Mal.
  }
}
 
// BIEN: Fallar durante el build si falta algo
// lib/env.ts
import { z } from 'zod'
const env = z.object({
  EXTERNAL_API_KEY: z.string().min(1),
}).parse(process.env)
// Si falta, el build falla con mensaje claro. Nunca llega a producción.

Configuración completa recomendada

aquí esta la estructura completa que recomiendo para manejar variables de entorno en un proyecto NextJS desplegado en Vercel:

Archivos del proyecto

Estructura de archivos

mi-proyecto/ ├── .env (valores no sensibles, base para todos los entornos) ├── .env.local (valores reales para desarrollo, NO en git) ├── .env.example (template para el equipo, SI en git) ├── .gitignore (excluye .env*.local) ├── env.d.ts (tipos de TypeScript para process.env) ├── lib/ │ └── env.ts (validación con Zod) └── next.config.ts (importa lib/env.ts para validar en build)

Archivo lib/env.ts completo

typescript
// lib/env.ts
import { z } from 'zod'
 
// Variables del servidor
const serverSchema = z.object({
  DATABASE_URL: z
    .string()
    .url('DATABASE_URL debe ser una URL válida de la base de datos'),
  STRIPE_SECRET_KEY: z
    .string()
    .startsWith('sk_', 'STRIPE_SECRET_KEY debe empezar con sk_'),
  RESEND_API_KEY: z
    .string()
    .min(1, 'RESEND_API_KEY no puede estar vacia'),
  REVALIDATION_SECRET: z
    .string()
    .min(16, 'REVALIDATION_SECRET debe tener al menos 16 caracteres'),
  NODE_ENV: z
    .enum(['development', 'production', 'test'])
    .default('development'),
})
 
// Variables del cliente
const clientSchema = z.object({
  NEXT_PUBLIC_SITE_URL: z
    .string()
    .url('NEXT_PUBLIC_SITE_URL debe ser una URL válida'),
  NEXT_PUBLIC_GA_ID: z
    .string()
    .optional(),
  NEXT_PUBLIC_STRIPE_PK: z
    .string()
    .startsWith('pk_', 'NEXT_PUBLIC_STRIPE_PK debe empezar con pk_')
    .optional(),
})
 
// Schema combinado
const envSchema = serverSchema.merge(clientSchema)
 
// Validar
function validateEnv() {
  const parsed = envSchema.safeParse(process.env)
 
  if (!parsed.success) {
    const errors = parsed.error.flatten().fieldErrors
    const errorMessages = Object.entries(errors)
      .map(([key, messages]) => `  ${key}: ${messages?.join(', ')}`)
      .join('\n')
 
    console.error('Variables de entorno invalidas:\n' + errorMessages)
 
    throw new Error(
      'Variables de entorno invalidas. Revisa la consola para más detalles.'
    )
  }
 
  return parsed.data
}
 
export const env = validateEnv()
export type Env = z.infer<typeof envSchema>

Archivo env.d.ts

typescript
// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    // Servidor
    DATABASE_URL: string
    STRIPE_SECRET_KEY: string
    RESEND_API_KEY: string
    REVALIDATION_SECRET: string
    NODE_ENV: 'development' | 'production' | 'test'
 
    // Cliente
    NEXT_PUBLIC_SITE_URL: string
    NEXT_PUBLIC_GA_ID?: string
    NEXT_PUBLIC_STRIPE_PK?: string
  }
}

Archivo .env

bash
# .env -- Valores base (no sensibles)
NEXT_PUBLIC_SITE_NAME=Mi Proyecto
NODE_ENV=development

Archivo .env.example

bash
# .env.example -- Copia como .env.local y rellena valores
 
# === Base de datos ===
DATABASE_URL=postgresql://user:password@localhost:5432/mi-db
 
# === Stripe ===
STRIPE_SECRET_KEY=sk_test_
NEXT_PUBLIC_STRIPE_PK=pk_test_
 
# === Email ===
RESEND_API_KEY=re_
 
# === Seguridad ===
REVALIDATION_SECRET=  # mínimo 16 caracteres
 
# === Sitio ===
NEXT_PUBLIC_SITE_URL=http://localhost:3000
 
# === Analytics (opcional) ===
NEXT_PUBLIC_GA_ID=

Variables de entorno en el flujo completo

Para visualizar como fluyen las variables en cada etapa:

plaintext
Desarrollo local:
  .env + .env.local → npm run dev → Variables disponibles
 
Build en Vercel (Production):
  .env + Vercel Dashboard (Production) → next build → Deploy a producción
 
Build en Vercel (Preview):
  .env + Vercel Dashboard (Preview) → next build → Deploy de PR
 
Runtime en Vercel:
  Variables del build + runtime env → Tu aplicación

Verificar que variables tiene tu deploy

typescript
// app/api/debug/env/route.ts
// SOLO para debugging en desarrollo. Eliminar antes de producción.
import { NextResponse } from 'next/server'
 
export async function GET() {
  // Solo permitir en desarrollo
  if (process.env.NODE_ENV === 'production') {
    return new NextResponse('Not found', { status: 404 })
  }
 
  return NextResponse.json({
    NODE_ENV: process.env.NODE_ENV,
    SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
    // NUNCA expongas variables secretas, ni en debug
    HAS_DATABASE_URL: !!process.env.DATABASE_URL,
    HAS_STRIPE_KEY: !!process.env.STRIPE_SECRET_KEY,
    HAS_RESEND_KEY: !!process.env.RESEND_API_KEY,
  })
}
Endpoint de debug

Este endpoint es solo para debugging durante el desarrollo. Eliminalo o desactivalo antes de hacer deploy a producción. Incluso mostrando solo booleanos, revela que servicios usa tu aplicación.

Preguntas frecuentes

¿Puedo usar variables de entorno en archivos de configuración?

Si. Archivos como next.config.ts corren en Node.js y tienen acceso a todas las variables:

typescript
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: process.env.IMAGE_CDN_HOSTNAME || 'images.unsplash.com',
      },
    ],
  },
}
 
export default nextConfig

¿Cómo uso variables de entorno en archivos CSS o Tailwind?

No puedes acceder directamente a process.env desde CSS. Usa CSS custom properties definidas desde JavaScript:

typescript
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html
      lang="es"
      style={{
        '--brand-color': process.env.NEXT_PUBLIC_BRAND_COLOR || '#3b82f6',
      } as React.CSSProperties}
    >
      <body>{children}</body>
    </html>
  )
}
css
/* En tu CSS */
.brand-button {
  background-color: var(--brand-color);
}

¿Qué pasa si cambio una variable en Vercel sin hacer redeploy?

Depende del tipo de variable:

  • Variables de servidor (sin NEXT_PUBLIC_): Toman efecto en la siguiente invocación de función serverless (no necesitan redeploy en la mayoria de casos)
  • Variables NEXT_PUBLIC_: Requieren redeploy porque se incrustan durante el build

¿Puedo tener variables de entorno por rama en Vercel?

Si. En el dashboard de Vercel, al editar una variable de Preview, puedes asociarla a ramas especificas:

plaintext
Variable: DATABASE_URL
Entorno: Preview
Ramas: staging, develop
 
Variable: DATABASE_URL
Entorno: Preview
Ramas: feature/*
Valor: (otra URL de base de datos)

Esto es útil para equipos grandes donde cada rama de feature tiene su propia base de datos.

Conclusion

Las variables de entorno en NextJS tienen reglas claras:

  1. Sin NEXT_PUBLIC_: Solo disponible en el servidor. Usa para secretos.
  2. Con NEXT_PUBLIC_: Disponible en todos lados. Solo para datos publicos.
  3. Validar con Zod: Falla durante el build, no en producción.
  4. Vercel dashboard: Configura valores por entorno (Production, Preview, Development).
  5. Nunca commitear: Los archivos .env.local no van a git.

El error más costoso es exponer una credencial en el frontend. El segundo más costoso es descubrir que falta una variable en producción cuando un usuario reporta un error. Validar al inicio y separar correctamente publicas de privadas elimina ambos problemas.


Recursos adicionales

#nextjs#vercel#variables-entorno#seguridad

Preguntas frecuentes

¿Cuál es la diferencia entre .env, .env.local y .env.production en NextJS?

.env es el archivo base que se carga siempre y puede subirse a git como template. .env.local contiene valores reales y secretos, nunca debe subirse a git. .env.production solo se carga cuando ejecutas next build o next start, y sus valores sobreescriben a .env. La prioridad de carga es: .env.local > .env.production > .env.

¿Qué hace el prefijo NEXT_PUBLIC_ en variables de entorno de NextJS?

El prefijo NEXT_PUBLIC_ le dice a NextJS que esa variable debe incluirse en el bundle de JavaScript del cliente. Sin este prefijo, la variable solo es accesible en el servidor (Server Components, API routes, middleware). Nunca pongas claves secretas como API keys o tokens de base de datos con NEXT_PUBLIC_ porque cualquier usuario puede verlas en el código fuente del navegador.

¿Cómo válido variables de entorno en NextJS con Zod?

Crea un archivo como lib/env.ts donde defines un schema de Zod con todas las variables que tu aplicación necesita. Llama a z.object().parse(process.env) al inicio de la app. Si falta alguna variable o tiene un formato invalido, la aplicación falla inmediatamente con un mensaje claro en vez de fallar en runtime con un error undefined difícil de debuggear.

¿Por qué mi variable de entorno es undefined en el cliente?

Las variables de entorno sin el prefijo NEXT_PUBLIC_ no estan disponibles en el navegador. Si necesitas acceder a una variable desde un Client Component, Asegúrate de que su nombre empiece con NEXT_PUBLIC_. Si es una variable secreta que no debe exponerse, accede a ella desde un Server Component o API route y pasa solo los datos necesarios al cliente.

¿Cómo configuró variables de entorno diferentes para producción y preview en Vercel?

En el dashboard de Vercel, ve a Settings > Environment Variables. Al agregar o editar una variable, selecciona el entorno donde aplica: Production, Preview o Development. Puedes tener valores diferentes para cada entorno. Por ejemplo, DATABASE_URL puede apuntar a la base de datos de producción en Production y a una base de staging en Preview.