guias·12 min de lectura

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:

typescript
// 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
typescript
// 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 repositorio

Las variables de entorno resuelven tres problemas fundamentales:

  1. Seguridad: Las credenciales no viven en el codigo fuente
  2. Flexibilidad: Valores diferentes para desarrollo, staging y produccion
  3. 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

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 documentacion de que variables necesita la aplicacion.

.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 produccion

bash
# .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-XXXXXXXXXX

Subir a git: Depende. Si solo tiene variables NEXT_PUBLIC_ sin informacion 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 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

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 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)

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 mas 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 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

VariableNEXT_PUBLIC_Razon
DATABASE_URLNoCredencial de base de datos
STRIPE_SECRET_KEYNoPermite cobrar dinero
RESEND_API_KEYNoPermite enviar emails
JWT_SECRETNoFirma tokens de autenticacion
SITE_URLSiInformacion publica
GA_MEASUREMENT_IDSiGoogle lo requiere en el frontend
STRIPE_PUBLISHABLE_KEYSiDiseñado para estar en el frontend
SENTRY_DSNSiEl 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:

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 tambien 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 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

1

Ve a tu proyecto en Vercel > Settings > Environment Variables

Aqui 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 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.local
💡
vercel 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:

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

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

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

typescript
// 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 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_.

Solucion: Si es una variable publica, agrega el prefijo. Si es secreta, accede desde el servidor:

typescript
// 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

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.

Solucion:

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 codigo:
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 deberia ser privada.

Solucion: Renombra la variable quitando el prefijo y accede solo desde el servidor:

bash
# Antes (MAL)
NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password123@host/db
 
# Despues (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 paginas estaticas siguen usando staging

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

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

Solucion A: Asercion de no-null (rapido pero no seguro):

typescript
const dbUrl = process.env.DATABASE_URL!

Solucion B: Validacion con Zod (recomendado):

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

Solucion 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'
  }
}
⚠️
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:

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, 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

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 codigo
# [ ] Revisar permisos de variables en Vercel
# [ ] Confirmar que .env.local no esta en git

4. Principio de minimo 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 produccion 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 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

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 (validacion 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 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

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

plaintext
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 aplicacion

Verificar que variables tiene tu deploy

typescript
// 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:

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

Como 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);
}

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:

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

  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 produccion.
  4. Vercel dashboard: Configura valores por entorno (Production, Preview, Development).
  5. Nunca commitear: Los archivos .env.local no 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

#nextjs#vercel#variables-entorno#seguridad

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.