Entorno
Variables de entorno requeridas y opcionales. El schema en apps/server/src/bootstrap/env.ts es la fuente de verdad.
Última actualización 27 oct 2026
Todas las env vars son validadas por un schema Zod en apps/server/src/bootstrap/env.ts. El servidor falla al arrancar con un error claro si falta una variable requerida o está mal formada. Los adapters opcionales arrancan como no-op cuando su configuración está sin definir — nunca necesitás llenar todas las claves.
Mínimo para arrancar
Estas cuatro son el piso. Sin ellas el servidor sale temprano:
BETTER_AUTH_SECRET=<openssl rand -base64 32>
BETTER_AUTH_URL=http://localhost:3005
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/boilerplate?schema=public
CORS_ORIGINS=http://localhost:3004Generá el auth secret con openssl rand -base64 32. CORS_ORIGINS es una lista separada por comas de los origins permitidos para el browser.
Auth providers (opcional)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=Dejá ambos vacíos para deshabilitar Google OAuth. El magic link se configura a través de tu provider de email — ver el módulo notifications.
Storage (opcional)
STORAGE_PROVIDER=null # null | local | s3 | uploadthing
# S3 / R2 / MinIO
STORAGE_S3_BUCKET=
STORAGE_S3_REGION=
STORAGE_S3_ACCESS_KEY_ID=
STORAGE_S3_SECRET_ACCESS_KEY=
STORAGE_S3_ENDPOINT= # ponelo para R2/MinIO; dejalo vacío para AWS
STORAGE_S3_PUBLIC_BASE_URL= # base del CDN
# Disco local (dev)
STORAGE_LOCAL_ROOT=
STORAGE_LOCAL_PUBLIC_BASE_URL=
# UploadThing
UPLOADTHING_TOKEN=
UPLOADTHING_APP_ID=STORAGE_PROVIDER=null (el default) saltea el registro de un adapter, así que los endpoints que necesitan uploads devuelven 503. El provider local arma URLs públicas desde BETTER_AUTH_URL (la URL del server), no APP_URL — los uploads los sirve la API, no Next.js.
Background jobs (opcional)
REDIS_URL=redis://localhost:6379
JOBS_REDIS_URL= # default a REDIS_URL
JOBS_PREFIX=bull
BULL_BOARD_ENABLED=true # monta la UI de /admin/queuesSin Redis, BullMQ se registra pero ningún worker conecta. BULL_BOARD_ENABLED=true monta una UI de Bull Board protegida en /admin/queues.
Rate limiting
RATE_LIMIT_REDIS_ENABLED=true # cae a memoria cuando REDIS_URL no está
RATE_LIMIT_READ_PER_MIN=300
RATE_LIMIT_WRITE_PER_MIN=60
RATE_LIMIT_AUTH_IP_PER_MIN=30
RATE_LIMIT_AUTH_EMAIL_PER_15MIN=5Los cuatro tiers (read, write, auth-ip, auth-email) tienen cada uno su propio prefijo de Redis (rl:read:, rl:write:, etc.) así los counters nunca colisionan. Los handlers del limiter se memoizan sobre la referencia RateLimitDeps, así el modo memoria se comporta idéntico al modo Redis (un store compartido por tier).
AI chat (opcional)
ANTHROPIC_API_KEY= # vacío = stream stub, sin llamadas upstream
AI_MODEL=claude-haiku-4-5 # modelo por defecto, siempre permitido
AI_ALLOWED_MODELS= # modelos extra que el cliente puede pedir (separados por coma)
AI_MAX_OUTPUT_TOKENS=1024 # tope duro de tokens generados por completion
AI_STREAM_TIMEOUT_MS=120000 # tope de tiempo (ms) por stream de chat; aborta el upstreamPOST /api/v1/ai/chat rechaza con 422 cualquier model fuera de AI_MODEL + AI_ALLOWED_MODELS, así que un caller no puede elegir un modelo más caro.
Observabilidad (opcional)
SENTRY_DSN= # dejalo vacío para deshabilitar
SENTRY_TRACES_SAMPLE_RATE=0.1
APP_VERSION=
OTEL_EXPORTER_OTLP_ENDPOINT= # dejalo vacío para deshabilitar trace export
OTEL_SERVICE_NAME=mern-saas-server
METRICS_ENABLED=true # endpoint /metrics de PrometheusOTel debe arrancar antes de que se carguen Express/Prisma. El primer import en apps/server/src/index.ts es ./otel-init.js por eso. No lo reordenes.
El SDK de Sentry server es @sentry/bun (no @sentry/node). logger.error(...) no auto-captura; usá el helper captureError(err, ctx) para errores que deban paginar a alguien.
Billing (depende del provider)
Configurá PAYMENT_PROVIDER con uno de stripe, mercado-pago, polar, después llená las claves de ese provider:
PAYMENT_PROVIDER=stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# o
PAYMENT_PROVIDER=mercado-pago
MERCADO_PAGO_ACCESS_TOKEN=
MERCADO_PAGO_WEBHOOK_SECRET=
# o
PAYMENT_PROVIDER=polar
POLAR_ACCESS_TOKEN=
POLAR_WEBHOOK_SECRET=
POLAR_SERVER=sandbox # o production[!WARNING]
POLAR_SERVERessandboxpor default fuera de producción. ConNODE_ENV=productionyPAYMENT_PROVIDER=polardebe setearse explícitamente (sandboxoproduction): el server se niega a arrancar si falta, así una variable olvidada no manda tráfico real al sandbox de Polar.
Ver Billing para la estrategia y el setup de webhooks.
Usage metering (opcional)
Deshabilitado por default. Activá el flag una vez que el downstream registró sus meters y un mapa plan → cap.
USAGE_METERING_ENABLED=false # poné "true" para enforzar cuotas
USAGE_FREE_PRICE_ID=price_free_synthetic # key sintético del tier Free; no es un price real de PolarVer Usage metering para la referencia completa.
Pagos cripto (opcional)
El módulo crypto-payments (Aliquot) arranca sin ninguna de estas. Mientras CRYPTO_MASTER_MNEMONIC no esté seteada, todo endpoint /api/v1/crypto/* y /api/v1/public/* responde 503 CRYPTO_NOT_CONFIGURED; el resto del server no se ve afectado.
CRYPTO_MASTER_MNEMONIC="<BIP-39 mnemonic>" # custody; unset → crypto endpoints answer 503
CRYPTO_RPC_POLYGON_AMOY=https://polygon-amoy-bor-rpc.publicnode.com,https://polygon-amoy.drpc.org
CRYPTO_RPC_BSC_TESTNET=https://bsc-testnet-rpc.publicnode.com
CRYPTO_RPC_BASE_SEPOLIA=https://sepolia.base.org,https://base-sepolia-rpc.publicnode.com
# CRYPTO_RPC_POLYGON_MAINNET= / CRYPTO_RPC_BSC_MAINNET= / CRYPTO_RPC_BASE_MAINNET=
# CRYPTO_CONFIRMATIONS_POLYGON_AMOY=5 # dev-only override (defaults: Polygon 64, BSC 15, Base 30)
CRYPTO_PLATFORM_FEE_BPS=50 # default processing fee (0.50 %) for orgs without their own config
CRYPTO_PAYOUT_FEE_POLYGON=0.10 # flat token fee per payout, per network family
CRYPTO_PAYOUT_FEE_BSC=0.15
CRYPTO_PAYOUT_FEE_BASE=0.10
# CRYPTO_LOGS_MAX_RANGE_BSC_TESTNET=200 # max blocks per eth_getLogs call (default 200, all 6 networks)
# CRYPTO_NATIVE_PRICE_USD_POLYGON=0.50 # fixed native price for refund fee quotes (BSC 600, BASE 3000)
# CRYPTO_CHECKOUT_BASE_URL=https://pay.example.com # hosted checkout origin; defaults to APP_URL
# TELEGRAM_BOT_TOKEN= # Telegram Mini-App initData verificationCRYPTO_MASTER_MNEMONICes la wallet de custodia (HD pathm/44'/60'/0'/0/i: el índice 0 es la hot wallet, las direcciones de depósito de las invoices arrancan en 1000). El proceso de la API deriva direcciones solo desde la clave pública extendida; la firma ocurre en el worker. Tiene que ser un mnemonic BIP-39 válido en inglés.CRYPTO_RPC_<NETWORK>acepta URLs separadas por coma (fallback de viem, la primera es la principal). Una red sin URLs aparece deshabilitada enGET /api/v1/crypto/networksy no puede recibir invoices. Para cadenas locales corrébun run chain:devy apuntá estas variables a las URLs que imprime.NEXT_PUBLIC_CRYPTO_RPC_<NETWORK>(cliente,apps/client/.env.local, opcional, una URL) es el RPC que el botón "Pagar con wallet" del checkout le pasa a la wallet del comprador cuando la wallet todavía no conoce la red (wallet_addEthereumChain). Sin definir, el checkout usa el RPC público de la red. Definilo solo para cadenas locales, por ejemploNEXT_PUBLIC_CRYPTO_RPC_POLYGON_AMOY=http://127.0.0.1:18002, así una wallet del navegador envía a la misma cadena anvil que escanea el servidor. Se embebe en el build: reconstruí (o reiniciánext dev) después de cambiarla.NEXT_PUBLIC_SITE_URL(cliente, opcional) es el origen canónico del sitio público: metadata, Open Graph, sitemap, robots, JSON-LD yllms.txt. Sin definir, eshttps://aliquot.cc, el dominio de producción. Definilo para un deploy de staging. Se embebe en el build.CRYPTO_CHECKOUT_BASE_URLarmapay_url(<base>/pay/<invoiceId>), los links de reclamo de refunds (<base>/pay/<invoiceId>/refund/<token>) y las URLs de payment links (<base>/l/<slug>).CRYPTO_LOGS_MAX_RANGE_<NETWORK>limita el rango de bloques de cada llamadaeth_getLogs(default 200:sepolia.base.orgno acepta más). El scanner además parte a la mitad cualquier rango que el provider rechace por tamaño y se pone al día con un backlog en pasos acotados, así que casi nunca hace falta tocarlo.CRYPTO_NATIVE_PRICE_USD_<FAMILY>(POLYGON0.50,BSC600,BASE3000) convierte el gas de un refund a unidades de stablecoin: el comprador recibegross − max(gas × max fee × precio, 0.01), fijado al reclamar.TELEGRAM_BOT_TOKENes el token del bot que abre el checkout como Telegram Mini-App (/pay/<invoiceId>?tg=1).POST /api/v1/public/telegram/verifyverifica con él la firma delinitDatade la Mini-App (HMAC-SHA256, secret = HMAC("WebAppData", token)) y rechaza unauth_datede más de 24 h. Sin el token ese endpoint responde 503TELEGRAM_NOT_CONFIGURED; el checkout sigue funcionando.- El worker (
bun run worker) corre el camino del dinero: scanner de cadena (2 s), expiración (10 s), sweeps (15 s), payouts (3 s), refunds (5 s). Necesita Redis; sin Redis no se escanea ni se envía nada. El gas nativo que llega a la hot wallet (índice 0) se registra automáticamente como fondeo del operador (EQUITY:OPERATOR_FUNDING).
[!WARNING] Con
NODE_ENV=productionel server se niega a arrancar con el mnemonic público de desarrollo (el commiteado enapps/server/.envy.env.example), y rechaza overridesCRYPTO_CONFIRMATIONS_*para mainnets.
Agregar una nueva variable
- Editá
apps/server/src/bootstrap/env.tsy extendé el schema Zod. Las vars requeridas deben tener.min(1); las opcionales usan.optional()o.default(...). - Agregá una entrada documentada en
apps/server/.env.example. - Leela desde
env.YOUR_VAR— nunca desdeprocess.envdirecto.