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:
// 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// 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 repositorioLas variables de entorno resuelven tres problemas fundamentales:
- Seguridad: Las credenciales no viven en el código fuente
- Flexibilidad: Valores diferentes para desarrollo, staging y producción
- 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
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
# .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=developmentSubir 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
# .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:3000Subir a git: Nunca. NextJS ya incluye .env.local en el .gitignore por defecto. Verifica que tu .gitignore lo tenga:
# .gitignore
.env*.local.env.production -- Valores para producción
# .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-XXXXXXXXXXSubir 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
# .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:3000Este 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
| Archivo | Contiene secretos | Subir a git | Cuando se carga |
|---|---|---|---|
.env | No | Si | Siempre |
.env.local | Si | No | Siempre (sobreescribe .env) |
.env.development | No | Si | Solo npm run dev |
.env.production | No (idealmente) | Depende | Solo npm run build |
.env.example | No (template) | Si | Nunca (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)
// 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, middlewareEstas variables nunca llegan al bundle de JavaScript del cliente. Si intentas accederlas desde un Client Component, obtienes undefined.
Variables CON el prefijo (publicas)
// 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 ladosEstas 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
// 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 tuyoRegla 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
| Variable | NEXT_PUBLIC_ | Razon |
|---|---|---|
| DATABASE_URL | No | Credencial de base de datos |
| STRIPE_SECRET_KEY | No | Permite cobrar dinero |
| RESEND_API_KEY | No | Permite enviar emails |
| JWT_SECRET | No | Firma tokens de autenticación |
| SITE_URL | Si | Información pública |
| GA_MEASUREMENT_ID | Si | Google lo requiere en el frontend |
| STRIPE_PUBLISHABLE_KEY | Si | Diseñado para estar en el frontend |
| SENTRY_DSN | Si | El 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:
// 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:
'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:
// 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:
// 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:
// 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} />
}// 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
Ve a tu proyecto en Vercel > Settings > Environment Variables
aquí puedes agregar, editar y eliminar variables.
Agrega cada variable con su valor
Escribe el nombre (Key) y el valor (Value) de cada variable.
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 deven local
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:
# 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.localvercel 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:
# 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
// 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/falseValidar 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:
// 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
// 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:
// 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
// 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_URLVentaja 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:
// 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 nextConfigAhora 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
'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:
// 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
# Build log en Vercel:
Error: DATABASE_URL is not defined
# Pero en local funciona perfectoCausa: La variable esta en tu .env.local pero no esta configurada en el dashboard de Vercel.
Solución:
# 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 productionError 3: Exponer secrets en el frontend
// 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 navegadorCausa: 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:
# Antes (MAL)
NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password123@host/db
# después (BIEN)
DATABASE_URL=postgresql://admin:password123@host/dbSi 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
// 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 stagingCausa: 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:
# Desde el dashboard: Deployments > Redeploy
# O desde CLI:
vercel --prodError 5: TypeScript no reconoce process.env
// TypeScript dice: Type 'string | undefined' is not assignable to type 'string'
const dbUrl: string = process.env.DATABASE_URL // Error de tipoSolución A: Aserción de no-null (rápido pero no seguro):
const dbUrl = process.env.DATABASE_URL!Solución B: Validación con Zod (recomendado):
import { env } from '@/lib/env' // Validado al inicio
const dbUrl = env.DATABASE_URL // TypeScript sabe qué es stringSolución C: Ampliar los tipos de process.env:
// 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:
# .gitignore
.env*.local
.env.production
.env.developmentSi ya commiteaste un archivo .env con secretos:
# 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 gitEl 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
# .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
# 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 git4. Principio de mínimo privilegio
# 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 webhooks5. Variables diferentes por entorno
# 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-dbNunca uses credenciales de producción en desarrollo. Un error en un script local puede afectar datos reales.
6. Validar al inicio, no en runtime
// 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
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
// 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
// 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
# .env -- Valores base (no sensibles)
NEXT_PUBLIC_SITE_NAME=Mi Proyecto
NODE_ENV=developmentArchivo .env.example
# .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:
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ónVerificar que variables tiene tu deploy
// 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:
// 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:
// 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>
)
}/* 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:
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:
- Sin NEXT_PUBLIC_: Solo disponible en el servidor. Usa para secretos.
- Con NEXT_PUBLIC_: Disponible en todos lados. Solo para datos publicos.
- Validar con Zod: Falla durante el build, no en producción.
- Vercel dashboard: Configura valores por entorno (Production, Preview, Development).
- Nunca commitear: Los archivos
.env.localno 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
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.
Articulos relacionados
Next.js 16: guía de Migración y Novedades
Migra tu proyecto de Next.js 15 a 16. Novedades principales, breaking changes, y pasos para actualizar sin romper tu app.
Testing en Next.js con Vitest y Playwright
Configura testing en tu proyecto Next.js. Unit tests con Vitest, E2E con Playwright, y como integrarlos en tu pipeline de CI/CD.
Tailwind CSS 4: Migración desde v3
Migra tu proyecto de Tailwind CSS 3 a 4. Cambios principales, nuevo sistema de configuración, CSS-first config y como actualizar sin romper tu app.