Переменные окружения (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: Проверка бандла на утечку секретов
- Разделяйте публичные и секретные переменные