EmpiezaTuSaaS

Variables de Entorno

Buenas practicas para manejar secretos y variables de entorno

Variables de Entorno

Un secreto filtrado puede comprometer toda tu aplicación: acceso a la base de datos, cobros fraudulentos con Stripe, envio de emails en tu nombre. Sigue estas practicas para evitarlo.

Regla de oro

Nunca subas archivos .env a Git. Nunca expongas secretos del servidor en el cliente.

.env.local y .gitignore

El archivo .env.local contiene tus secretos de desarrollo. Verifica que este en .gitignore:

.gitignore
# Variables de entorno
.env
.env.local
.env.*.local

El boilerplate incluye .env.example con las variables necesarias pero sin valores reales:

.env.example
# App
NEXT_PUBLIC_APP_URL="http://localhost:3000"

# Database
DATABASE_URL=""

# Better Auth
BETTER_AUTH_SECRET=""
BETTER_AUTH_URL="http://localhost:3000"

# Stripe
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=""

# Resend
RESEND_API_KEY=""

Servidor vs Cliente

Next.js tiene una regla simple pero critica:

PrefijoAccesible enSeguridad
NEXT_PUBLIC_Cliente + ServidorVisible para cualquiera
Sin prefijoSolo ServidorProtegido

Variables públicas (cliente)

Solo estas variables deben tener el prefijo NEXT_PUBLIC_:

  • NEXT_PUBLIC_APP_URL - URL de tu aplicación
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY - Clave publica de Stripe

Estas variables se incluyen en el bundle de JavaScript del navegador. Cualquier usuario puede verlas.

Variables privadas (servidor)

Todas las demas variables son privadas y solo accesibles en Server Components, API Routes y Server Actions:

  • DATABASE_URL
  • STRIPE_SECRET_KEY
  • STRIPE_WEBHOOK_SECRET
  • BETTER_AUTH_SECRET
  • RESEND_API_KEY
  • GOOGLE_CLIENT_SECRET

Si accidentalmente agregas NEXT_PUBLIC_ a una variable secreta, esa variable se expone en el navegador. Revisa esto con cuidado.

Validar variables al iniciar

No esperes a que falle una peticion para descubrir que falta una variable. Valida al arrancar:

lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  // Publicas
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),

  // Privadas
  DATABASE_URL: z.string().min(1, "DATABASE_URL es obligatoria"),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
  BETTER_AUTH_SECRET: z.string().min(16, "BETTER_AUTH_SECRET debe tener al menos 16 caracteres"),
  BETTER_AUTH_URL: z.string().url(),
  RESEND_API_KEY: z.string().startsWith("re_"),
});

export const env = envSchema.parse({
  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
  DATABASE_URL: process.env.DATABASE_URL,
  STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
  STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
  BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
  BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
  RESEND_API_KEY: process.env.RESEND_API_KEY,
});

Importa env en vez de usar process.env directamente:

// Mal
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Bien
import { env } from "@/lib/env";
const stripe = new Stripe(env.STRIPE_SECRET_KEY);

Generar secretos seguros

# Para BETTER_AUTH_SECRET
openssl rand -base64 32

# Alternativa con Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Genera un secreto nuevo para cada entorno (desarrollo, staging, producción). Nunca reutilices secretos entre entornos.

Checklist de seguridad

  • .env.local está en .gitignore
  • No hay secretos en el código fuente (busca con git log -p -S "sk_live")
  • Solo variables realmente públicas tienen prefijo NEXT_PUBLIC_
  • Variables se validan al arrancar con Zod
  • Secretos son distintos entre desarrollo y producción
  • Equipo usa .env.example como referencia, nunca comparte .env.local

On this page