import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import ms, { StringValue } from 'ms'; @Injectable() export class EnvironmentService { constructor(private configService: ConfigService) {} getNodeEnv(): string { return this.configService.get('NODE_ENV', 'development'); } isDevelopment(): boolean { return this.getNodeEnv() === 'development'; } getAppUrl(): string { const rawUrl = this.configService.get('APP_URL') || `http://localhost:${this.getPort()}`; const { origin } = new URL(rawUrl); return origin; } isHttps(): boolean { const appUrl = this.configService.get('APP_URL'); try { const url = new URL(appUrl); return url.protocol === 'https:'; } catch (error) { return false; } } getSubdomainHost(): string { return this.configService.get('SUBDOMAIN_HOST'); } getPort(): number { return parseInt(this.configService.get('PORT', '3000')); } getAppSecret(): string { return this.configService.get('APP_SECRET'); } getDatabaseURL(): string { return this.configService.get('DATABASE_URL'); } getDatabaseMaxPool(): number { return parseInt(this.configService.get('DATABASE_MAX_POOL', '10')); } getRedisUrl(): string { return this.configService.get( 'REDIS_URL', 'redis://localhost:6379', ); } getJwtTokenExpiresIn(): string { return this.configService.get('JWT_TOKEN_EXPIRES_IN', '90d'); } getCookieExpiresIn(): Date { const expiresInStr = this.getJwtTokenExpiresIn(); let msUntilExpiry: number; try { msUntilExpiry = ms(expiresInStr as StringValue); } catch (err) { msUntilExpiry = ms('90d'); } return new Date(Date.now() + msUntilExpiry); } getGotenbergUrl(): string | undefined { return this.configService.get('GOTENBERG_URL'); } getStorageDriver(): string { return this.configService.get('STORAGE_DRIVER', 'local'); } getFileUploadSizeLimit(): string { return this.configService.get('FILE_UPLOAD_SIZE_LIMIT', '50mb'); } getFileImportSizeLimit(): string { return this.configService.get('FILE_IMPORT_SIZE_LIMIT', '200mb'); } getAwsS3AccessKeyId(): string { return this.configService.get('AWS_S3_ACCESS_KEY_ID'); } getAwsS3SecretAccessKey(): string { return this.configService.get('AWS_S3_SECRET_ACCESS_KEY'); } getAwsS3Region(): string { return this.configService.get('AWS_S3_REGION'); } getAwsS3Bucket(): string { return this.configService.get('AWS_S3_BUCKET'); } getAwsS3Endpoint(): string { return this.configService.get('AWS_S3_ENDPOINT'); } getAwsS3ForcePathStyle(): boolean { return this.configService.get('AWS_S3_FORCE_PATH_STYLE'); } getAwsS3Url(): string { return this.configService.get('AWS_S3_URL'); } getMailDriver(): string { return this.configService.get('MAIL_DRIVER', 'log'); } getMailFromAddress(): string { return this.configService.get('MAIL_FROM_ADDRESS'); } getMailFromName(): string { return this.configService.get('MAIL_FROM_NAME', 'Docmost'); } getSmtpHost(): string { return this.configService.get('SMTP_HOST'); } getSmtpPort(): number { return parseInt(this.configService.get('SMTP_PORT')); } getSmtpSecure(): boolean { const secure = this.configService .get('SMTP_SECURE', 'false') .toLowerCase(); return secure === 'true'; } getSmtpIgnoreTLS(): boolean { const ignoretls = this.configService .get('SMTP_IGNORETLS', 'false') .toLowerCase(); return ignoretls === 'true'; } getSmtpUsername(): string { return this.configService.get('SMTP_USERNAME'); } getSmtpPassword(): string { return this.configService.get('SMTP_PASSWORD'); } getPostmarkToken(): string { return this.configService.get('POSTMARK_TOKEN'); } getDrawioUrl(): string { return this.configService.get('DRAWIO_URL'); } isCloud(): boolean { const cloudConfig = this.configService .get('CLOUD', 'false') .toLowerCase(); return cloudConfig === 'true'; } isSelfHosted(): boolean { return !this.isCloud(); } getStripePublishableKey(): string { return this.configService.get('STRIPE_PUBLISHABLE_KEY'); } getStripeSecretKey(): string { return this.configService.get('STRIPE_SECRET_KEY'); } getStripeWebhookSecret(): string { return this.configService.get('STRIPE_WEBHOOK_SECRET'); } getBillingTrialDays(): number { return parseInt(this.configService.get('BILLING_TRIAL_DAYS', '14')); } getCollabUrl(): string { return this.configService.get('COLLAB_URL'); } isCollabDisableRedis(): boolean { const isStandalone = this.configService .get('COLLAB_DISABLE_REDIS', 'false') .toLowerCase(); return isStandalone === 'true'; } isDisableTelemetry(): boolean { const disable = this.configService .get('DISABLE_TELEMETRY', 'false') .toLowerCase(); return disable === 'true'; } getPostHogHost(): string { return this.configService.get('POSTHOG_HOST'); } getPostHogKey(): string { return this.configService.get('POSTHOG_KEY'); } getSearchDriver(): string { return this.configService .get('SEARCH_DRIVER', 'database') .toLowerCase(); } getTypesenseUrl(): string { return this.configService .get('TYPESENSE_URL', 'http://localhost:8108') .toLowerCase(); } getTypesenseApiKey(): string { return this.configService.get('TYPESENSE_API_KEY'); } getTypesenseLocale(): string { return this.configService .get('TYPESENSE_LOCALE', 'en') .toLowerCase(); } getAiDriver(): string { return this.configService.get('AI_DRIVER'); } getAiEmbeddingModel(): string { return this.configService.get('AI_EMBEDDING_MODEL'); } getAiCompletionModel(): string { return this.configService.get('AI_COMPLETION_MODEL'); } getAiChatModel(): string { return ( this.configService.get('AI_CHAT_MODEL') || this.configService.get('AI_COMPLETION_MODEL') ); } getAiEmbeddingDimension(): number { return parseInt( this.configService.get('AI_EMBEDDING_DIMENSION'), 10, ); } getAiEmbeddingSupportsMrl(): boolean | undefined { const val = this.configService.get('AI_EMBEDDING_SUPPORTS_MRL'); if (val === undefined || val === null || val === '') return undefined; return val === 'true'; } getOpenAiApiKey(): string { return this.configService.get('OPENAI_API_KEY'); } getOpenAiApiUrl(): string { return this.configService.get('OPENAI_API_URL'); } getGeminiApiKey(): string { return this.configService.get('GEMINI_API_KEY'); } getOllamaApiUrl(): string { return this.configService.get( 'OLLAMA_API_URL', 'http://localhost:11434', ); } getEventStoreDriver(): string { return this.configService .get('EVENT_STORE_DRIVER', 'postgres') .toLowerCase(); } getClickHouseUrl(): string { return this.configService.get('CLICKHOUSE_URL'); } getBaseQueryCacheEnabled(): boolean { const enabled = this.configService .get('BASE_QUERY_CACHE_ENABLED', 'false') .toLowerCase(); return enabled === 'true'; } getBaseQueryCacheMinRows(): number { return parseInt( this.configService.get('BASE_QUERY_CACHE_MIN_ROWS', '25000'), 10, ); } getBaseQueryCacheMaxCollections(): number { // Default is intentionally low (50) because a single-node self-host with // ~100 MB per collection can pin ~5 GB RSS at the cap. SaaS/larger // deployments can raise via env. return parseInt( this.configService.get('BASE_QUERY_CACHE_MAX_COLLECTIONS', '50'), 10, ); } getBaseQueryCacheWarmTopN(): number { return parseInt( this.configService.get('BASE_QUERY_CACHE_WARM_TOP_N', '50'), 10, ); } getBaseQueryCacheDebug(): boolean { return ( this.configService .get('BASE_QUERY_CACHE_DEBUG', 'false') .toLowerCase() === 'true' ); } getBaseQueryCacheTrace(): boolean { return ( this.configService .get('BASE_QUERY_CACHE_TRACE', 'false') .toLowerCase() === 'true' ); } getBaseQueryCacheMemoryLimit(): string { // Per-DuckDB-instance memory ceiling. DuckDB accepts human-readable sizes: // '256MB', '1GB', etc. Default 512MB is sized for bases up to ~300K rows // with moderate schemas without spilling. DuckDB automatically spills // to `temp_directory` when this is exceeded, so over-allocating is // cheap — the risk is under-sizing. return this.configService.get( 'BASE_QUERY_CACHE_MEMORY_LIMIT', '512MB', ); } getBaseQueryCacheTempDirectory(): string { // Directory DuckDB uses to spill pages when an instance exceeds its // memory_limit. Defaults to the system temp dir plus a namespace so // different processes don't collide. Setting this explicitly is what // enables spill-to-disk on `:memory:` instances — without it, DuckDB // OOMs at memory_limit instead of paging. const defaultPath = `${require('node:os').tmpdir()}/docmost-duckdb-cache`; return this.configService.get( 'BASE_QUERY_CACHE_TEMP_DIR', defaultPath, ); } getBaseQueryCacheThreads(): number { // Per-DuckDB-instance thread budget. Defaults to 2 so multiple concurrent // instances don't fight for every core on a shared host. return parseInt( this.configService.get('BASE_QUERY_CACHE_THREADS', '2'), 10, ); } getBaseQueryCacheReaderPoolSize(): number { // Number of reader connections held open against the shared DuckDB // instance. Reads are dispatched via `withReader()` which checks out a // connection, runs the query, returns it. Bigger pool = more concurrent // reads without serialization, at the cost of per-connection overhead // (each connection carries its own catalog snapshot + prepared-statement // cache ~= 300 KB). // // Default 4 matches libuv's default thread-pool size. Raise to 8+ if // you see p99 list latency correlate with concurrent request volume. return parseInt( this.configService.get('BASE_QUERY_CACHE_READER_POOL_SIZE', '4'), 10, ); } }