Skip to content

Environment Configuration

Environment Variables Schema

typescript
// infrastructure/config/env.schema.ts
import { z } from "zod";

export const envSchema = z.object({
  // Application
  NODE_ENV: z.enum(["development", "staging", "production"]),
  PORT: z.coerce.number().default(3000),
  API_PREFIX: z.string().default("api/v1"),

  // Database
  DATABASE_URL: z.string().url(),
  DATABASE_POOL_MIN: z.coerce.number().default(2),
  DATABASE_POOL_MAX: z.coerce.number().default(10),

  // Redis (optional)
  REDIS_URL: z.string().url().optional(),

  // JWT
  JWT_SECRET: z.string().min(32),
  JWT_ACCESS_EXPIRES_IN: z.string().default("15m"),
  JWT_REFRESH_EXPIRES_IN: z.string().default("7d"),

  // Payments (Stripe)
  STRIPE_SECRET_KEY: z.string().optional(),
  STRIPE_WEBHOOK_SECRET: z.string().optional(),

  // Payments (YooKassa)
  YOOKASSA_SHOP_ID: z.string().optional(),
  YOOKASSA_SECRET_KEY: z.string().optional(),

  // Notifications
  SENDGRID_API_KEY: z.string().optional(),
  SENDGRID_FROM_EMAIL: z.string().email().optional(),
  TWILIO_ACCOUNT_SID: z.string().optional(),
  TWILIO_AUTH_TOKEN: z.string().optional(),
  TWILIO_FROM_NUMBER: z.string().optional(),

  // Storage
  S3_BUCKET: z.string().optional(),
  S3_REGION: z.string().optional(),
  S3_ACCESS_KEY: z.string().optional(),
  S3_SECRET_KEY: z.string().optional(),

  // Frontend URL (for CORS)
  FRONTEND_URL: z.string().url(),

  // Feature flags
  FEATURE_INVESTMENTS_ENABLED: z.coerce.boolean().default(true),
  FEATURE_ECOMMERCE_ENABLED: z.coerce.boolean().default(true),
});

export type Env = z.infer<typeof envSchema>;

Configuration Service

typescript
// infrastructure/config/config.service.ts
@Injectable()
export class ConfigService {
  private readonly env: Env;

  constructor() {
    const result = envSchema.safeParse(process.env);

    if (!result.success) {
      const errors = result.error.flatten().fieldErrors;
      console.error("Invalid environment variables:");
      Object.entries(errors).forEach(([key, messages]) => {
        console.error(`  ${key}: ${messages?.join(", ")}`);
      });
      process.exit(1);
    }

    this.env = result.data;
  }

  get<K extends keyof Env>(key: K): Env[K] {
    return this.env[key];
  }

  get isProduction(): boolean {
    return this.env.NODE_ENV === "production";
  }

  get isDevelopment(): boolean {
    return this.env.NODE_ENV === "development";
  }

  get database() {
    return {
      url: this.env.DATABASE_URL,
      poolMin: this.env.DATABASE_POOL_MIN,
      poolMax: this.env.DATABASE_POOL_MAX,
    };
  }

  get jwt() {
    return {
      secret: this.env.JWT_SECRET,
      accessExpiresIn: this.env.JWT_ACCESS_EXPIRES_IN,
      refreshExpiresIn: this.env.JWT_REFRESH_EXPIRES_IN,
    };
  }

  get payments() {
    return {
      stripe: this.env.STRIPE_SECRET_KEY
        ? {
            secretKey: this.env.STRIPE_SECRET_KEY,
            webhookSecret: this.env.STRIPE_WEBHOOK_SECRET,
          }
        : null,
      yookassa: this.env.YOOKASSA_SHOP_ID
        ? {
            shopId: this.env.YOOKASSA_SHOP_ID,
            secretKey: this.env.YOOKASSA_SECRET_KEY,
          }
        : null,
    };
  }
}

Example .env Files

Development (.env.example)

bash
# .env.example (commit this)
NODE_ENV=development
PORT=3000
API_PREFIX=api/v1

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/iwm_dev

# JWT (generate with: openssl rand -base64 32)
JWT_SECRET=your-secret-here-min-32-chars
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Frontend
FRONTEND_URL=http://localhost:5173

# Optional integrations (leave empty in dev)
STRIPE_SECRET_KEY=
YOOKASSA_SHOP_ID=
SENDGRID_API_KEY=

Production (.env.production)

bash
# .env.production (DO NOT commit, use secrets manager)
NODE_ENV=production
DATABASE_URL=postgresql://...  # From secrets manager
JWT_SECRET=...                  # From secrets manager
STRIPE_SECRET_KEY=sk_live_...   # From secrets manager