Variables de entorno en NextJS y Vercel: Guia segura
Guia completa sobre variables de entorno en NextJS y Vercel. Aprende a configurar .env, NEXT_PUBLIC_, validacion con Zod y mejores practicas de seguridad para proteger tus secrets.
Variables de entorno en NextJS y Vercel: Guia segura
Las variables de entorno en NextJS y Vercel son el mecanismo para configurar tu aplicacion sin hardcodear valores que cambian entre entornos. Claves de API, URLs de bases de datos, tokens de autenticacion: todo lo que tu aplicacion necesita para funcionar pero que no debe vivir en el codigo fuente.
El problema es que un error en la configuracion puede exponer credenciales sensibles en el navegador del usuario, o hacer que tu aplicacion falle en produccion porque una variable que existe en tu .env.local no esta configurada en Vercel. Esta guia cubre como evitar ambos escenarios.
Por que las variables de entorno son criticas
Consideremos un ejemplo concreto. Tu aplicacion usa Stripe para procesar pagos:
// Si hardcodeas la clave en el codigo:
const stripe = new Stripe('sk_live_abc123xyz789...')
// Problema: cualquiera con acceso al repo ve tu clave
// Problema: no puedes cambiarla sin modificar codigo
// Problema: la misma clave se usa en desarrollo y produccion// Con variables de entorno:
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
// La clave vive fuera del codigo
// 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 codigo fuente
- Flexibilidad: Valores diferentes para desarrollo, staging y produccion
- Portabilidad: El mismo codigo funciona en cualquier entorno
Archivos .env en NextJS: cual usar y cuando
NextJS soporta multiples archivos .env, cada uno con un proposito especifico. El orden de prioridad determina cual sobreescribe a cual.
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 documentacion de que variables necesita la aplicacion.
.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 produccion
# .env.production
# Valores especificos para el build de produccion
# 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 informacion 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 documentacion para tu equipo.
Buena practica
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 mas 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 mas 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 unica excepcion son las claves publicas diseñadas para estar en el frontend, como la publishable key de Stripe (pk_).
Guia rapida: 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 autenticacion |
| SITE_URL | Si | Informacion publica |
| 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 publico por diseño |
Acceder a variables en Server Components vs Client Components
NextJS 15 con App Router distingue claramente entre codigo que corre en el servidor y codigo que corre en el cliente. Esto afecta directamente como accedes a las variables de entorno. Si necesitas repasar la diferencia, revisa la guia 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 tambien 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 minimo privilegio
Nunca pases mas informacion 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
Aqui 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 produccion
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 util cuando empiezas en un proyecto existente. Descarga todas las variables de entorno de development a tu .env.local automaticamente.
Valores diferentes por entorno
Es comun necesitar valores distintos para produccion 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 produccion.
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 codigo 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 produccion. La solucion es validar todas las variables al inicio de la aplicacion.
Patron basico con Zod
Si no conoces Zod, revisa la guia completa de Zod para validacion. Aqui 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 valida'),
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 valida'),
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 modulo
export const env = envSchema.parse(process.env)
// Tipo inferido automaticamente
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 aplicacion falla inmediatamente durante el build con un mensaje descriptivo en vez de fallar silenciosamente en produccion.
Validacion durante el build
Para que la validacion 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 configuracion
}
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_.
Solucion: Si es una variable publica, agrega el prefijo. Si es secreta, accede desde el servidor:
// Opcion A: Si es seguro exponerla (ej. publishable key)
const pk = process.env.NEXT_PUBLIC_STRIPE_PK // Funciona en cliente
// Opcion 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.
Solucion:
# 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 codigo:
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 deberia ser privada.
Solucion: Renombra la variable quitando el prefijo y accede solo desde el servidor:
# Antes (MAL)
NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password123@host/db
# Despues (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 paginas estaticas siguen usando stagingCausa: NEXT_PUBLIC_ variables se incrustan durante next build. Cambiarlas en Vercel requiere un nuevo build.
Solucion: Despues 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 tipoSolucion A: Asercion de no-null (rapido pero no seguro):
const dbUrl = process.env.DATABASE_URL!Solucion B: Validacion con Zod (recomendado):
import { env } from '@/lib/env' // Validado al inicio
const dbUrl = env.DATABASE_URL // TypeScript sabe que es stringSolucion 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'
}
}Ampliacion de tipos
La opcion C (archivo de declaracion) le dice a TypeScript que las variables existen, pero no valida en runtime que realmente tengan un valor. Combina con la validacion de Zod para tener seguridad completa.
Mejores practicas 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, cambialos inmediatamente. Herramientas como datahogo monitorean tu repositorio y te alertan si detectan credenciales expuestas en el codigo o en el historial de commits.
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 codigo
# [ ] Revisar permisos de variables en Vercel
# [ ] Confirmar que .env.local no esta en git4. Principio de minimo 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 produccion 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 produccion.Configuracion completa recomendada
Aqui 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 (validacion 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 valida 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 valida'),
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 mas 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= # Minimo 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 produccion
Build en Vercel (Preview):
.env + Vercel Dashboard (Preview) → next build → Deploy de PR
Runtime en Vercel:
Variables del build + runtime env → Tu aplicacionVerificar que variables tiene tu deploy
// app/api/debug/env/route.ts
// SOLO para debugging en desarrollo. Eliminar antes de produccion.
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 produccion. Incluso mostrando solo booleanos, revela que servicios usa tu aplicacion.
Preguntas frecuentes
Puedo usar variables de entorno en archivos de configuracion?
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 nextConfigComo 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);
}Que 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 invocacion de funcion 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 util 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 produccion.
- Vercel dashboard: Configura valores por entorno (Production, Preview, Development).
- Nunca commitear: Los archivos
.env.localno van a git.
El error mas costoso es exponer una credencial en el frontend. El segundo mas costoso es descubrir que falta una variable en produccion cuando un usuario reporta un error. Validar al inicio y separar correctamente publicas de privadas elimina ambos problemas.
Recursos adicionales
Preguntas frecuentes
Cual 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.
Que 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 codigo fuente del navegador.
Como valido 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 aplicacion necesita. Llama a z.object().parse(process.env) al inicio de la app. Si falta alguna variable o tiene un formato invalido, la aplicacion falla inmediatamente con un mensaje claro en vez de fallar en runtime con un error undefined dificil de debuggear.
Por que 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, asegurate 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.
Como configuro variables de entorno diferentes para produccion 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 produccion en Production y a una base de staging en Preview.
Articulos relacionados
Next.js 16: Guia de Migracion 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: Migracion desde v3
Migra tu proyecto de Tailwind CSS 3 a 4. Cambios principales, nuevo sistema de configuracion, CSS-first config y como actualizar sin romper tu app.