Перейти к основному содержимому

Переменные окружения (Environment Variables)

Все клиентские env-переменные попадают в публичный бандл и могут быть прочитаны любым пользователем.

Правила

FORBIDDEN

FORBIDDEN: Хранить в клиентских переменных:

  • Секретные ключи API
  • Ключи подписи (JWT secrets)
  • Пароли
  • Private API tokens
  • Database credentials
  • Encryption keys

MAY

MAY: Хранить в клиентских переменных:

  • Публичные идентификаторы (Sentry DSN, Google Analytics ID)
  • URL-адреса API
  • Флаги функциональности
  • Публичные конфигурации (Stripe Publishable Key)

MUST

MUST: Проверять финальный бандл на наличие случайно утекших секретов

MUST: Использовать серверные переменные (без префикса NEXT_PUBLIC_) для секретов

Примеры

Запрещенные практики

# ❌ FORBIDDEN: Секретные ключи в клиентских переменных

# .env.local
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx # Опасно!
NEXT_PUBLIC_DATABASE_PASSWORD=xxx # Критически опасно!
NEXT_PUBLIC_JWT_SECRET=xxx # Критически опасно!
NEXT_PUBLIC_OPENAI_API_KEY=sk-xxx # Опасно!
NEXT_PUBLIC_AWS_SECRET_ACCESS_KEY=xxx # Критически опасно!
// ❌ FORBIDDEN: Использование серверных переменных на клиенте
'use client';

function PaymentForm() {
// Если добавить NEXT_PUBLIC_ префикс - секрет утечет в бандл!
const secretKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY; // Опасно!

return <form>...</form>;
}

Правильный подход

# ✅ Правильно: Разделение публичных и секретных переменных

# .env.local

# Клиентские (публичные) переменные - попадают в бандл
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx # OK
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx # OK
NEXT_PUBLIC_API_URL=https://api.example.com # OK
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX # OK
NEXT_PUBLIC_APP_VERSION=1.2.3 # OK

# Серверные (секретные) переменные - только на сервере
STRIPE_SECRET_KEY=sk_live_xxx # Только на сервере
DATABASE_URL=postgresql://xxx # Только на сервере
JWT_SECRET=xxx # Только на сервере
OPENAI_API_KEY=sk-xxx # Только на сервере
AWS_SECRET_ACCESS_KEY=xxx # Только на сервере
// ✅ Правильно: Публичные ключи на клиенте
'use client';

import { loadStripe } from '@stripe/stripe-js';

function PaymentForm() {
// Publishable key безопасен для клиента
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!;

return (
<Elements stripe={loadStripe(publishableKey)}>
<CheckoutForm />
</Elements>
);
}
// ✅ Правильно: Секретные ключи только на сервере
// app/api/payment/route.ts (Server-side)
import Stripe from 'stripe';

export async function POST(request: Request) {
// Secret key доступен только на сервере
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16'
});

const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'usd'
});

return Response.json({ clientSecret: paymentIntent.client_secret });
}

Валидация переменных окружения

// ✅ MUST: Валидация env-переменных при старте приложения
// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
// Клиентские переменные
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
NEXT_PUBLIC_GA_ID: z.string().startsWith('G-').optional(),

// Серверные переменные (доступны только на сервере)
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
OPENAI_API_KEY: z.string().startsWith('sk-')
});

export const env = envSchema.parse(process.env);

// Использование
import { env } from '@/lib/env';

// На сервере:
const stripe = new Stripe(env.STRIPE_SECRET_KEY);

// На клиенте (через публичную переменную):
const apiUrl = env.NEXT_PUBLIC_API_URL;

Проверка на утечку секретов в бандле

# ✅ SHOULD: Проверка финального бандла на секреты

# После сборки проверьте бандл:
npm run build

# Поиск потенциальных секретов в .next/static
grep -r "sk_live" .next/static/ # Stripe secret keys
grep -r "sk-" .next/static/ # OpenAI keys
grep -r "Bearer " .next/static/ # API tokens
grep -r "postgresql://" .next/static/ # Database URLs

# Если что-то найдено - это ошибка!

Работа с разными окружениями

# .env.local (не коммитится)
NEXT_PUBLIC_API_URL=http://localhost:3001
DATABASE_URL=postgresql://localhost:5432/dev

# .env.production (не коммитится, используется на сервере)
NEXT_PUBLIC_API_URL=https://api.production.com
DATABASE_URL=postgresql://production-db:5432/prod

# .env.example (коммитится, шаблон для других разработчиков)
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
STRIPE_SECRET_KEY=sk_test_your_key_here
JWT_SECRET=your_jwt_secret_here

Частые ошибки

// ❌ Ошибка 1: Использование клиентской переменной для секрета
const apiKey = process.env.NEXT_PUBLIC_API_KEY; // Утечет в бандл!

// ✅ Правильно: API ключ на сервере
// app/api/data/route.ts
export async function GET() {
const apiKey = process.env.API_KEY; // Только на сервере
const data = await fetch('https://api.service.com', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
return Response.json(data);
}
// ❌ Ошибка 2: Передача серверной переменной в клиентский компонент
// app/page.tsx (Server Component)
export default function Page() {
const secret = process.env.JWT_SECRET; // На сервере - OK

// Передаем в Client Component - ОПАСНО!
return <ClientComponent secret={secret} />; // Утечет в HTML!
}

// ✅ Правильно: Не передавать секреты в клиентские компоненты
export default function Page() {
// Используем секрет только на сервере
const data = await validateToken(process.env.JWT_SECRET);

// Передаем только результат
return <ClientComponent data={data} />;
}
// ❌ Ошибка 3: Логирование переменных окружения
console.log('All env vars:', process.env); // Может утечь в логи!

// ✅ Правильно: Не логировать env-переменные
// Или использовать whitelist для безопасных переменных
const safeEnvVars = {
NODE_ENV: process.env.NODE_ENV,
API_URL: process.env.NEXT_PUBLIC_API_URL
};
console.log('Safe env vars:', safeEnvVars);

Таблица классификации переменных

ПеременнаяТипПрефиксГде использовать
Stripe Publishable KeyПубличныйNEXT_PUBLIC_Клиент
Stripe Secret KeyСекрет-Только сервер
API URLПубличныйNEXT_PUBLIC_Клиент и сервер
Database URLСекрет-Только сервер
JWT SecretСекрет-Только сервер
Sentry DSNПубличныйNEXT_PUBLIC_Клиент
Google Analytics IDПубличныйNEXT_PUBLIC_Клиент
OpenAI API KeyСекрет-Только сервер
Feature FlagsПубличныйNEXT_PUBLIC_Клиент

📌 Ключевые моменты:

  • Все переменные с префиксом NEXT_PUBLIC_ попадают в клиентский бандл
  • FORBIDDEN: Секреты в клиентских переменных
  • MUST: Валидация env-переменных при старте
  • SHOULD: Проверка бандла на утечку секретов
  • Разделяйте публичные и секретные переменные