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

Environment

Любая переменная с префиксом NEXT_PUBLIC_, VITE_ и аналогами попадает в клиентский бандл и доступна в браузере любому пользователю.

Правила

MUST

  • Использовать серверные переменные (без публичного префикса) для секретов
  • Проверять финальный бандл на наличие случайно утекших секретов
  • Валидировать env-переменные при старте приложения

FORBIDDEN

Хранить в клиентских переменных (NEXT_PUBLIC_*, VITE_*):

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

MAY

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

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

Примеры

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

# ❌ FORBIDDEN: Секретные ключи в клиентских переменных
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx # Опасно!
NEXT_PUBLIC_JWT_SECRET=xxx # Критически опасно!
NEXT_PUBLIC_DATABASE_PASSWORD=xxx # Критически опасно!
NEXT_PUBLIC_OPENAI_API_KEY=sk-xxx # Опасно!
// ❌ FORBIDDEN: Передача серверной переменной в клиентский компонент
// app/page.tsx (Server Component)
export default function Page() {
const secret = process.env.JWT_SECRET; // На сервере — OK
return <ClientComponent secret={secret} />; // Утечёт в HTML!
}

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

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

# ✅ Серверные (секретные) — только на сервере
STRIPE_SECRET_KEY=sk_live_xxx
DATABASE_URL=postgresql://xxx
JWT_SECRET=xxx
OPENAI_API_KEY=sk-xxx
// ✅ Секретные ключи только на сервере
// app/api/payment/route.ts
import Stripe from 'stripe';

export async function POST(request: Request) {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16'
});
// ...
}

// ✅ Правильно: использовать только результат на клиенте
export default function Page() {
const data = await validateToken(process.env.JWT_SECRET); // Только сервер
return <ClientComponent data={data} />; // Передаём только результат
}

Валидация переменных при старте

// ✅ 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_'),
});

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

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

# ✅ SHOULD: Проверка после сборки
npm run build

grep -r "sk_live" .next/static/ # Stripe secret keys
grep -r "sk-" .next/static/ # OpenAI keys
grep -r "postgresql://" .next/static/ # Database URLs

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

Автоматизация

MUST: Pre-commit hook — запрет коммита .env файлов

# lefthook.yml
pre-commit:
commands:
no-env-files:
run: |
if git diff --cached --name-only | grep -E '\.env($|\.)'; then
echo "❌ Попытка закоммитить .env файл! Удалите его из индекса."
exit 1
fi

Пример конфигурации: lefthook.yml

# ✅ .env файлы всегда в .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore

SHOULD: .env.example как шаблон

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

Таблица классификации

ПеременнаяТипПрефиксГде использовать
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_Клиент