From 7435ff6da625d2b8d20658fe7f86016e92cf1256 Mon Sep 17 00:00:00 2001 From: imeepos Date: Wed, 17 Jun 2026 01:58:24 -0700 Subject: [PATCH] Remove dotenv and inject runtime env --- bun.lock | 1 - server/experiments/experimentEnv.ts | 1 - server/package.json | 1 - server/register.ts | 1 - server/src/cron/cronInit.ts | 3 +- server/src/db/dbUtils.ts | 3 +- server/src/db/initDrizzle.ts | 17 ++- server/src/db/pgHealthMonitor.ts | 3 +- server/src/db/pgPoolMonitor.ts | 5 +- server/src/db/redactDatabaseUrl.ts | 7 +- server/src/external/ai/initAi.ts | 5 +- server/src/external/autumn/autumnCli.ts | 19 ++-- server/src/external/autumn/autumnRpcCli.ts | 9 +- .../external/autumn/autumnWebhookRouter.ts | 3 +- .../src/external/aws/ecs/awsTaskIdentity.ts | 7 +- server/src/external/aws/ecs/onAwsEcs.ts | 3 +- .../aws/eventbridge/eventBridgeUtils.ts | 7 +- .../aws/eventbridge/initEventBridge.ts | 7 +- server/src/external/aws/s3/adminS3Config.ts | 5 +- server/src/external/axiom/initAxiom.ts | 5 +- server/src/external/connect/connectUtils.ts | 5 +- server/src/external/connect/initStripeCli.ts | 18 +-- .../connect/registerConnectWebhook.ts | 3 +- server/src/external/hatchet/initHatchet.ts | 3 +- .../infisical/fetchInfisicalSecrets.ts | 11 +- .../src/external/infisical/initInfisical.ts | 27 ++--- server/src/external/logtail/logtailUtils.ts | 34 ++++-- .../redis/getReachableDragonflyUrl.ts | 5 +- server/src/external/redis/initRedisV2.ts | 7 +- .../redis/initUtils/createRedisClient.ts | 7 +- .../redis/initUtils/redisClientRegistry.ts | 7 +- .../external/redis/initUtils/redisConfig.ts | 11 +- .../external/redis/initUtils/redisV2Config.ts | 3 +- server/src/external/resend/loopsUtils.ts | 5 +- server/src/external/resend/resendUtils.ts | 3 +- server/src/external/resend/safeResend.ts | 3 +- .../misc/registerRevenuecatWebhook.ts | 7 +- .../revenueCat/misc/revenuecatOAuth.ts | 11 +- .../stripe/handleStripeWebhookEvent.ts | 3 +- .../external/stripe/stripeOnboardingUtils.ts | 4 +- .../utils/lockStripeSubscriptionUtils.ts | 3 +- .../releaseScheduleIfLastPhase.ts | 3 +- .../stripeConnectSeederMiddleware.ts | 9 +- .../stripeIdempotencyMiddleware.ts | 3 +- .../stripeLegacySeederMiddleware.ts | 5 +- .../stripeSyncMiddleware.ts | 3 +- .../external/supabase/createSupabaseClient.ts | 5 +- server/src/external/svix/svixHelpers.ts | 3 +- server/src/external/svix/svixUtils.ts | 5 +- .../src/external/tinybird/initClickhouse.ts | 5 +- .../src/external/tinybird/initTinybirdV2.ts | 5 +- .../migrationItemEventsDataSource.ts | 5 +- server/src/external/tinybird/tinybirdUtils.ts | 5 +- server/src/external/vercel/misc/vercelAuth.ts | 3 +- .../external/vercel/misc/vercelSdkOptions.ts | 5 +- server/src/honoMiddlewares/baseMiddleware.ts | 5 +- .../honoMiddlewares/errorSkipMiddleware.ts | 3 +- .../honoMiddlewares/rateLimitMiddleware.ts | 7 +- .../requestLogging/logRequestResult.ts | 5 +- .../responseFilterMiddleware.ts | 3 +- server/src/honoMiddlewares/traceMiddleware.ts | 3 +- server/src/honoUtils/HonoEnv.ts | 1 + server/src/honoUtils/handleReadyCheck.ts | 3 +- server/src/index.ts | 10 +- server/src/init.ts | 9 +- server/src/initHono.ts | 8 +- server/src/instrumentation.ts | 16 +-- .../internal/admin/handleSlackAdminChat.ts | 3 +- .../admin/handleUpsertSlackMcpOAuthClient.ts | 3 +- .../src/internal/api/events/EventService.ts | 5 +- .../internal/auth/oauth/atmnOAuthClients.ts | 3 +- .../auth/oauth/internalMcpOAuthClients.ts | 3 +- .../auth/oauth/oauthAccessTokenApiKey.ts | 3 +- server/src/internal/auth/oauth/oauthRouter.ts | 3 +- .../internal/balances/track/runAsyncTrack.ts | 3 +- .../internal/balances/track/runBatchTrack.ts | 3 +- .../balances/track/utils/queueTrack.ts | 3 +- .../deductionV2/executeRedisDeductionV2.ts | 3 +- .../queueRefreshEntityAggregate.ts | 3 +- .../internal/billing/attach/handleAttach.ts | 9 +- .../billing/v2/handlers/handleAttachV2.ts | 3 +- .../v2/handlers/handleCreateSchedule.ts | 9 +- .../billing/v2/handlers/handleMultiAttach.ts | 3 +- .../v2/handlers/handleUpdateSubscription.ts | 3 +- ...lidateStripeSubscriptionActionOwnership.ts | 5 +- server/src/internal/chat/chatUtils.ts | 15 +-- .../handlers/handleConfirmCheckout.ts | 3 +- .../customers/cancel/handleCancelV2.ts | 3 +- .../batchDeleteCachedFullCustomers.ts | 3 +- server/src/internal/emails/constants.ts | 5 +- .../internal/emails/sendInvitationEmail.ts | 3 +- server/src/internal/emails/sendOTPEmail.ts | 3 +- .../src/internal/invoices/InvoiceService.ts | 3 +- .../migrationSteps/migrateCustomers.ts | 4 - .../v2/handlers/handleRunMigration.ts | 3 +- .../logs/logMigrateCustomerResult.ts | 3 +- .../misc/edgeConfig/edgeConfigStore.ts | 3 +- .../misc/feedback/handleSubmitFeedback.ts | 3 +- .../misc/pricingAgent/pricingAgentRouter.ts | 11 +- .../misc/rateLimiter/rateLimitConfigs.ts | 3 +- server/src/internal/misc/trmnl/trmnlRouter.ts | 3 +- .../orgs/handlers/handleGetUploadUrl.ts | 3 +- .../handlers/handleResetDefaultAccount.ts | 3 +- .../handleGetRevenueCatOAuthUrl.ts | 7 +- .../handleRevenueCatOAuthCallback.ts | 3 +- .../stripeHandlers/handleDeleteStripe.ts | 5 +- .../stripeHandlers/handleGetOAuthUrl.ts | 13 ++- .../stripeHandlers/handleOAuthCallback.ts | 3 +- .../orgs/orgUtils/createConnectAccount.ts | 1 - .../handlers/handleGetPlatformOAuth.ts | 7 +- .../platformBeta/platformBetaRouter.ts | 3 +- .../productUtils/detectProductVariant.ts | 3 +- server/src/node.ts | 9 +- .../blueGreen/blueGreenReadinessChecks.ts | 8 +- .../hatchetWorkflows/createWorkflowTask.ts | 3 +- server/src/queue/initSqs.ts | 13 ++- server/src/queue/initWorkers.ts | 11 +- server/src/queue/processMessage.ts | 3 +- server/src/queue/queueUtils.ts | 3 +- server/src/routers/chatProxyRouter.ts | 5 +- server/src/routers/internalRouter.ts | 3 +- server/src/sentry.ts | 5 +- server/src/trigger/configureTrigger.ts | 5 +- .../src/trigger/utils/createTriggerContext.ts | 3 +- server/src/utils/auth.ts | 56 +++++----- .../utils/authUtils/afterSessionCreated.ts | 3 +- .../utils/authUtils/afterSessionDeleted.ts | 3 +- server/src/utils/constants.ts | 4 +- server/src/utils/corsOrigins.ts | 3 +- server/src/utils/encryptUtils.ts | 3 +- server/src/utils/envUtils.ts | 103 +++++++++++++++--- server/src/utils/initUtils.ts | 21 ++-- server/src/utils/logging/initLogger.ts | 15 +-- server/src/utils/memoryMonitor.ts | 3 +- .../src/utils/otel/FilteringSpanProcessor.ts | 3 +- server/src/utils/posthog.ts | 7 +- server/src/utils/scriptUtils/scriptUtils.ts | 6 +- server/src/workers.ts | 8 +- server/tests/clearMasterOrg.ts | 4 - server/tests/setup/v2Features.ts | 4 - .../unit/logging/init-logger-env.test.ts | 50 +++++++++ .../tests/utils/browserPool/browserConfig.ts | 1 - server/tests/utils/stripeUtils.ts | 2 - server/worker-configuration.d.ts | 46 ++++---- server/wrangler.jsonc | 1 + 145 files changed, 636 insertions(+), 402 deletions(-) create mode 100644 server/tests/unit/logging/init-logger-env.test.ts diff --git a/bun.lock b/bun.lock index 55c4e460c..d25ad8880 100644 --- a/bun.lock +++ b/bun.lock @@ -514,7 +514,6 @@ "date-fns": "^4.1.0", "decimal.js": "^10.5.0", "detect-content-type": "^1.2.0", - "dotenv": "^16.5.0", "drizzle-orm": "catalog:", "express": "^4.21.1", "express-rate-limit": "^7.5.1", diff --git a/server/experiments/experimentEnv.ts b/server/experiments/experimentEnv.ts index c53bd00ec..ed4c76502 100644 --- a/server/experiments/experimentEnv.ts +++ b/server/experiments/experimentEnv.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { loadLocalEnv } from "../src/utils/envUtils"; loadLocalEnv(); diff --git a/server/package.json b/server/package.json index d3532192c..b6e930465 100644 --- a/server/package.json +++ b/server/package.json @@ -115,7 +115,6 @@ "date-fns": "^4.1.0", "decimal.js": "^10.5.0", "detect-content-type": "^1.2.0", - "dotenv": "^16.5.0", "drizzle-orm": "catalog:", "express": "^4.21.1", "express-rate-limit": "^7.5.1", diff --git a/server/register.ts b/server/register.ts index df0fc8eec..22f8352e4 100644 --- a/server/register.ts +++ b/server/register.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { loadLocalEnv } from "./src/utils/envUtils"; import Stripe from "stripe"; import { diff --git a/server/src/cron/cronInit.ts b/server/src/cron/cronInit.ts index 96bcf28cc..c1dadc12b 100644 --- a/server/src/cron/cronInit.ts +++ b/server/src/cron/cronInit.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import "../sentry.ts"; import { CronJob } from "cron"; import { initDrizzle } from "../db/initDrizzle.js"; @@ -37,7 +38,7 @@ const logCronHeartbeat = () => { }; const main = async () => { - if (process.env.DISABLE_CRON === "true") { + if (runtimeEnv.DISABLE_CRON === "true") { console.log(`Cron disabled!`); return; } diff --git a/server/src/db/dbUtils.ts b/server/src/db/dbUtils.ts index fc0ef5fe0..aeeacb65a 100644 --- a/server/src/db/dbUtils.ts +++ b/server/src/db/dbUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { getTableColumns, type SQL, sql } from "drizzle-orm"; import type { PgTable } from "drizzle-orm/pg-core"; @@ -115,7 +116,7 @@ export const isConnectionDropError = ({ }; /** Throws if the connection string looks like a production database. Single source of truth for this check. */ -export const assertNotProductionDb = (url = process.env.DATABASE_URL || "") => { +export const assertNotProductionDb = (url = runtimeEnv.DATABASE_URL || "") => { if (url.includes("us-east-2")) { throw new Error( "Refusing to run against production database (connection string contains us-east-2)", diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts index e024552f5..81191abb6 100644 --- a/server/src/db/initDrizzle.ts +++ b/server/src/db/initDrizzle.ts @@ -1,7 +1,4 @@ -import dotenv from "dotenv"; - -dotenv.config(); - +import { runtimeEnv } from "@/utils/envUtils.js"; import { schemas as schema } from "@autumn/shared"; import { instrumentDrizzleClient } from "@kubiks/otel-drizzle"; @@ -58,8 +55,8 @@ export const initDrizzle = ({ name?: string; } = {}) => { const envDbUrl = replica - ? process.env.DATABASE_REPLICA_URL - : process.env.DATABASE_URL; + ? runtimeEnv.DATABASE_REPLICA_URL + : runtimeEnv.DATABASE_URL; const dbUrl = databaseUrl || envDbUrl || ""; @@ -96,7 +93,7 @@ export const initDrizzle = ({ }; // Strict latency limits in prod; relaxed locally so dev pool warm-up doesn't kill tests. -const isProd = process.env.NODE_ENV === "production"; +const isProd = runtimeEnv.NODE_ENV === "production"; const poolMaxFromEnv = ({ envVar, @@ -105,7 +102,7 @@ const poolMaxFromEnv = ({ envVar: string; fallback: number; }): number => { - const parsed = Number(process.env[envVar]); + const parsed = Number(runtimeEnv[envVar]); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; }; @@ -151,7 +148,7 @@ export const { db: dbCritical, client: clientCritical } = initDrizzle({ name: "critical", maxConnections: criticalPoolMax, connectTimeout: isProd ? 2 : 30, - databaseUrl: process.env.DATABASE_CRITICAL_URL, + databaseUrl: runtimeEnv.DATABASE_CRITICAL_URL, poolConfig: { application_name: "autumn-critical", query_timeout: isProd ? 2_000 : 30_000, @@ -169,7 +166,7 @@ export const { db: dbGeneral, client: clientGeneral } = initDrizzle({ // -- Replica pool: used as fallback when primary is degraded -- // Only created if DATABASE_REPLICA_URL is configured. -const replicaResult = process.env.DATABASE_REPLICA_URL +const replicaResult = runtimeEnv.DATABASE_REPLICA_URL ? initDrizzle({ name: "replica", replica: true, diff --git a/server/src/db/pgHealthMonitor.ts b/server/src/db/pgHealthMonitor.ts index 3494b40ec..5799ed528 100644 --- a/server/src/db/pgHealthMonitor.ts +++ b/server/src/db/pgHealthMonitor.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { SQL } from "drizzle-orm"; import type { Pool } from "pg"; import { logger } from "@/external/logtail/logtailUtils.js"; @@ -243,7 +244,7 @@ export const getPgHealthState = (): { failureCount, probeActive: probeInterval !== null, firstProbeSuccessAt, - hasReplica: !!process.env.DATABASE_REPLICA_URL, + hasReplica: !!runtimeEnv.DATABASE_REPLICA_URL, }); /** Force DEGRADED state (for testing). Does NOT start the recovery probe. */ diff --git a/server/src/db/pgPoolMonitor.ts b/server/src/db/pgPoolMonitor.ts index 21aed6622..6504904b6 100644 --- a/server/src/db/pgPoolMonitor.ts +++ b/server/src/db/pgPoolMonitor.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { Pool } from "pg"; import { logger } from "@/external/logtail/logtailUtils.js"; @@ -11,8 +12,8 @@ const registry = new Map(); let snapshotInterval: ReturnType | null = null; const getRole = (): string => { - if (process.env.WORKER === "true") return "worker"; - if (process.env.CRON === "true") return "cron"; + if (runtimeEnv.WORKER === "true") return "worker"; + if (runtimeEnv.CRON === "true") return "cron"; return "http"; }; diff --git a/server/src/db/redactDatabaseUrl.ts b/server/src/db/redactDatabaseUrl.ts index 7f7cadca0..1228a0bad 100644 --- a/server/src/db/redactDatabaseUrl.ts +++ b/server/src/db/redactDatabaseUrl.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createHash } from "node:crypto"; const hash = (value: string) => @@ -33,9 +34,9 @@ export const redactDatabaseUrl = (databaseUrl?: string) => { }; export const getRedactedDatabaseUrls = () => ({ - primary: redactDatabaseUrl(process.env.DATABASE_URL), - replica: redactDatabaseUrl(process.env.DATABASE_REPLICA_URL), + primary: redactDatabaseUrl(runtimeEnv.DATABASE_URL), + replica: redactDatabaseUrl(runtimeEnv.DATABASE_REPLICA_URL), critical: redactDatabaseUrl( - process.env.DATABASE_CRITICAL_URL || process.env.DATABASE_URL, + runtimeEnv.DATABASE_CRITICAL_URL || runtimeEnv.DATABASE_URL, ), }); diff --git a/server/src/external/ai/initAi.ts b/server/src/external/ai/initAi.ts index 327a1204f..9ddbf7f67 100644 --- a/server/src/external/ai/initAi.ts +++ b/server/src/external/ai/initAi.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createAnthropic } from "@ai-sdk/anthropic"; -export const anthropicClient = process.env.ANTHROPIC_API_KEY +export const anthropicClient = runtimeEnv.ANTHROPIC_API_KEY ? createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, + apiKey: runtimeEnv.ANTHROPIC_API_KEY, }) : undefined; diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index dbd508657..27a5fdd3b 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -1,8 +1,5 @@ /** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */ -import dotenv from "dotenv"; - -dotenv.config(); - +import { runtimeEnv } from "@/utils/envUtils.js"; import { type ApiBaseEntity, type ApiCusFeatureV3, @@ -93,9 +90,9 @@ export class AutumnInt { liveUrl?: boolean; skipCacheDeletion?: boolean; } = {}) { - // this.apiKey = apiKey || process.env.AUTUMN_API_KEY || ""; + // this.apiKey = apiKey || runtimeEnv.AUTUMN_API_KEY || ""; this.apiKey = - apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || ""; + apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || ""; this.headers = { Authorization: `Bearer ${this.apiKey}`, @@ -110,7 +107,7 @@ export class AutumnInt { this.headers["org-config"] = JSON.stringify(orgConfig); } - const envBase = process.env.AUTUMN_TEST_BASE_URL; + const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL; const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null; this.baseUrl = baseUrl || @@ -316,7 +313,7 @@ export class AutumnInt { Object.keys(headers).length > 0 ? headers : undefined, ); - const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0"); + const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0"); const defaultTimeout = concurrency > 1 ? 5000 : 4000; const finalTimeout = timeout ?? defaultTimeout; if (finalTimeout) { @@ -1219,7 +1216,7 @@ export class AutumnInt { Object.keys(headers).length > 0 ? headers : undefined, ); - const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0"); + const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0"); const defaultTimeout = concurrency > 1 ? 5000 : 4000; const finalTimeout = timeout ?? defaultTimeout; if (finalTimeout) { @@ -1257,7 +1254,7 @@ export class AutumnInt { ): Promise => { const data = await this.post(`/billing.create_schedule`, params); - const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0"); + const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0"); const defaultTimeout = concurrency > 1 ? 5000 : 4000; const finalTimeout = timeout ?? defaultTimeout; if (finalTimeout) { @@ -1288,7 +1285,7 @@ export class AutumnInt { Object.keys(headers).length > 0 ? headers : undefined, ); - const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0"); + const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0"); const defaultTimeout = concurrency > 1 ? 5000 : 4000; const finalTimeout = timeout ?? defaultTimeout; if (finalTimeout) { diff --git a/server/src/external/autumn/autumnRpcCli.ts b/server/src/external/autumn/autumnRpcCli.ts index 4055f7f7c..3e100d8e4 100644 --- a/server/src/external/autumn/autumnRpcCli.ts +++ b/server/src/external/autumn/autumnRpcCli.ts @@ -1,8 +1,5 @@ /** biome-ignore-all lint/suspicious/noExplicitAny: RPC test client needs flexible payload typing */ -import dotenv from "dotenv"; - -dotenv.config(); - +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, type OrgConfig } from "@autumn/shared"; import AutumnError from "./autumnCli.js"; @@ -27,7 +24,7 @@ export class AutumnRpcCli { liveUrl?: boolean; } = {}) { this.apiKey = - apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || ""; + apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || ""; this.headers = { Authorization: `Bearer ${this.apiKey}`, @@ -42,7 +39,7 @@ export class AutumnRpcCli { this.headers["org-config"] = JSON.stringify(orgConfig); } - const envBase = process.env.AUTUMN_TEST_BASE_URL; + const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL; const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null; this.baseUrl = baseUrl || diff --git a/server/src/external/autumn/autumnWebhookRouter.ts b/server/src/external/autumn/autumnWebhookRouter.ts index a5f111081..77c7bf769 100644 --- a/server/src/external/autumn/autumnWebhookRouter.ts +++ b/server/src/external/autumn/autumnWebhookRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode } from "@autumn/shared"; import { Hono } from "hono"; import { Webhook } from "svix"; @@ -17,7 +18,7 @@ const verifyAutumnWebhook = async ({ svixSignature: string | undefined; }; }) => { - const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!); + const wh = new Webhook(runtimeEnv.AUTUMN_WEBHOOK_SECRET!); const { svixId, svixTimestamp, svixSignature } = headers; diff --git a/server/src/external/aws/ecs/awsTaskIdentity.ts b/server/src/external/aws/ecs/awsTaskIdentity.ts index e16b0d7cb..4ec9de2de 100644 --- a/server/src/external/aws/ecs/awsTaskIdentity.ts +++ b/server/src/external/aws/ecs/awsTaskIdentity.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { z } from "zod/v4"; /** @@ -55,9 +56,9 @@ export const resolveAwsTaskIdentity = async (): Promise => { identityPromise = (async (): Promise => { const imageSha = - process.env.FC_GIT_COMMIT_SHA || process.env.IMAGE_TAG || null; + runtimeEnv.FC_GIT_COMMIT_SHA || runtimeEnv.IMAGE_TAG || null; - const metadataUri = process.env.ECS_CONTAINER_METADATA_URI_V4; + const metadataUri = runtimeEnv.ECS_CONTAINER_METADATA_URI_V4; let serviceArn: string | null = null; if (metadataUri) { @@ -94,7 +95,7 @@ export const resolveAwsTaskIdentity = async (): Promise => { `[awsTaskIdentity] ECS metadata fetch failed: ${error instanceof Error ? error.message : error}; gate will fail open`, ); } - } else if (process.env.NODE_ENV === "production") { + } else if (runtimeEnv.NODE_ENV === "production") { console.warn( "[awsTaskIdentity] ECS_CONTAINER_METADATA_URI_V4 unset in production — gate will fail open", ); diff --git a/server/src/external/aws/ecs/onAwsEcs.ts b/server/src/external/aws/ecs/onAwsEcs.ts index 4204523bd..679ff8ffc 100644 --- a/server/src/external/aws/ecs/onAwsEcs.ts +++ b/server/src/external/aws/ecs/onAwsEcs.ts @@ -5,5 +5,6 @@ * so it's the canonical "am I on AWS?" gate — same one * `awsTaskIdentity` uses to discover the running service. */ +import { runtimeEnv } from "@/utils/envUtils.js"; export const onAwsEcs = (): boolean => - Boolean(process.env.ECS_CONTAINER_METADATA_URI_V4); + Boolean(runtimeEnv.ECS_CONTAINER_METADATA_URI_V4); diff --git a/server/src/external/aws/eventbridge/eventBridgeUtils.ts b/server/src/external/aws/eventbridge/eventBridgeUtils.ts index fca452363..2a34564b0 100644 --- a/server/src/external/aws/eventbridge/eventBridgeUtils.ts +++ b/server/src/external/aws/eventbridge/eventBridgeUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { CreateScheduleCommand, DeleteScheduleCommand, @@ -8,14 +9,14 @@ import { extractLocalEndpoint } from "@/queue/initSqs.js"; import { schedulerClient } from "./initEventBridge.js"; const isLocalQueue = (): boolean => - !!extractLocalEndpoint({ queueUrl: process.env.SQS_QUEUE_URL_V2 }); + !!extractLocalEndpoint({ queueUrl: runtimeEnv.SQS_QUEUE_URL_V2 }); const SCHEDULE_GROUP = "default"; -const SCHEDULER_ROLE_ARN = process.env.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || ""; +const SCHEDULER_ROLE_ARN = runtimeEnv.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || ""; /** Derives SQS ARN from URL: https://sqs..amazonaws.com// -> arn:aws:sqs::: */ const getSqsQueueArn = (): string => { - const url = process.env.SQS_QUEUE_URL_V2 || ""; + const url = runtimeEnv.SQS_QUEUE_URL_V2 || ""; const match = url.match( /^https:\/\/sqs\.([a-z0-9-]+)\.amazonaws\.com\/(\d+)\/(.+)$/, ); diff --git a/server/src/external/aws/eventbridge/initEventBridge.ts b/server/src/external/aws/eventbridge/initEventBridge.ts index a28e6f05c..9e8bd9192 100644 --- a/server/src/external/aws/eventbridge/initEventBridge.ts +++ b/server/src/external/aws/eventbridge/initEventBridge.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { SchedulerClient } from "@aws-sdk/client-scheduler"; import { DEFAULT_AWS_REGION, @@ -7,11 +8,11 @@ import { const getSchedulerClientConfig = () => ({ region: extractRegionFromQueueUrl({ - queueUrl: process.env.SQS_QUEUE_URL_V2, + queueUrl: runtimeEnv.SQS_QUEUE_URL_V2, }) || DEFAULT_AWS_REGION, credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || "", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "", + accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "", + secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "", }, }); diff --git a/server/src/external/aws/s3/adminS3Config.ts b/server/src/external/aws/s3/adminS3Config.ts index 274084d80..1a8e6edb6 100644 --- a/server/src/external/aws/s3/adminS3Config.ts +++ b/server/src/external/aws/s3/adminS3Config.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json"; export const ADMIN_ROLLOUT_CONFIG_KEY = "admin/rollout-config.json"; export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json"; @@ -21,8 +22,8 @@ export const BLUE_GREEN_CRON_ACTIVE_SLOT_KEY = "admin/blue-green-cron-active-slot.json"; export const BLUE_GREEN_HEARTBEAT_KEY_PREFIX = "admin/blue-green-heartbeats"; -const bucket = process.env.S3_BUCKET || "autumn-prod-server"; -const region = process.env.S3_REGION || "us-east-2"; +const bucket = runtimeEnv.S3_BUCKET || "autumn-prod-server"; +const region = runtimeEnv.S3_REGION || "us-east-2"; export const getAdminS3Config = () => { return { diff --git a/server/src/external/axiom/initAxiom.ts b/server/src/external/axiom/initAxiom.ts index d6445f156..bc7d3479a 100644 --- a/server/src/external/axiom/initAxiom.ts +++ b/server/src/external/axiom/initAxiom.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { Axiom } from "@axiomhq/js"; -const AXIOM_ADMIN_TOKEN = process.env.AXIOM_ADMIN_TOKEN; -const AXIOM_ORG_ID = process.env.AXIOM_ORG_ID; +const AXIOM_ADMIN_TOKEN = runtimeEnv.AXIOM_ADMIN_TOKEN; +const AXIOM_ORG_ID = runtimeEnv.AXIOM_ORG_ID; export const axiomClient: Axiom | null = AXIOM_ADMIN_TOKEN ? new Axiom({ diff --git a/server/src/external/connect/connectUtils.ts b/server/src/external/connect/connectUtils.ts index 78ff00458..d95a15e42 100644 --- a/server/src/external/connect/connectUtils.ts +++ b/server/src/external/connect/connectUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, InternalError, type Organization } from "@autumn/shared"; import type { DrizzleCli } from "@server/db/initDrizzle.js"; import { OrgService } from "@server/internal/orgs/OrgService.js"; @@ -42,8 +43,8 @@ export const deauthorizeAccount = async ({ await masterStripe.oauth.deauthorize({ client_id: env === AppEnv.Live - ? process.env.STRIPE_LIVE_CLIENT_ID || "" - : process.env.STRIPE_SANDBOX_CLIENT_ID || "", + ? runtimeEnv.STRIPE_LIVE_CLIENT_ID || "" + : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "", stripe_user_id: accountId, }); logger.info(`Deauthorized account ${accountId} for ${env}`); diff --git a/server/src/external/connect/initStripeCli.ts b/server/src/external/connect/initStripeCli.ts index b3691e561..4878c9275 100644 --- a/server/src/external/connect/initStripeCli.ts +++ b/server/src/external/connect/initStripeCli.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, InternalError, @@ -6,7 +7,6 @@ import { } from "@autumn/shared"; import { decryptData } from "@server/utils/encryptUtils.js"; import { instrumentStripe } from "@server/utils/otel/instrumentStripe.js"; -import "dotenv/config"; import type { DrizzleCli } from "@server/db/initDrizzle.js"; import Stripe from "stripe"; import { @@ -25,19 +25,19 @@ export const initMasterStripe = (params?: { let secretKey: string; if (params?.env === AppEnv.Live) { - if (!process.env.STRIPE_LIVE_SECRET_KEY) { + if (!runtimeEnv.STRIPE_LIVE_SECRET_KEY) { throw new InternalError({ message: "STRIPE_LIVE_SECRET_KEY env variable is not set", }); } - secretKey = process.env.STRIPE_LIVE_SECRET_KEY; + secretKey = runtimeEnv.STRIPE_LIVE_SECRET_KEY; } else { - if (!process.env.STRIPE_SANDBOX_SECRET_KEY) { + if (!runtimeEnv.STRIPE_SANDBOX_SECRET_KEY) { throw new InternalError({ message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set", }); } - secretKey = process.env.STRIPE_SANDBOX_SECRET_KEY; + secretKey = runtimeEnv.STRIPE_SANDBOX_SECRET_KEY; } const cacheKey = buildMasterCacheKey({ @@ -56,7 +56,9 @@ export const initMasterStripe = (params?: { ? ("2025-02-24.acacia" as any) : undefined, }); - return params?.skipInstrumentation ? client : instrumentStripe({ client }); + return params?.skipInstrumentation + ? client + : instrumentStripe({ client }); }, }); }; @@ -136,9 +138,9 @@ export const getStripeWebhookSecret = async ({ let secret: string; if (env === AppEnv.Live) { - secret = process.env.STRIPE_LIVE_WEBHOOK_SECRET || ""; + secret = runtimeEnv.STRIPE_LIVE_WEBHOOK_SECRET || ""; } else { - secret = process.env.STRIPE_SANDBOX_WEBHOOK_SECRET || ""; + secret = runtimeEnv.STRIPE_SANDBOX_WEBHOOK_SECRET || ""; } if (!secret) { diff --git a/server/src/external/connect/registerConnectWebhook.ts b/server/src/external/connect/registerConnectWebhook.ts index 1b794e733..8c54a5ce5 100644 --- a/server/src/external/connect/registerConnectWebhook.ts +++ b/server/src/external/connect/registerConnectWebhook.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; @@ -15,7 +16,7 @@ export const registerConnectWebhook = async ({ const stripeCli = initPlatformStripe({ masterOrg: org, env }); const curWebhookEndpoints = await stripeCli.webhookEndpoints.list(); - const backendUrl = process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL; + const backendUrl = runtimeEnv.SERVER_URL || runtimeEnv.STRIPE_WEBHOOK_URL; const webhookUrl = `${backendUrl}/webhooks/connect/${env}?org_id=${org.id}`; diff --git a/server/src/external/hatchet/initHatchet.ts b/server/src/external/hatchet/initHatchet.ts index fb27adcca..6d2a903f1 100644 --- a/server/src/external/hatchet/initHatchet.ts +++ b/server/src/external/hatchet/initHatchet.ts @@ -1,5 +1,6 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1"; -export const isHatchetEnabled = !!process.env.HATCHET_CLIENT_TOKEN; +export const isHatchetEnabled = !!runtimeEnv.HATCHET_CLIENT_TOKEN; export const hatchet = isHatchetEnabled ? HatchetClient.init() : null; diff --git a/server/src/external/infisical/fetchInfisicalSecrets.ts b/server/src/external/infisical/fetchInfisicalSecrets.ts index 1ca76e2d8..83b19c9c8 100644 --- a/server/src/external/infisical/fetchInfisicalSecrets.ts +++ b/server/src/external/infisical/fetchInfisicalSecrets.ts @@ -3,9 +3,10 @@ * time via `syncEnvVars` to push secrets to the cloud env. Kept SDK-free * so trigger.config.ts can import it without bloating the build. * - * Runtime code uses `initInfisical` (SDK-based, populates process.env). + * Runtime code uses `initInfisical` (SDK-based, populates runtimeEnv). */ +import { runtimeEnv } from "@/utils/envUtils.js"; export type InfisicalSyncEnvVar = { name: string; value: string }; export type FetchInfisicalSecretsArgs = { @@ -106,10 +107,10 @@ export const fetchInfisicalSecretsFromEnv = ( ctxEnv: Record = {}, ): Promise => fetchInfisicalSecrets({ - clientId: process.env.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID, + clientId: runtimeEnv.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID, clientSecret: - process.env.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET, - projectId: process.env.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID, + runtimeEnv.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET, + projectId: runtimeEnv.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID, environment: - process.env.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT, + runtimeEnv.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT, }); diff --git a/server/src/external/infisical/initInfisical.ts b/server/src/external/infisical/initInfisical.ts index 0c6977767..c0e05a895 100644 --- a/server/src/external/infisical/initInfisical.ts +++ b/server/src/external/infisical/initInfisical.ts @@ -1,18 +1,19 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { InfisicalSDK } from "@infisical/sdk"; import { loadLocalEnv } from "@/utils/envUtils.js"; import { mask } from "@/utils/genUtils"; /** - * Initialize Infisical and load secrets into process.env - * This allows all existing code using process.env to work seamlessly + * Initialize Infisical and load secrets into runtimeEnv + * This allows all existing code using runtimeEnv to work seamlessly */ export const initInfisical = async (params?: { secretPath?: string }) => { loadLocalEnv(); // Only initialize if credentials are provided - const clientId = process.env.INFISICAL_CLIENT_ID; - const clientSecret = process.env.INFISICAL_CLIENT_SECRET; - const projectId = process.env.INFISICAL_PROJECT_ID; - const environment = process.env.INFISICAL_ENVIRONMENT; + const clientId = runtimeEnv.INFISICAL_CLIENT_ID; + const clientSecret = runtimeEnv.INFISICAL_CLIENT_SECRET; + const projectId = runtimeEnv.INFISICAL_PROJECT_ID; + const environment = runtimeEnv.INFISICAL_ENVIRONMENT; if (!clientId || !clientSecret || !projectId || !environment) { console.log("⚠️ Infisical credentials not found - skipping initialization"); @@ -37,8 +38,8 @@ export const initInfisical = async (params?: { secretPath?: string }) => { recursive: true, }); - // Load secrets into process.env - // Note: Existing process.env variables take precedence (won't be overridden) + // Load secrets into runtimeEnv + // Note: Existing runtimeEnv variables take precedence (won't be overridden) let loadedCount = 0; for (const secret of allSecrets.secrets) { @@ -48,22 +49,22 @@ export const initInfisical = async (params?: { secretPath?: string }) => { `Retrieving restricted secret: ${secret.secretKey}, Path: ${secret.secretPath}, value: ${mask(secret.secretValue, 3, 2)}`, ); } - if (!process.env[secret.secretKey]) { - process.env[secret.secretKey] = secret.secretValue; + if (!runtimeEnv[secret.secretKey]) { + runtimeEnv[secret.secretKey] = secret.secretValue; loadedCount++; } } for (const importSecrets of allSecrets?.imports ?? []) { for (const importSecret of importSecrets.secrets) { - if (!process.env[importSecret.secretKey]) { - process.env[importSecret.secretKey] = importSecret.secretValue; + if (!runtimeEnv[importSecret.secretKey]) { + runtimeEnv[importSecret.secretKey] = importSecret.secretValue; loadedCount++; } } } - console.log(`✅ Infisical: loaded ${loadedCount} secrets into process.env`); + console.log(`✅ Infisical: loaded ${loadedCount} secrets into runtimeEnv`); } catch (error) { console.error("❌ Failed to initialize Infisical:", error); throw error; diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts index d110eb816..5a374e2d6 100644 --- a/server/src/external/logtail/logtailUtils.ts +++ b/server/src/external/logtail/logtailUtils.ts @@ -1,9 +1,8 @@ -import "dotenv/config"; - import type pino from "pino"; import { initLogger } from "@/utils/logging/initLogger"; +import { runtimeEnv } from "@/utils/envUtils.js"; -const pinoLogger = initLogger(); +const pinoLogger = initLogger({}, runtimeEnv); const createLogMethod = (pinoMethod: any, logtailMethod?: any) => { function rewriteAppPath(str: string) { @@ -73,7 +72,10 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => { }; }; -const createLoggerStructure = (basePinoLogger: pino.Logger): Logger => ({ +const createLoggerStructure = ( + basePinoLogger: pino.Logger, + env: Env, +): Logger => ({ debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)), info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)), warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)), @@ -85,16 +87,20 @@ const createLoggerStructure = (basePinoLogger: pino.Logger): Logger => ({ context: any; onlyProd?: boolean; }) => { - if (onlyProd && process.env.NODE_ENV !== "production") { - return createLoggerStructure(basePinoLogger); + if (onlyProd && (env.NODE_ENV as string | undefined) !== "production") { + return createLoggerStructure(basePinoLogger, env); } const childPinoLogger = basePinoLogger.child(context); - return createLoggerStructure(childPinoLogger); + return createLoggerStructure(childPinoLogger, env); }, }); -export const createLogger = () => createLoggerStructure(pinoLogger); +export const createLogger = (env: Env) => + createLoggerStructure( + env === runtimeEnv ? pinoLogger : initLogger({}, env), + env, + ); /** * Lazy dual-output logger (stdout JSON + axiom). Used only by long-running @@ -102,12 +108,16 @@ export const createLogger = () => createLoggerStructure(pinoLogger); * our axiom store. Default `logger` / `createLogger` are unaffected. */ let dualPinoLogger: pino.Logger | null = null; -export const createDualLogger = () => { - if (!dualPinoLogger) dualPinoLogger = initLogger({ mode: "dual" }); - return createLoggerStructure(dualPinoLogger); +export const createDualLogger = (env: Env) => { + if (env !== runtimeEnv) { + return createLoggerStructure(initLogger({ mode: "dual" }, env), env); + } + if (!dualPinoLogger) + dualPinoLogger = initLogger({ mode: "dual" }, runtimeEnv); + return createLoggerStructure(dualPinoLogger, runtimeEnv); }; -export const logger = createLogger(); +export const logger = createLogger(runtimeEnv); export type Logger = { debug: (...args: any[]) => void; info: (...args: any[]) => void; diff --git a/server/src/external/redis/getReachableDragonflyUrl.ts b/server/src/external/redis/getReachableDragonflyUrl.ts index 588426f32..91209485f 100644 --- a/server/src/external/redis/getReachableDragonflyUrl.ts +++ b/server/src/external/redis/getReachableDragonflyUrl.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js"; /** @@ -20,10 +21,10 @@ import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js"; export const getReachableDragonflyUrl = (url: string): string => { if (onAwsEcs()) return url; - const privateUrl = process.env.CACHE_V2_DRAGONFLY_URL?.trim(); + const privateUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim(); if (!privateUrl || url.trim() !== privateUrl) return url; - const publicUrl = process.env.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim(); + const publicUrl = runtimeEnv.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim(); if (!publicUrl) return url; return publicUrl; diff --git a/server/src/external/redis/initRedisV2.ts b/server/src/external/redis/initRedisV2.ts index 208558cdb..ea31f3af5 100644 --- a/server/src/external/redis/initRedisV2.ts +++ b/server/src/external/redis/initRedisV2.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { Redis } from "ioredis"; import { logger } from "@/external/logtail/logtailUtils.js"; import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js"; @@ -12,7 +13,7 @@ import { supportsUpstashShebangForRedisV2, } from "./initUtils/redisV2Config.js"; -const rawDragonflyUrl = process.env.CACHE_V2_DRAGONFLY_URL?.trim(); +const rawDragonflyUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim(); const dragonflyUrl = rawDragonflyUrl ? getReachableDragonflyUrl(rawDragonflyUrl) : undefined; @@ -27,8 +28,8 @@ export const redisV2: Redis = createRedisConnection({ }); const alternateInstanceUrls: Partial> = { - upstash: process.env.CACHE_V2_UPSTASH_URL?.trim() || undefined, - redis: process.env.CACHE_V2_REDIS_URL?.trim() || undefined, + upstash: runtimeEnv.CACHE_V2_UPSTASH_URL?.trim() || undefined, + redis: runtimeEnv.CACHE_V2_REDIS_URL?.trim() || undefined, dragonfly: dragonflyUrl, }; diff --git a/server/src/external/redis/initUtils/createRedisClient.ts b/server/src/external/redis/initUtils/createRedisClient.ts index 098cd4d49..18b25ff8f 100644 --- a/server/src/external/redis/initUtils/createRedisClient.ts +++ b/server/src/external/redis/initUtils/createRedisClient.ts @@ -1,10 +1,11 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { Redis } from "ioredis"; import { instrumentRedis } from "../otel/instrumentRedis.js"; import { cacheBackupUrl } from "./redisConfig.js"; import { registerRedisCommands } from "./registerRedisCommands.js"; const REDIS_COMMAND_TIMEOUT_MS = - process.env.NODE_ENV === "production" ? 10_000 : 60_000; + runtimeEnv.NODE_ENV === "production" ? 10_000 : 60_000; const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => { try { @@ -36,8 +37,8 @@ export const createRedisClient = ({ const instance = new Redis(cacheUrl, { tls: - process.env.CACHE_CERT && !cacheBackupUrl - ? { ca: process.env.CACHE_CERT } + runtimeEnv.CACHE_CERT && !cacheBackupUrl + ? { ca: runtimeEnv.CACHE_CERT } : undefined, family: 4, keepAlive: 10000, diff --git a/server/src/external/redis/initUtils/redisClientRegistry.ts b/server/src/external/redis/initUtils/redisClientRegistry.ts index 620cef5f8..31656a91b 100644 --- a/server/src/external/redis/initUtils/redisClientRegistry.ts +++ b/server/src/external/redis/initUtils/redisClientRegistry.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { Redis } from "ioredis"; import { createDisabledRedis, createRedisClient } from "./createRedisClient.js"; import { @@ -8,12 +9,14 @@ import { primaryCacheUrl, } from "./redisConfig.js"; -if (process.env.CACHE_BACKUP_URL?.trim()) { +if (runtimeEnv.CACHE_BACKUP_URL?.trim()) { console.log( `[Redis] Using CACHE_BACKUP_URL for all regions (primary region: ${currentRegion})`, ); } else if (!hasRedisConfig) { - console.warn("[Redis] No Redis URL configured. Running in Postgres-only mode."); + console.warn( + "[Redis] No Redis URL configured. Running in Postgres-only mode.", + ); } else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion })) { console.log(`Using regional cache: ${currentRegion}`); } diff --git a/server/src/external/redis/initUtils/redisConfig.ts b/server/src/external/redis/initUtils/redisConfig.ts index 8b8df529b..724a18fd2 100644 --- a/server/src/external/redis/initUtils/redisConfig.ts +++ b/server/src/external/redis/initUtils/redisConfig.ts @@ -1,4 +1,5 @@ // Region constants +import { runtimeEnv } from "@/utils/envUtils.js"; const REGION_US_EAST_2 = "us-east-2"; const REGION_US_WEST_2 = "us-west-2"; @@ -6,9 +7,9 @@ const REGION_US_WEST_2 = "us-west-2"; const ALL_REGIONS = [REGION_US_EAST_2, REGION_US_WEST_2] as const; // Current region this instance is running in -export const currentRegion = process.env.AWS_REGION || REGION_US_WEST_2; +export const currentRegion = runtimeEnv.AWS_REGION || REGION_US_WEST_2; -export const cacheBackupUrl = process.env.CACHE_BACKUP_URL?.trim(); +export const cacheBackupUrl = runtimeEnv.CACHE_BACKUP_URL?.trim(); // Map of region to cache URL. When CACHE_BACKUP_URL is set, all regions use it // (failover / single backup endpoint). @@ -18,12 +19,12 @@ const regionToCacheUrl: Record = cacheBackupUrl [REGION_US_WEST_2]: cacheBackupUrl, } : { - [REGION_US_EAST_2]: process.env.CACHE_URL_US_EAST, - [REGION_US_WEST_2]: process.env.CACHE_URL, + [REGION_US_EAST_2]: runtimeEnv.CACHE_URL_US_EAST, + [REGION_US_WEST_2]: runtimeEnv.CACHE_URL, }; export const primaryCacheUrl = - regionToCacheUrl[currentRegion] || process.env.CACHE_URL || cacheBackupUrl; + regionToCacheUrl[currentRegion] || runtimeEnv.CACHE_URL || cacheBackupUrl; export const hasRedisConfig = Boolean(primaryCacheUrl); /** Get all regions that have configured cache URLs */ diff --git a/server/src/external/redis/initUtils/redisV2Config.ts b/server/src/external/redis/initUtils/redisV2Config.ts index ae8d44506..7283138aa 100644 --- a/server/src/external/redis/initUtils/redisV2Config.ts +++ b/server/src/external/redis/initUtils/redisV2Config.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js"; export const REDIS_V2_COMMAND_TIMEOUT_MS = - process.env.NODE_ENV === "production" ? 1_000 : 10_000; + runtimeEnv.NODE_ENV === "production" ? 1_000 : 10_000; export const getRedisV2ConnectionConfig = ({ cacheV2Url, diff --git a/server/src/external/resend/loopsUtils.ts b/server/src/external/resend/loopsUtils.ts index 07cf9de2b..867d6b2ce 100644 --- a/server/src/external/resend/loopsUtils.ts +++ b/server/src/external/resend/loopsUtils.ts @@ -1,13 +1,14 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { User } from "better-auth"; import { LoopsClient } from "loops"; import { logger } from "../logtail/logtailUtils.js"; const createLoopsCli = () => { - return new LoopsClient(process.env.LOOPS_API_KEY || ""); + return new LoopsClient(runtimeEnv.LOOPS_API_KEY || ""); }; export const createLoopsContact = async (user: User) => { - if (!process.env.LOOPS_API_KEY) return; + if (!runtimeEnv.LOOPS_API_KEY) return; try { const email = user.email; diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts index 76c894ba3..bcf3a284c 100644 --- a/server/src/external/resend/resendUtils.ts +++ b/server/src/external/resend/resendUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { Resend } from "resend"; import { logger } from "../logtail/logtailUtils.js"; @@ -11,7 +12,7 @@ interface ResendEmailProps { } export const createResendCli = () => { - return new Resend(process.env.RESEND_API_KEY); + return new Resend(runtimeEnv.RESEND_API_KEY); }; export const sendTextEmail = async ({ diff --git a/server/src/external/resend/safeResend.ts b/server/src/external/resend/safeResend.ts index f6725569f..2e39bc750 100644 --- a/server/src/external/resend/safeResend.ts +++ b/server/src/external/resend/safeResend.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { logger } from "../logtail/logtailUtils.js"; export function safeResend any>({ @@ -8,7 +9,7 @@ export function safeResend any>({ action: string; }): (...args: Parameters) => Promise | undefined> { return async (...args: Parameters) => { - if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) { + if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) { logger.warn( `RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`, ); diff --git a/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts b/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts index 76328996d..12acd64dd 100644 --- a/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts +++ b/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv } from "@autumn/shared"; import type { initRevenuecatCli } from "./initRevenuecatCli.js"; @@ -8,9 +9,9 @@ type RcCli = ReturnType; * can reach a local tunnel); production uses BETTER_AUTH_URL. */ const getServerBaseUrl = (): string | undefined => - process.env.NODE_ENV !== "production" - ? process.env.NGROK_URL - : process.env.BETTER_AUTH_URL; + runtimeEnv.NODE_ENV !== "production" + ? runtimeEnv.NGROK_URL + : runtimeEnv.BETTER_AUTH_URL; export const getRevenuecatWebhookUrl = ({ orgId, diff --git a/server/src/external/revenueCat/misc/revenuecatOAuth.ts b/server/src/external/revenueCat/misc/revenuecatOAuth.ts index b2427bc6b..698f3baca 100644 --- a/server/src/external/revenueCat/misc/revenuecatOAuth.ts +++ b/server/src/external/revenueCat/misc/revenuecatOAuth.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { CodeChallengeMethod, generateCodeVerifier, @@ -48,8 +49,8 @@ export const findMissingRcScopes = (grantedScopes: string[]): string[] => { }; const getRcOAuthClient = () => { - const clientId = process.env.REVENUECAT_OAUTH_CLIENT_ID; - const clientSecret = process.env.REVENUECAT_OAUTH_CLIENT_SECRET; + const clientId = runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID; + const clientSecret = runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET; if (!clientId || !clientSecret) { throw new Error("RevenueCat OAuth client credentials not configured"); @@ -59,10 +60,10 @@ const getRcOAuthClient = () => { }; export const getRcOAuthRedirectUri = () => { - let serverUrl = process.env.BETTER_AUTH_URL; + let serverUrl = runtimeEnv.BETTER_AUTH_URL; - if (process.env.NGROK_URL) { - serverUrl = process.env.NGROK_URL; + if (runtimeEnv.NGROK_URL) { + serverUrl = runtimeEnv.NGROK_URL; } return `${(serverUrl ?? "").replace(/\/+$/, "")}/revenuecat/oauth_callback`; diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts index 9b847a297..7a8654309 100644 --- a/server/src/external/stripe/handleStripeWebhookEvent.ts +++ b/server/src/external/stripe/handleStripeWebhookEvent.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import * as Sentry from "@sentry/bun"; import type { Context } from "hono"; import { Stripe } from "stripe"; @@ -122,7 +123,7 @@ export const handleStripeWebhookEvent = async ( } if ( - process.env.NODE_ENV === "development" && + runtimeEnv.NODE_ENV === "development" && error instanceof Error && error.message.includes("No stripe account linked to organization") ) { diff --git a/server/src/external/stripe/stripeOnboardingUtils.ts b/server/src/external/stripe/stripeOnboardingUtils.ts index 5f6f1d43d..4e71ab389 100644 --- a/server/src/external/stripe/stripeOnboardingUtils.ts +++ b/server/src/external/stripe/stripeOnboardingUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type AppEnv, ErrCode } from "@autumn/shared"; import Stripe from "stripe"; import RecaseError from "@/utils/errorUtils.js"; @@ -20,8 +21,7 @@ export const createWebhookEndpoint = async ( ) => { const stripe = new Stripe(apiKey); - const webhookBaseUrl = - process.env.STRIPE_WEBHOOK_URL || process.env.SERVER_URL; + const webhookBaseUrl = runtimeEnv.STRIPE_WEBHOOK_URL || runtimeEnv.SERVER_URL; if (!webhookBaseUrl) { throw new RecaseError({ diff --git a/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts b/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts index 9ed66e31f..ec80e336d 100644 --- a/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts +++ b/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { getPrimaryRedis } from "@/external/redis/initRedis"; import { tryRedisRead, tryRedisWrite } from "@/utils/cacheUtils/cacheUtils"; @@ -15,7 +16,7 @@ export const setStripeSubscriptionLock = async ({ `sub:${stripeSubscriptionId}`, JSON.stringify({ lockedAtMs }), "EX", - process.env.NODE_ENV === "production" ? 60 : 3, + runtimeEnv.NODE_ENV === "production" ? 60 : 3, ), primaryRedis, ); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts index f4f34d811..0ec9575c6 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createStripeCli } from "@/external/connect/createStripeCli"; import { isStripeSubscriptionScheduleInLastPhase } from "@/external/stripe/subscriptionSchedules/utils/classifyStripeSubscriptionScheduleUtils"; import { stripeSubscriptionScheduleToPhaseIndex } from "@/external/stripe/subscriptionSchedules/utils/convertStripeSubscriptionScheduleUtils"; @@ -73,7 +74,7 @@ export const releaseScheduleIfLastPhase = async ({ return true; } catch (error: unknown) { if (error instanceof Error) { - if (process.env.NODE_ENV === "development") { + if (runtimeEnv.NODE_ENV === "development") { logger.warn( `[handleSchedulePhaseChanges] failed to release schedule: ${error.message}`, ); diff --git a/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts index 94ccf332d..f636bee3d 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type AppEnv, AuthType, @@ -54,8 +55,8 @@ export const stripeConnectSeederMiddleware = async ( const signature = c.req.header("stripe-signature") || ""; const skipVerify = - process.env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" && - process.env.NODE_ENV !== "production"; + runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" && + runtimeEnv.NODE_ENV !== "production"; let event: Stripe.Event; if (skipVerify) { @@ -78,7 +79,7 @@ export const stripeConnectSeederMiddleware = async ( ); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); - if (process.env.NODE_ENV !== "development") { + if (runtimeEnv.NODE_ENV !== "development") { logger.warn(`Webhook verification error: ${message}`); } return c.json({ error: message }, 400); @@ -114,7 +115,7 @@ export const stripeConnectSeederMiddleware = async ( return c.json({ error: "Failed to resolve org for Stripe webhook" }, 500); } - if (process.env.NODE_ENV !== "development") { + if (runtimeEnv.NODE_ENV !== "development") { logger.error( `Account ID ${accountId} not linked to any org, skipping Stripe webhook`, ); diff --git a/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts index e05e2e49f..2403e0059 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { tryCatch } from "@autumn/shared"; import type { Context, Next } from "hono"; import { redis } from "@/external/redis/initRedis"; @@ -16,7 +17,7 @@ export const stripeIdempotencyMiddleware = async ( c: Context, next: Next, ) => { - if (process.env.NODE_ENV === "development") { + if (runtimeEnv.NODE_ENV === "development") { await next(); return; } diff --git a/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts index 54f033f55..d5eaed76f 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type AppEnv, AuthType } from "@autumn/shared"; import type { Context, Next } from "hono"; import Stripe from "stripe"; @@ -54,8 +55,8 @@ export const stripeLegacySeederMiddleware = async ( const signature = c.req.header("stripe-signature") || ""; const skipVerify = - process.env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" && - process.env.NODE_ENV !== "production"; + runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" && + runtimeEnv.NODE_ENV !== "production"; let event: Stripe.Event; if (skipVerify) { diff --git a/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts b/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts index 410de04ca..22e5cbb0b 100644 --- a/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts +++ b/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync"; import type { Context, Next } from "hono"; import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js"; @@ -21,7 +22,7 @@ export const stripeSyncMiddleware = async ( if (!org || !stripeEvent) return; if ( - process.env.NODE_ENV === "production" && + runtimeEnv.NODE_ENV === "production" && !isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug }) ) return; diff --git a/server/src/external/supabase/createSupabaseClient.ts b/server/src/external/supabase/createSupabaseClient.ts index 4624c2397..263fc7c9a 100644 --- a/server/src/external/supabase/createSupabaseClient.ts +++ b/server/src/external/supabase/createSupabaseClient.ts @@ -1,10 +1,11 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createClient } from "@supabase/supabase-js"; export const createSupabaseClient = () => { try { return createClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_SERVICE_KEY!, + runtimeEnv.SUPABASE_URL!, + runtimeEnv.SUPABASE_SERVICE_KEY!, ); } catch (error) { console.error("Error creating Supabase client:", error); diff --git a/server/src/external/svix/svixHelpers.ts b/server/src/external/svix/svixHelpers.ts index e84e4339c..9657da231 100644 --- a/server/src/external/svix/svixHelpers.ts +++ b/server/src/external/svix/svixHelpers.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { AppEnv, Organization } from "@autumn/shared"; import * as Sentry from "@sentry/bun"; import { getSentryTags } from "@/external/sentry/sentryUtils.js"; @@ -53,7 +54,7 @@ export const sendSvixEvent = async ({ idempotencyKey?: string; tags?: string[]; }) => { - if (!process.env.SVIX_API_KEY) return; + if (!runtimeEnv.SVIX_API_KEY) return; const { org, env } = ctx; diff --git a/server/src/external/svix/svixUtils.ts b/server/src/external/svix/svixUtils.ts index 9608fd8a0..4afb94488 100644 --- a/server/src/external/svix/svixUtils.ts +++ b/server/src/external/svix/svixUtils.ts @@ -1,9 +1,10 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, type Organization } from "@autumn/shared"; import { Svix } from "svix"; import { logger } from "../logtail/logtailUtils.js"; export const createSvixCli = () => { - return new Svix(process.env.SVIX_API_KEY as string); + return new Svix(runtimeEnv.SVIX_API_KEY as string); }; export function safeSvix any>({ @@ -14,7 +15,7 @@ export function safeSvix any>({ action: string; }): (...args: Parameters) => Promise | undefined> { return async (...args: Parameters) => { - if (!process.env.SVIX_API_KEY) { + if (!runtimeEnv.SVIX_API_KEY) { logger.warn(`SVIX_API_KEY is not set, skipping ${action}`); return; } diff --git a/server/src/external/tinybird/initClickhouse.ts b/server/src/external/tinybird/initClickhouse.ts index 6e944fdc0..832a81070 100644 --- a/server/src/external/tinybird/initClickhouse.ts +++ b/server/src/external/tinybird/initClickhouse.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type ClickHouseClient, createClient } from "@clickhouse/client"; -const TINYBIRD_CLICKHOUSE_URL = process.env.TINYBIRD_US_EAST_CLICKHOUSE_URL; -const TINYBIRD_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN; +const TINYBIRD_CLICKHOUSE_URL = runtimeEnv.TINYBIRD_US_EAST_CLICKHOUSE_URL; +const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN; if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) { console.log( diff --git a/server/src/external/tinybird/initTinybirdV2.ts b/server/src/external/tinybird/initTinybirdV2.ts index f50dd5b51..20eae0071 100644 --- a/server/src/external/tinybird/initTinybirdV2.ts +++ b/server/src/external/tinybird/initTinybirdV2.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createTinybirdApi } from "@tinybirdco/sdk"; -const TINYBIRD_SECONDARY_API_URL = process.env.TINYBIRD_API_URL; -const TINYBIRD_SECONDARY_TOKEN = process.env.TINYBIRD_TOKEN; +const TINYBIRD_SECONDARY_API_URL = runtimeEnv.TINYBIRD_API_URL; +const TINYBIRD_SECONDARY_TOKEN = runtimeEnv.TINYBIRD_TOKEN; /** Secondary Tinybird API client for dual-write safety net during region cutover. * Reads from the legacy TINYBIRD_API_URL / TINYBIRD_TOKEN env vars (europe-west2 diff --git a/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts index f92235955..364a8d4d1 100644 --- a/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts +++ b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { defineDatasource, defineEndpoint, @@ -9,8 +10,8 @@ import { t, } from "@tinybirdco/sdk"; -const TINYBIRD_US_EAST_API_URL = process.env.TINYBIRD_US_EAST_API_URL; -const TINYBIRD_US_EAST_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN; +const TINYBIRD_US_EAST_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL; +const TINYBIRD_US_EAST_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN; const migrationTinybirdConfig = TINYBIRD_US_EAST_API_URL && TINYBIRD_US_EAST_TOKEN diff --git a/server/src/external/tinybird/tinybirdUtils.ts b/server/src/external/tinybird/tinybirdUtils.ts index 1af3e38e2..f6f55d1f6 100644 --- a/server/src/external/tinybird/tinybirdUtils.ts +++ b/server/src/external/tinybird/tinybirdUtils.ts @@ -1,8 +1,9 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; -const TINYBIRD_API_URL = process.env.TINYBIRD_US_EAST_API_URL; -const TINYBIRD_TOKEN = process.env.TINYBIRD_US_EAST_TOKEN; +const TINYBIRD_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL; +const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN; export type TinybirdConfig = { baseUrl: string; diff --git a/server/src/external/vercel/misc/vercelAuth.ts b/server/src/external/vercel/misc/vercelAuth.ts index 5bb8775ce..023b63508 100644 --- a/server/src/external/vercel/misc/vercelAuth.ts +++ b/server/src/external/vercel/misc/vercelAuth.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, type Organization } from "@autumn/shared"; import { createRemoteJWKSet, jwtVerify } from "jose"; import { JWTExpired, JWTInvalid } from "jose/errors"; @@ -42,7 +43,7 @@ const synthesizeTestClaims = ({ env: AppEnv; testOptions?: VercelOidcTestOptions; }): OidcClaims | null => { - if (process.env.NODE_ENV === "production") return null; + if (runtimeEnv.NODE_ENV === "production") return null; if (testOptions?.allowVercelTestOidc !== true) return null; if (!token.startsWith(TEST_OIDC_PREFIX)) return null; diff --git a/server/src/external/vercel/misc/vercelSdkOptions.ts b/server/src/external/vercel/misc/vercelSdkOptions.ts index 8f7ecf918..d54eaf76d 100644 --- a/server/src/external/vercel/misc/vercelSdkOptions.ts +++ b/server/src/external/vercel/misc/vercelSdkOptions.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; export type VercelSdkTestOptions = { mockVercelApi?: boolean; }; @@ -8,9 +9,9 @@ export type VercelSdkTestOptions = { export const getVercelSdkServerURL = ( testOptions?: VercelSdkTestOptions, ): string | undefined => { - if (process.env.NODE_ENV === "production") return undefined; + if (runtimeEnv.NODE_ENV === "production") return undefined; if (testOptions?.mockVercelApi !== true) return undefined; - const base = process.env.BETTER_AUTH_URL; + const base = runtimeEnv.BETTER_AUTH_URL; if (!base) return undefined; return `${base.replace(/\/$/, "")}/__test/vercel/api`; }; diff --git a/server/src/honoMiddlewares/baseMiddleware.ts b/server/src/honoMiddlewares/baseMiddleware.ts index b44ffd1e6..b7c662da2 100644 --- a/server/src/honoMiddlewares/baseMiddleware.ts +++ b/server/src/honoMiddlewares/baseMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ApiVersionClass, AppEnv, @@ -79,7 +80,7 @@ export const baseMiddleware = async (c: Context, next: Next) => { entity_id: entityId, user_agent: c.req.header("user-agent"), ip_address: c.req.header("x-forwarded-for"), - region: process.env.AWS_REGION, + region: runtimeEnv.AWS_REGION, query: c.req.query(), body: redactSensitiveRequestBody({ body }), @@ -129,7 +130,7 @@ export const baseMiddleware = async (c: Context, next: Next) => { useReplica: c.req.header("x-use-replica") === "true", mockVercelApi: c.req.header("x-mock-vercel-api") === "true", allowVercelTestOidc: - process.env.NODE_ENV !== "production" && + runtimeEnv.NODE_ENV !== "production" && c.req.header("x-allow-vercel-test-oidc") === "true", }, }); diff --git a/server/src/honoMiddlewares/errorSkipMiddleware.ts b/server/src/honoMiddlewares/errorSkipMiddleware.ts index 48d9d865d..1251d0a55 100644 --- a/server/src/honoMiddlewares/errorSkipMiddleware.ts +++ b/server/src/honoMiddlewares/errorSkipMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError } from "@autumn/shared"; import type { Context } from "hono"; import type { ContentfulStatusCode } from "hono/utils/http-status"; @@ -47,7 +48,7 @@ const ROUTE_SPECIFIC_RULES: Array<{ match: (err: Error) => err.message.includes( "STRIPE_WEBHOOK_SECRET env variable is not set (live)", - ) && process.env.NODE_ENV === "development", + ) && runtimeEnv.NODE_ENV === "development", statusCode: 500, }, ]; diff --git a/server/src/honoMiddlewares/rateLimitMiddleware.ts b/server/src/honoMiddlewares/rateLimitMiddleware.ts index 6e8c85cb0..34a8b8fd0 100644 --- a/server/src/honoMiddlewares/rateLimitMiddleware.ts +++ b/server/src/honoMiddlewares/rateLimitMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { Context, Env, Next } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { @@ -24,9 +25,9 @@ export const rateLimitMiddleware = async (c: Context, next: Next) => { if ( rateLimitType === RateLimitType.Attach && - (process.env.NODE_ENV === "development" || - process.env.NODE_ENV === "test") && - ctx.org?.id === process.env.TESTS_ORG_ID + (runtimeEnv.NODE_ENV === "development" || + runtimeEnv.NODE_ENV === "test") && + ctx.org?.id === runtimeEnv.TESTS_ORG_ID ) { return await next(); } diff --git a/server/src/honoMiddlewares/requestLogging/logRequestResult.ts b/server/src/honoMiddlewares/requestLogging/logRequestResult.ts index c5df17764..b91cc50b7 100644 --- a/server/src/honoMiddlewares/requestLogging/logRequestResult.ts +++ b/server/src/honoMiddlewares/requestLogging/logRequestResult.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import chalk from "chalk"; import type { Context } from "hono"; import type { AutumnContext, HonoEnv } from "@/honoUtils/HonoEnv.js"; @@ -14,7 +15,7 @@ const HIGH_VOLUME_SUCCESS_ROUTES = new Set([ ]); const SUCCESS_REQUEST_LOG_SAMPLE_RATE = Number.parseFloat( - process.env.AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE ?? "0", + runtimeEnv.AXIOM_SUCCESS_REQUEST_LOG_SAMPLE_RATE ?? "0", ); const shouldSampleSuccessLog = () => @@ -80,7 +81,7 @@ export const logRequestResult = async ({ if ( Object.keys(ctx.extraLogs).length > 0 && - process.env.NODE_ENV === "development" + runtimeEnv.NODE_ENV === "development" ) { const maskedLogs = maskExtraLogs(ctx.extraLogs); ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`); diff --git a/server/src/honoMiddlewares/responseFilter/responseFilterMiddleware.ts b/server/src/honoMiddlewares/responseFilter/responseFilterMiddleware.ts index 76a332869..152d21462 100644 --- a/server/src/honoMiddlewares/responseFilter/responseFilterMiddleware.ts +++ b/server/src/honoMiddlewares/responseFilter/responseFilterMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AuthType } from "@autumn/shared"; import type { Context, Next } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; @@ -49,7 +50,7 @@ export const responseFilterMiddleware = async ( const ctx = c.get("ctx"); if (ctx?.authType === AuthType.Dashboard) return; const isNonProd = - process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test"; + runtimeEnv.NODE_ENV === "development" || runtimeEnv.NODE_ENV === "test"; if (isNonProd && ctx?.testOptions?.keepInternalFields === true) return; // Only process JSON responses diff --git a/server/src/honoMiddlewares/traceMiddleware.ts b/server/src/honoMiddlewares/traceMiddleware.ts index 61a840d0a..3c02393dd 100644 --- a/server/src/honoMiddlewares/traceMiddleware.ts +++ b/server/src/honoMiddlewares/traceMiddleware.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { context, trace } from "@opentelemetry/api"; import type { MiddlewareHandler } from "hono"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; @@ -28,7 +29,7 @@ export const traceEnrichMiddleware: MiddlewareHandler = async (c, next) => { user_id: ctx.userId || undefined, auth_type: ctx.authType, api_version: ctx.apiVersion?.semver, - region: process.env.AWS_REGION, + region: runtimeEnv.AWS_REGION, full_subject_rollout_enabled: ctx.org ? isFullSubjectRolloutEnabled({ ctx }) : undefined, diff --git a/server/src/honoUtils/HonoEnv.ts b/server/src/honoUtils/HonoEnv.ts index 3fbefb7b5..e9fe9999e 100644 --- a/server/src/honoUtils/HonoEnv.ts +++ b/server/src/honoUtils/HonoEnv.ts @@ -92,6 +92,7 @@ export type RequestContext = { export type AutumnContext = RequestContext; export type HonoEnv = { + Bindings: Env; Variables: { ctx: AutumnContext; validated: boolean; diff --git a/server/src/honoUtils/handleReadyCheck.ts b/server/src/honoUtils/handleReadyCheck.ts index 86845bed8..9c18f931f 100644 --- a/server/src/honoUtils/handleReadyCheck.ts +++ b/server/src/honoUtils/handleReadyCheck.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { timingSafeEqual } from "node:crypto"; import type { Context } from "hono"; import { clientCritical } from "@/db/initDrizzle.js"; @@ -7,7 +8,7 @@ import { getRedisV2Availability } from "@/external/redis/initUtils/redisV2Availa import type { HonoEnv } from "./HonoEnv.js"; const POSTGRES_TIMEOUT_MS = 1_000; -const READY_CHECK_TOKEN = process.env.READY_CHECK_TOKEN?.trim(); +const READY_CHECK_TOKEN = runtimeEnv.READY_CHECK_TOKEN?.trim(); const checkPostgresReady = async () => { try { diff --git a/server/src/index.ts b/server/src/index.ts index 89d1a16c1..6d3a4a9ae 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,10 +1,14 @@ -import { createHonoApp } from "./initHono.js"; +import { setRuntimeEnv } from "@/utils/envUtils.js"; import type { ExecutionContext } from "hono"; +import type { createHonoApp } from "./initHono.js"; -const app = createHonoApp(); +let app: ReturnType | null = null; export default { - fetch(request: Request, env: unknown, executionCtx: ExecutionContext) { + async fetch(request: Request, env: Env, executionCtx: ExecutionContext) { + setRuntimeEnv(env); + const { createHonoApp } = await import("./initHono.js"); + app ??= createHonoApp(env); return app.fetch(request, env, executionCtx); }, }; diff --git a/server/src/init.ts b/server/src/init.ts index 254807594..7e8f851ba 100644 --- a/server/src/init.ts +++ b/server/src/init.ts @@ -1,4 +1,5 @@ // Sentry + OpenTelemetry must be imported before any application code +import { runtimeEnv } from "@/utils/envUtils.js"; await import("./sentry.js"); import cluster from "node:cluster"; @@ -55,7 +56,7 @@ let shuttingDown = false; const init = async ({ startupStartedAt }: { startupStartedAt: number }) => { logger.info(getRedactedDatabaseUrls(), "DB URLs"); - const app = createHonoApp(); + const app = createHonoApp(runtimeEnv as unknown as Env); initPgHealthMonitor({ client: clientCritical }); startPgPoolMonitor(); @@ -72,8 +73,8 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => { startRedisMonitor(); startRedisV2Monitor(); - const PORT = process.env.SERVER_PORT - ? Number.parseInt(process.env.SERVER_PORT) + const PORT = runtimeEnv.SERVER_PORT + ? Number.parseInt(runtimeEnv.SERVER_PORT) : 8080; const requestListener = getRequestListener(app.fetch); @@ -94,7 +95,7 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => { }); }; -if (process.env.NODE_ENV === "development") { +if (runtimeEnv.NODE_ENV === "development") { registerFatalErrorHandlers(); await init({ startupStartedAt: Date.now() }); registerShutdownHandlers(); diff --git a/server/src/initHono.ts b/server/src/initHono.ts index 15badabc1..82198e4d5 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -15,8 +15,8 @@ import { handleReadyCheck } from "./honoUtils/handleReadyCheck.js"; import { handleListAuthOrganizations } from "./internal/auth/handleListAuthOrganizations.js"; import { oauthRouter } from "./internal/auth/oauth/oauthRouter.js"; import { cliRouter } from "./internal/dev/cli/cliRouter.js"; -import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; import { handleRevenueCatOAuthCallback } from "./internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.js"; +import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; import { apiRouter } from "./routers/apiRouter.js"; import { createChatProxyRouter } from "./routers/chatProxyRouter.js"; import { internalRouter } from "./routers/internalRouter.js"; @@ -48,7 +48,7 @@ const ALLOWED_HEADERS = [ "User-Agent", // Required for better-auth v1.4.0+ compatibility with Safari/Zen browser ]; -export const createHonoApp = () => { +export const createHonoApp = (env?: Env) => { const app = new Hono(); app.route("", createChatProxyRouter()); @@ -100,7 +100,7 @@ export const createHonoApp = () => { // Add Render region identifier header for load balancer verification app.use("*", async (c, next) => { await next(); - c.header("x-region", process.env.AWS_REGION); + c.header("x-region", c.env.AWS_REGION); }); // Webhook routes @@ -111,7 +111,7 @@ export const createHonoApp = () => { // Vercel SDK test mock — mounted in dev/test, used only when // `ctx.testOptions.mockVercelApi` points the SDK at this route. - if (process.env.NODE_ENV !== "production") { + if ((env?.NODE_ENV as string | undefined) !== "production") { app.route("/__test/vercel/api", vercelTestApiRouter); } diff --git a/server/src/instrumentation.ts b/server/src/instrumentation.ts index 1192dac41..8cb967a1a 100644 --- a/server/src/instrumentation.ts +++ b/server/src/instrumentation.ts @@ -1,4 +1,4 @@ -import "dotenv/config"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { DiagConsoleLogger, DiagLogLevel, diag } from "@opentelemetry/api"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; @@ -14,14 +14,14 @@ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN); let sdk: NodeSDK | null = null; -if (process.env.AXIOM_TOKEN) { +if (runtimeEnv.AXIOM_TOKEN) { // NodeSDK reads OTEL_SERVICE_NAME to set the service resource attribute - process.env.OTEL_SERVICE_NAME = "autumn-server"; + runtimeEnv.OTEL_SERVICE_NAME = "autumn-server"; const traceExporter = new OTLPTraceExporter({ url: "https://api.axiom.co/v1/traces", headers: { - Authorization: `Bearer ${process.env.AXIOM_TOKEN}`, + Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`, "X-Axiom-Dataset": "otel", }, }); @@ -30,18 +30,18 @@ if (process.env.AXIOM_TOKEN) { // auto-add a BatchSpanProcessor for `traceExporter` when `spanProcessors` // is set. We must wire the exporter processor explicitly. // Dev: short 1s flush for fast feedback. Prod: default 5s for throughput. - const isDev = process.env.NODE_ENV !== "production"; + const isDev = runtimeEnv.NODE_ENV !== "production"; const exportProcessor = new BatchSpanProcessor(traceExporter, { scheduledDelayMillis: isDev ? 1000 : 5000, }); const filteredExportProcessor = new FilteringSpanProcessor(exportProcessor); - const metricReader = process.env.AXIOM_METRICS_DATASET + const metricReader = runtimeEnv.AXIOM_METRICS_DATASET ? new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: "https://api.axiom.co/v1/metrics", headers: { - Authorization: `Bearer ${process.env.AXIOM_TOKEN}`, - "x-axiom-metrics-dataset": process.env.AXIOM_METRICS_DATASET, + Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`, + "x-axiom-metrics-dataset": runtimeEnv.AXIOM_METRICS_DATASET, }, }), exportIntervalMillis: 60_000, diff --git a/server/src/internal/admin/handleSlackAdminChat.ts b/server/src/internal/admin/handleSlackAdminChat.ts index dc8d91325..a522f4800 100644 --- a/server/src/internal/admin/handleSlackAdminChat.ts +++ b/server/src/internal/admin/handleSlackAdminChat.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import crypto, { randomUUID } from "node:crypto"; import { stripOAuthTokenPrefix } from "@autumn/auth"; import { @@ -82,7 +83,7 @@ const getOrgSummary = async ({ const decryptChatCredentialToken = ({ token }: { token: string }) => { const key = crypto .createHash("sha256") - .update(process.env.ENCRYPTION_PASSWORD ?? "") + .update(runtimeEnv.ENCRYPTION_PASSWORD ?? "") .digest(); const buffer = Buffer.from(token, "base64"); if (buffer[0] !== 1) throw new Error("Unsupported encrypted payload"); diff --git a/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts b/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts index 0e818a667..771a7a488 100644 --- a/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts +++ b/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts @@ -1,9 +1,10 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { registerMcpOAuthClient } from "@/internal/auth/actions/index.js"; import { createRoute } from "../../honoMiddlewares/routeHandler"; const getClientUrl = () => - (process.env.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, ""); + (runtimeEnv.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, ""); const getSlackMcpRedirectUris = () => { const clientUrl = getClientUrl(); diff --git a/server/src/internal/api/events/EventService.ts b/server/src/internal/api/events/EventService.ts index f8eed02a6..0ba7b218e 100644 --- a/server/src/internal/api/events/EventService.ts +++ b/server/src/internal/api/events/EventService.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, type EventInsert, events, RecaseError } from "@autumn/shared"; import { and, desc, eq } from "drizzle-orm"; import { StatusCodes } from "http-status-codes"; @@ -14,7 +15,7 @@ export class EventService { event: EventInsert | EventInsert[]; logger?: Logger; }) { - if (process.env.NODE_ENV !== "development") return; + if (runtimeEnv.NODE_ENV !== "development") return; try { const results = await db .insert(events) @@ -48,7 +49,7 @@ export class EventService { env: string; limit?: number; }) { - if (process.env.NODE_ENV === "production") return []; + if (runtimeEnv.NODE_ENV === "production") return []; const results = await db .select({ id: events.id, diff --git a/server/src/internal/auth/oauth/atmnOAuthClients.ts b/server/src/internal/auth/oauth/atmnOAuthClients.ts index 40673e395..56c0c3f44 100644 --- a/server/src/internal/auth/oauth/atmnOAuthClients.ts +++ b/server/src/internal/auth/oauth/atmnOAuthClients.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { oauthClientRepo } from "../repos/index.js"; @@ -5,7 +6,7 @@ const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]); const configuredAtmnClientIds = () => new Set( - (process.env.ATMN_OAUTH_CLIENT_IDS ?? "") + (runtimeEnv.ATMN_OAUTH_CLIENT_IDS ?? "") .split(",") .map((id) => id.trim()) .filter(Boolean), diff --git a/server/src/internal/auth/oauth/internalMcpOAuthClients.ts b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts index f3fec8927..d3ddfb083 100644 --- a/server/src/internal/auth/oauth/internalMcpOAuthClients.ts +++ b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts @@ -1,10 +1,11 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { MCP_CLIENT_KIND } from "@autumn/auth/oauth"; import type { Context } from "hono"; import { type DrizzleCli, db } from "@/db/initDrizzle.js"; import { auth } from "@/utils/auth.js"; import { oauthClientRepo } from "../repos/index.js"; -const INTERNAL_MCP_CLIENT_ID = process.env.INTERNAL_MCP_OAUTH_CLIENT_ID; +const INTERNAL_MCP_CLIENT_ID = runtimeEnv.INTERNAL_MCP_OAUTH_CLIENT_ID; const INTERNAL_MCP_CLIENT_NAME = "Autumn internal-mcp"; const INTERNAL_MCP_CLIENT_NAME_NORMALIZED = INTERNAL_MCP_CLIENT_NAME.toLowerCase(); diff --git a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts index cc356406b..bfe1fbee5 100644 --- a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts +++ b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { stripOAuthTokenPrefix } from "@autumn/auth"; import { AppEnv, @@ -19,7 +20,7 @@ import { isAtmnOAuthClientId } from "./atmnOAuthClients.js"; import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js"; const getOAuthIssuer = () => - `${process.env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`; + `${runtimeEnv.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`; const verifyResourceAccessToken = async ({ accessToken, diff --git a/server/src/internal/auth/oauth/oauthRouter.ts b/server/src/internal/auth/oauth/oauthRouter.ts index 6e514e2fa..5f8e956bc 100644 --- a/server/src/internal/auth/oauth/oauthRouter.ts +++ b/server/src/internal/auth/oauth/oauthRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, @@ -22,7 +23,7 @@ const getClientLookupRateLimitKey = (c: Context) => const oauthClientLookupLimiter = rateLimiter({ windowMs: 60 * 1000, - limit: process.env.NODE_ENV === "development" ? 1000 : 60, + limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 60, standardHeaders: "draft-6", keyGenerator: getClientLookupRateLimitKey, }); diff --git a/server/src/internal/balances/track/runAsyncTrack.ts b/server/src/internal/balances/track/runAsyncTrack.ts index 987a9bb3d..888170a70 100644 --- a/server/src/internal/balances/track/runAsyncTrack.ts +++ b/server/src/internal/balances/track/runAsyncTrack.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError, type TrackParams } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { queueTrack } from "./utils/queueTrack.js"; @@ -12,7 +13,7 @@ export const runAsyncTrack = async ({ ctx: AutumnContext; body: TrackParams; }): Promise => { - const queueUrl = process.env.TRACK_ASYNC_SQS_QUEUE_URL; + const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL; if (!queueUrl) { ctx.logger.error( "[track] async=true requested but TRACK_ASYNC_SQS_QUEUE_URL is unset", diff --git a/server/src/internal/balances/track/runBatchTrack.ts b/server/src/internal/balances/track/runBatchTrack.ts index 091e4e202..81663a470 100644 --- a/server/src/internal/balances/track/runBatchTrack.ts +++ b/server/src/internal/balances/track/runBatchTrack.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type BatchTrackParams, ErrCode, RecaseError } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { JobName } from "@/queue/JobName.js"; @@ -15,7 +16,7 @@ export const runBatchTrack = async ({ ctx: AutumnContext; body: BatchTrackParams; }): Promise => { - const queueUrl = process.env.TRACK_ASYNC_SQS_QUEUE_URL; + const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL; if (!queueUrl) { ctx.logger.error( "[track] batch track requested but TRACK_ASYNC_SQS_QUEUE_URL is unset", diff --git a/server/src/internal/balances/track/utils/queueTrack.ts b/server/src/internal/balances/track/utils/queueTrack.ts index afffb6fe9..f397304d8 100644 --- a/server/src/internal/balances/track/utils/queueTrack.ts +++ b/server/src/internal/balances/track/utils/queueTrack.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { TrackParams } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { JobName } from "@/queue/JobName.js"; @@ -17,7 +18,7 @@ export const queueTrack = async ({ messageDeduplicationId?: string; }) => { try { - const resolvedQueueUrl = queueUrl ?? process.env.TRACK_SQS_QUEUE_URL; + const resolvedQueueUrl = queueUrl ?? runtimeEnv.TRACK_SQS_QUEUE_URL; if (!resolvedQueueUrl) { ctx.logger.warn( "[track] Redis unavailable and TRACK_SQS_QUEUE_URL is unset; falling back to synchronous track", diff --git a/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts b/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts index 5a178110c..88064edbe 100644 --- a/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts +++ b/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type FullCusEntWithFullCusProduct, type FullSubject, @@ -232,7 +233,7 @@ export const executeRedisDeductionV2 = async ({ } : null, unwind_value: unwindValue ?? null, - debug: process.env.NODE_ENV !== "production", + debug: runtimeEnv.NODE_ENV !== "production", }; const targetRedis = redisInstance ?? ctx.redisV2; diff --git a/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts b/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts index d08a84080..fc887d6e8 100644 --- a/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts +++ b/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts @@ -1,8 +1,9 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { AppEnv } from "@autumn/shared"; import { JobName } from "@/queue/JobName.js"; export const REFRESH_ENTITY_AGGREGATE_DEDUP_BUCKET_MS = - process.env.NODE_ENV === "development" ? 1000 : 5000; + runtimeEnv.NODE_ENV === "development" ? 1000 : 5000; /** * Buffer added after the bucket boundary so the trailing enqueue fires *after* diff --git a/server/src/internal/billing/attach/handleAttach.ts b/server/src/internal/billing/attach/handleAttach.ts index 217c3badc..eab671050 100644 --- a/server/src/internal/billing/attach/handleAttach.ts +++ b/server/src/internal/billing/attach/handleAttach.ts @@ -1,4 +1,9 @@ -import { type AttachResponseV1, AttachResponseV1Schema, Scopes } from "@autumn/shared"; +import { runtimeEnv } from "@/utils/envUtils.js"; +import { + type AttachResponseV1, + AttachResponseV1Schema, + Scopes, +} from "@autumn/shared"; import { AttachBodyV0Schema } from "../../../../../shared/api/billing/attach/prevVersions/attachBodyV0"; import { AffectedResource, @@ -21,7 +26,7 @@ export const handleAttach = createRoute({ resource: AffectedResource.Attach, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 60000, errorMessage: diff --git a/server/src/internal/billing/v2/handlers/handleAttachV2.ts b/server/src/internal/billing/v2/handlers/handleAttachV2.ts index 36905121d..464c98e07 100644 --- a/server/src/internal/billing/v2/handlers/handleAttachV2.ts +++ b/server/src/internal/billing/v2/handlers/handleAttachV2.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AffectedResource, ApiVersion, @@ -19,7 +20,7 @@ export const handleAttachV2 = createRoute({ }, resource: AffectedResource.Attach, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts b/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts index 4f3ba14df..517a929a3 100644 --- a/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts +++ b/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts @@ -1,7 +1,8 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { - CreateScheduleParamsV0Schema, - type CreateScheduleResponse, - Scopes, + CreateScheduleParamsV0Schema, + type CreateScheduleResponse, + Scopes, } from "@autumn/shared"; import { billingActions } from "@/internal/billing/v2/actions"; import { buildBillingLockKey } from "@/internal/billing/v2/utils/billingLock/buildBillingLockKey"; @@ -13,7 +14,7 @@ export const handleCreateSchedule = createRoute({ body: CreateScheduleParamsV0Schema, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/billing/v2/handlers/handleMultiAttach.ts b/server/src/internal/billing/v2/handlers/handleMultiAttach.ts index 0c181615d..9d0dde249 100644 --- a/server/src/internal/billing/v2/handlers/handleMultiAttach.ts +++ b/server/src/internal/billing/v2/handlers/handleMultiAttach.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AffectedResource, InternalError, @@ -14,7 +15,7 @@ export const handleMultiAttach = createRoute({ body: MultiAttachParamsV0Schema, resource: AffectedResource.MultiAttach, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts index 5fb219bb1..d4ce36394 100644 --- a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts +++ b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AffectedResource, ApiVersion, @@ -19,7 +20,7 @@ export const handleUpdateSubscription = createRoute({ }, resource: AffectedResource.ApiSubscriptionUpdate, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/billing/v2/providers/stripe/utils/connect/validateStripeSubscriptionActionOwnership.ts b/server/src/internal/billing/v2/providers/stripe/utils/connect/validateStripeSubscriptionActionOwnership.ts index aea8a4359..cc35448e6 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/connect/validateStripeSubscriptionActionOwnership.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/connect/validateStripeSubscriptionActionOwnership.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { BillingContext, StripeSubscriptionAction } from "@autumn/shared"; import { AppEnv, ErrCode, RecaseError } from "@autumn/shared"; import { stripeSubscriptionToApplication } from "@/external/stripe/subscriptions/utils/convertStripeSubscription"; @@ -6,8 +7,8 @@ import { isStripeConnected } from "@/internal/orgs/orgUtils"; const expectedStripeApplicationId = ({ ctx }: { ctx: AutumnContext }) => ctx.env === AppEnv.Live - ? process.env.STRIPE_LIVE_CLIENT_ID - : process.env.STRIPE_SANDBOX_CLIENT_ID; + ? runtimeEnv.STRIPE_LIVE_CLIENT_ID + : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID; const shouldValidateStripeApplicationOwnership = ({ ctx, diff --git a/server/src/internal/chat/chatUtils.ts b/server/src/internal/chat/chatUtils.ts index 1d6b0037d..2de53aee1 100644 --- a/server/src/internal/chat/chatUtils.ts +++ b/server/src/internal/chat/chatUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError } from "@autumn/shared"; export const slackProvider = "slack" as const; @@ -31,7 +32,7 @@ export const getMissingSlackScopes = (scopes: string[]) => { }; export const getRequiredChatEnv = (key: string) => { - const value = process.env[key]; + const value = runtimeEnv[key]; if (value) return value; throw new RecaseError({ @@ -42,20 +43,20 @@ export const getRequiredChatEnv = (key: string) => { }; export const getChatStateSecret = () => - process.env.CHAT_STATE_SECRET ?? - process.env.SLACK_STATE_SECRET ?? - process.env.BETTER_AUTH_SECRET ?? + runtimeEnv.CHAT_STATE_SECRET ?? + runtimeEnv.SLACK_STATE_SECRET ?? + runtimeEnv.BETTER_AUTH_SECRET ?? getRequiredChatEnv("ENCRYPTION_PASSWORD"); export const createSlackInstallUrl = (state: string) => { - const scope = process.env.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(","); + const scope = runtimeEnv.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(","); const params = new URLSearchParams({ client_id: getRequiredChatEnv("SLACK_CLIENT_ID"), scope, state, }); - if (process.env.SLACK_REDIRECT_URI) { - params.set("redirect_uri", process.env.SLACK_REDIRECT_URI); + if (runtimeEnv.SLACK_REDIRECT_URI) { + params.set("redirect_uri", runtimeEnv.SLACK_REDIRECT_URI); } return `https://slack.com/oauth/v2/authorize?${params}`; }; diff --git a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts index abb687f02..8df4bff73 100644 --- a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts +++ b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AffectedResource, type Checkout, @@ -24,7 +25,7 @@ export const handleConfirmCheckout = createRoute({ resource: AffectedResource.Attach, body: ConfirmCheckoutParamsSchema, lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/customers/cancel/handleCancelV2.ts b/server/src/internal/customers/cancel/handleCancelV2.ts index 733079e0b..4a377a9b8 100644 --- a/server/src/internal/customers/cancel/handleCancelV2.ts +++ b/server/src/internal/customers/cancel/handleCancelV2.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { UpdateSubscriptionV1Params } from "@autumn/shared"; import { Scopes } from "@autumn/shared"; import { createRoute } from "@/honoMiddlewares/routeHandler"; @@ -16,7 +17,7 @@ import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumn export const handleCancelV2 = createRoute({ scopes: [Scopes.Billing.Write], lock: - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ? { ttlMs: 120000, errorMessage: diff --git a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts index f3dac625e..8532200cd 100644 --- a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts +++ b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { AppEnv } from "@autumn/shared"; import type { Redis } from "ioredis"; import { @@ -18,7 +19,7 @@ type CustomerToDelete = { customerId: string; }; -const isProductionNode = process.env.NODE_ENV === "production"; +const isProductionNode = runtimeEnv.NODE_ENV === "production"; /** * Per org: all keys share `{orgId}` so Redis Cluster stays in one slot per pipeline. diff --git a/server/src/internal/emails/constants.ts b/server/src/internal/emails/constants.ts index 6263b410f..fa6c25ffc 100644 --- a/server/src/internal/emails/constants.ts +++ b/server/src/internal/emails/constants.ts @@ -1,2 +1,3 @@ -export const FROM_AUTUMN = `Autumn `; -export const FROM_AYUSH = `Ayush `; +import { runtimeEnv } from "@/utils/envUtils.js"; +export const FROM_AUTUMN = `Autumn `; +export const FROM_AYUSH = `Ayush `; diff --git a/server/src/internal/emails/sendInvitationEmail.ts b/server/src/internal/emails/sendInvitationEmail.ts index b1d1a1b56..839bb3692 100644 --- a/server/src/internal/emails/sendInvitationEmail.ts +++ b/server/src/internal/emails/sendInvitationEmail.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { logger } from "@/external/logtail/logtailUtils.js"; import { sendTextEmail } from "@/external/resend/resendUtils.js"; import { safeResend } from "@/external/resend/safeResend.js"; @@ -8,7 +9,7 @@ const getInvitationEmailBody = ({ orgName }: { orgName: string }) => { Click the link below to create an account / sign in to Autumn and accept the invitation. -${process.env.CLIENT_URL}/sign-in +${runtimeEnv.CLIENT_URL}/sign-in `; }; diff --git a/server/src/internal/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts index a5531b505..4f698278f 100644 --- a/server/src/internal/emails/sendOTPEmail.ts +++ b/server/src/internal/emails/sendOTPEmail.ts @@ -1,10 +1,11 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { logger } from "@/external/logtail/logtailUtils.js"; import { createResendCli } from "@/external/resend/resendUtils.js"; import { FROM_AUTUMN } from "./constants.js"; import OTPEmail from "./OTPEmail.js"; const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => { - if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) { + if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) { logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`); return; } diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts index 9440b31c8..a94473a50 100644 --- a/server/src/internal/invoices/InvoiceService.ts +++ b/server/src/internal/invoices/InvoiceService.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type ApiInvoiceV1, type Customer, @@ -43,7 +44,7 @@ export const processInvoice = ({ currency: invoice.currency, created_at: invoice.created_at, hosted_invoice_url: isStripe - ? `${process.env.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}` + ? `${runtimeEnv.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}` : null, // hosted_invoice_url: invoice.hosted_invoice_url, // items: withItems diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts index 01e14c37b..fa23c21c4 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts @@ -1,7 +1,3 @@ -import dotenv from "dotenv"; - -dotenv.config(); - import { type Customer, type FullProduct, diff --git a/server/src/internal/migrations/v2/handlers/handleRunMigration.ts b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts index 5f432a0e5..5a78e52cd 100644 --- a/server/src/internal/migrations/v2/handlers/handleRunMigration.ts +++ b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { auth } from "@trigger.dev/sdk/v3"; import { z } from "zod/v4"; @@ -72,7 +73,7 @@ export const handleRunMigration = createRoute({ }); } - const isDev = process.env.NODE_ENV === "development"; + const isDev = runtimeEnv.NODE_ENV === "development"; const { migrationRunId, triggerRunId } = await withMigrationRunClaim({ ctx, migration, diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts b/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts index f5cd329b1..d7d0e3076 100644 --- a/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts +++ b/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import chalk from "chalk"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { addExtrasToLogs } from "@/utils/logging/addContextToLogs.js"; @@ -62,7 +63,7 @@ export const logMigrateCustomerResult = ({ if ( Object.keys(ctx.extraLogs).length > 0 && - process.env.NODE_ENV === "development" + runtimeEnv.NODE_ENV === "development" ) { const maskedLogs = maskExtraLogs(ctx.extraLogs); ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`); diff --git a/server/src/internal/misc/edgeConfig/edgeConfigStore.ts b/server/src/internal/misc/edgeConfig/edgeConfigStore.ts index b2635fbc1..4146f5889 100644 --- a/server/src/internal/misc/edgeConfig/edgeConfigStore.ts +++ b/server/src/internal/misc/edgeConfig/edgeConfigStore.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, ms } from "@autumn/shared"; import type { S3Client } from "@aws-sdk/client-s3"; import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; @@ -26,7 +27,7 @@ export const createEdgeConfigStore = ({ s3Key, schema, defaultValue, - pollIntervalMs = process.env.NODE_ENV === "development" + pollIntervalMs = runtimeEnv.NODE_ENV === "development" ? ms.seconds(1) : ms.seconds(10), s3Client: injectedS3Client, diff --git a/server/src/internal/misc/feedback/handleSubmitFeedback.ts b/server/src/internal/misc/feedback/handleSubmitFeedback.ts index 727413cc0..36d5ace3c 100644 --- a/server/src/internal/misc/feedback/handleSubmitFeedback.ts +++ b/server/src/internal/misc/feedback/handleSubmitFeedback.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { z } from "zod/v4"; import { Scopes } from "@autumn/shared"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; @@ -16,7 +17,7 @@ export const handleSubmitFeedback = createRoute({ const userEmail = ctx.user?.email ?? "Unknown user"; const orgSlug = ctx.org?.slug ?? "Unknown org"; - const webhookUrl = process.env.DISCORD_FEEDBACK_WEBHOOK; + const webhookUrl = runtimeEnv.DISCORD_FEEDBACK_WEBHOOK; if (!webhookUrl) { console.warn("DISCORD_FEEDBACK_WEBHOOK not configured"); return c.json({ success: true }); diff --git a/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts b/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts index a66b2ffaf..f2cb593dd 100644 --- a/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts +++ b/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { createAnthropic } from "@ai-sdk/anthropic"; import { type AgentPricingConfig, InternalError } from "@autumn/shared"; import { withTracing } from "@posthog/ai"; @@ -12,12 +13,12 @@ import { OrganisationConfigurationSchema } from "./pricingAgentSchemas.js"; // PostHog client singleton let phClient: PostHog | null = null; const getPostHogClient = (): PostHog | null => { - if (!process.env.POSTHOG_API_KEY) { + if (!runtimeEnv.POSTHOG_API_KEY) { return null; } if (!phClient) { - phClient = new PostHog(process.env.POSTHOG_API_KEY, { - host: process.env.POSTHOG_HOST || "https://us.i.posthog.com", + phClient = new PostHog(runtimeEnv.POSTHOG_API_KEY, { + host: runtimeEnv.POSTHOG_HOST || "https://us.i.posthog.com", }); } return phClient; @@ -122,7 +123,7 @@ pricingAgentRouter.post("/chat", async (c) => { } = await c.req.json(); const ctx = c.var.ctx; - if (!process.env.ANTHROPIC_API_KEY) { + if (!runtimeEnv.ANTHROPIC_API_KEY) { throw new InternalError({ message: "ANTHROPIC_API_KEY not configured", code: "anthropic_not_configured", @@ -150,7 +151,7 @@ When the user asks to make changes, modify this existing configuration rather th // Create Anthropic client and optionally wrap with PostHog tracing const anthropicClient = createAnthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, + apiKey: runtimeEnv.ANTHROPIC_API_KEY, }); const baseModel = anthropicClient("claude-opus-4-5"); diff --git a/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts b/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts index 821e6f2dd..d4e3865d1 100644 --- a/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts +++ b/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ApiVersion, ApiVersionClass } from "@autumn/shared"; import type { Context } from "hono"; import { matchRoute } from "../../../honoMiddlewares/middlewareUtils"; @@ -224,7 +225,7 @@ export const resolveRateLimit = ({ export const RATE_LIMIT_CONFIGS: Record = { [RateLimitType.General]: { name: "general", - limit: process.env.NODE_ENV === "development" ? 1000 : 25, + limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 25, windowMs: 1000, notInRedis: false, scope: RateLimitScope.Org, diff --git a/server/src/internal/misc/trmnl/trmnlRouter.ts b/server/src/internal/misc/trmnl/trmnlRouter.ts index 3e0cf2df0..97360ad33 100644 --- a/server/src/internal/misc/trmnl/trmnlRouter.ts +++ b/server/src/internal/misc/trmnl/trmnlRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { Hono } from "hono"; import { rateLimiter } from "hono-rate-limiter"; import { traceEnrichMiddleware } from "@/honoMiddlewares/traceMiddleware.js"; @@ -10,7 +11,7 @@ import { trmnlAuthMiddleware } from "./trmnlAuthMiddleware.js"; // TRMNL rate limiter: 10 requests per 30 minutes in production, 1000 in dev const trmnlScreenLimiter = rateLimiter({ windowMs: 60 * 1000 * 30, // 30 minutes - limit: process.env.NODE_ENV === "development" ? 1000 : 10, + limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 10, standardHeaders: "draft-6", keyGenerator: (c) => c.req.header("x-trmnl-id") ?? "unknown", }); diff --git a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts index 38ac73773..a71efc9e3 100644 --- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts +++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { ErrCode, Scopes } from "@autumn/shared"; import { getUploadUrl } from "@/external/supabase/storageUtils.js"; import RecaseError from "@/utils/errorUtils.js"; @@ -11,7 +12,7 @@ export const handleGetUploadUrl = createRoute({ const path = `logo/${org.id}`; - if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { + if (!runtimeEnv.SUPABASE_URL || !runtimeEnv.SUPABASE_SERVICE_KEY) { throw new RecaseError({ message: "Supabase storage not set up", code: ErrCode.SupabaseNotFound, diff --git a/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts b/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts index 1aa9b579a..f994d3865 100644 --- a/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts +++ b/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, ErrCode, Scopes } from "@autumn/shared"; import { initMasterStripe } from "@/external/connect/initStripeCli.js"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; @@ -12,7 +13,7 @@ export const handleResetDefaultAccount = createRoute({ const { db, org, logger, env } = ctx; // Validation: Only allow for test org - if (org.id !== process.env.TESTS_ORG_ID) { + if (org.id !== runtimeEnv.TESTS_ORG_ID) { throw new RecaseError({ message: "This endpoint can only be used for test organizations", code: ErrCode.InvalidRequest, diff --git a/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts b/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts index 8840ff585..9b0ca1a62 100644 --- a/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts +++ b/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { z } from "zod/v4"; import { @@ -20,8 +21,8 @@ export const handleGetRevenueCatOAuthUrl = createRoute({ const { org, env } = ctx; if ( - !process.env.REVENUECAT_OAUTH_CLIENT_ID || - !process.env.REVENUECAT_OAUTH_CLIENT_SECRET + !runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID || + !runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET ) { throw new RecaseError({ message: "RevenueCat OAuth client credentials not configured", @@ -30,7 +31,7 @@ export const handleGetRevenueCatOAuthUrl = createRoute({ }); } - const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173"; + const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:5173"; const envPrefix = env === AppEnv.Sandbox ? "/sandbox" : ""; const redirectUri = redirect_url || `${frontendUrl}${envPrefix}/dev?tab=revenuecat`; diff --git a/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts b/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts index 07aa3bf04..0922548ae 100644 --- a/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts +++ b/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, type Organization, @@ -75,7 +76,7 @@ export const handleRevenueCatOAuthCallback = async (c: Context) => { const { db } = initDrizzle(); - const frontendUrl = process.env.CLIENT_URL || "http://localhost:3000"; + const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:3000"; let redirectUrl = new URL(`${frontendUrl}`); redirectUrl.searchParams.set("tab", "revenuecat"); let isPlatformFlow = false; diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts index 54629170f..a85a5f689 100644 --- a/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, type Organization, @@ -44,8 +45,8 @@ const disconnectStripe = async ({ await masterStripe.oauth.deauthorize({ client_id: env === AppEnv.Live - ? process.env.STRIPE_LIVE_CLIENT_ID || "" - : process.env.STRIPE_SANDBOX_CLIENT_ID || "", + ? runtimeEnv.STRIPE_LIVE_CLIENT_ID || "" + : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "", stripe_user_id: accountId, }); } catch (error) { diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts index 3b69428ca..fc437f35f 100644 --- a/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { z } from "zod/v4"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; @@ -15,8 +16,8 @@ export const handleGetOAuthUrl = createRoute({ const clientId = env === AppEnv.Live - ? process.env.STRIPE_LIVE_CLIENT_ID - : process.env.STRIPE_SANDBOX_CLIENT_ID; + ? runtimeEnv.STRIPE_LIVE_CLIENT_ID + : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID; if (!clientId) { throw new RecaseError({ @@ -27,7 +28,7 @@ export const handleGetOAuthUrl = createRoute({ } // Generate OAuth state and store in Redis - const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173"; + const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:5173"; const redirectUri = redirect_url || `${frontendUrl}/dev?tab=stripe`; @@ -42,13 +43,13 @@ export const handleGetOAuthUrl = createRoute({ `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write`, ); - let serverUrl = process.env.BETTER_AUTH_URL; + let serverUrl = runtimeEnv.BETTER_AUTH_URL; if (env === AppEnv.Live && serverUrl?.includes("localhost")) { serverUrl = `https://express.dev.useautumn.com`; } - if (process.env.NGROK_URL) { - serverUrl = process.env.NGROK_URL; + if (runtimeEnv.NGROK_URL) { + serverUrl = runtimeEnv.NGROK_URL; } // Add state + redirect_uri diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts index 4dc08a608..48700681c 100644 --- a/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv } from "@autumn/shared"; import type { Context } from "hono"; import { initDrizzle } from "@/db/initDrizzle.js"; @@ -19,7 +20,7 @@ export const handleOAuthCallback = async (c: Context) => { const { db } = initDrizzle(); // Build frontend redirect URL (default) - const frontendUrl = process.env.CLIENT_URL || "http://localhost:3000"; + const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:3000"; let redirectUrl = new URL(`${frontendUrl}`); redirectUrl.searchParams.set("tab", "stripe"); diff --git a/server/src/internal/orgs/orgUtils/createConnectAccount.ts b/server/src/internal/orgs/orgUtils/createConnectAccount.ts index 89cd72c4a..e9dfe226d 100644 --- a/server/src/internal/orgs/orgUtils/createConnectAccount.ts +++ b/server/src/internal/orgs/orgUtils/createConnectAccount.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { AppEnv } from "@autumn/shared"; import type { User } from "better-auth"; import type { Organization } from "better-auth/plugins"; diff --git a/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts index f1205100d..3f1607465 100644 --- a/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts +++ b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { z } from "zod/v4"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; @@ -44,8 +45,8 @@ export const handleGetPlatformOAuth = createRoute({ // Get appropriate Stripe client ID based on environment const clientId = env === "live" - ? process.env.STRIPE_LIVE_CLIENT_ID - : process.env.STRIPE_SANDBOX_CLIENT_ID; + ? runtimeEnv.STRIPE_LIVE_CLIENT_ID + : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID; if (!clientId) { throw new RecaseError({ @@ -63,7 +64,7 @@ export const handleGetPlatformOAuth = createRoute({ oauthUrl.searchParams.set("state", stateKey); oauthUrl.searchParams.set( "redirect_uri", - `${process.env.BETTER_AUTH_URL || "https://express.dev.useautumn.com"}/stripe/oauth_callback`, + `${runtimeEnv.BETTER_AUTH_URL || "https://express.dev.useautumn.com"}/stripe/oauth_callback`, ); logger.info(`Generated OAuth URL for platform org ${org.slug} (${env})`); diff --git a/server/src/internal/platform/platformBeta/platformBetaRouter.ts b/server/src/internal/platform/platformBeta/platformBetaRouter.ts index f620a7b0d..4df5eb2c1 100644 --- a/server/src/internal/platform/platformBeta/platformBetaRouter.ts +++ b/server/src/internal/platform/platformBeta/platformBetaRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js"; @@ -18,7 +19,7 @@ platformBetaRouter.use("*", async (c, next) => { const ctx = c.get("ctx"); const { org, logger } = ctx; - if (!process.env.AUTUMN_SECRET_KEY) { + if (!runtimeEnv.AUTUMN_SECRET_KEY) { return next(); } diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts index 298a9fe5d..1f639a072 100644 --- a/server/src/internal/products/productUtils/detectProductVariant.ts +++ b/server/src/internal/products/productUtils/detectProductVariant.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { anthropic } from "@ai-sdk/anthropic"; import { BillingInterval, type FullProduct } from "@autumn/shared"; import { generateObject } from "ai"; @@ -34,7 +35,7 @@ export const detectBaseVariant = async ({ logger: Logger; }) => { logger.info(`Detecting base variant for ${curProduct.id}`); - if (!process.env.ANTHROPIC_API_KEY) return; + if (!runtimeEnv.ANTHROPIC_API_KEY) return; const existingProducts = (await ProductService.listFull({ db, diff --git a/server/src/node.ts b/server/src/node.ts index 38cd7f7ed..bd7afd1d1 100644 --- a/server/src/node.ts +++ b/server/src/node.ts @@ -1,12 +1,13 @@ // Entry point: Load Infisical secrets, then start the Node/Bun app. -import "dotenv/config"; - +import { setRuntimeEnvFromProcess } from "@/utils/envUtils.js"; import cluster from "node:cluster"; import { initInfisical } from "./external/infisical/initInfisical.js"; -// Load Infisical secrets into process.env ONLY in master/primary process. -// Infisical will NOT override existing env vars (from .env above). +setRuntimeEnvFromProcess(); + +// Load Infisical secrets into runtimeEnv ONLY in master/primary process. +// Infisical will NOT override existing env vars supplied by the process. if (cluster.isPrimary) { await initInfisical(); } diff --git a/server/src/queue/blueGreen/blueGreenReadinessChecks.ts b/server/src/queue/blueGreen/blueGreenReadinessChecks.ts index 63b051113..d7cfe8d7c 100644 --- a/server/src/queue/blueGreen/blueGreenReadinessChecks.ts +++ b/server/src/queue/blueGreen/blueGreenReadinessChecks.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { GetQueueAttributesCommand, SQSClient } from "@aws-sdk/client-sqs"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { @@ -43,8 +44,8 @@ const probe = async ({ const getConfiguredQueueUrls = () => [ QUEUE_URL, - process.env.TRACK_SQS_QUEUE_URL, - process.env.TRACK_ASYNC_SQS_QUEUE_URL, + runtimeEnv.TRACK_SQS_QUEUE_URL, + runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL, ].filter((url): url is string => Boolean(url)); export const getBlueGreenQueueUrls = ({ @@ -61,8 +62,7 @@ export const getBlueGreenQueueUrls = ({ // the SDK falls back to (the queue URL's host). const sqsClientsByRegion = new Map(); const getSqsClientForQueue = (queueUrl: string): SQSClient => { - const region = - extractRegionFromQueueUrl({ queueUrl }) ?? DEFAULT_AWS_REGION; + const region = extractRegionFromQueueUrl({ queueUrl }) ?? DEFAULT_AWS_REGION; const cached = sqsClientsByRegion.get(region); if (cached) return cached; const client = new SQSClient({ region }); diff --git a/server/src/queue/hatchetWorkflows/createWorkflowTask.ts b/server/src/queue/hatchetWorkflows/createWorkflowTask.ts index c2e899c75..8a97789ec 100644 --- a/server/src/queue/hatchetWorkflows/createWorkflowTask.ts +++ b/server/src/queue/hatchetWorkflows/createWorkflowTask.ts @@ -5,6 +5,7 @@ import { db } from "@/db/initDrizzle.js"; import { createLogger } from "@/external/logtail/logtailUtils.js"; import { getSentryTags } from "@/external/sentry/sentryUtils.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { generateId } from "@/utils/genUtils.js"; import { addWorkflowToLogs } from "@/utils/logging/addContextToLogs.js"; import { createWorkerContext } from "../createWorkerContext.js"; @@ -32,7 +33,7 @@ export const createWorkflowTask = ({ }): ((input: TInput, hatchetCtx: Context) => Promise) => { return async (input: TInput, hatchetCtx: Context) => { const { orgId, env, customerId } = input; - const logger = createLogger(); + const logger = createLogger(runtimeEnv); // Get workflow/task name from Hatchet context const workflowName = hatchetCtx.workflowName(); diff --git a/server/src/queue/initSqs.ts b/server/src/queue/initSqs.ts index 20ee05cfd..7168b54eb 100644 --- a/server/src/queue/initSqs.ts +++ b/server/src/queue/initSqs.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { SQSClient } from "@aws-sdk/client-sqs"; import { DEFAULT_AWS_REGION, @@ -23,7 +24,7 @@ export const extractLocalEndpoint = ({ }; const getSqsClientConfig = ({ queueUrl }: { queueUrl?: string } = {}) => { - const resolvedQueueUrl = queueUrl ?? process.env.SQS_QUEUE_URL_V2; + const resolvedQueueUrl = queueUrl ?? runtimeEnv.SQS_QUEUE_URL_V2; const endpoint = extractLocalEndpoint({ queueUrl: resolvedQueueUrl }); const region = extractRegionFromQueueUrl({ queueUrl: resolvedQueueUrl }) || @@ -33,14 +34,14 @@ const getSqsClientConfig = ({ queueUrl }: { queueUrl?: string } = {}) => { region, ...(endpoint ? { endpoint } : {}), credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || "", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "", + accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "", + secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "", }, }; }; const getSqsClientCacheKey = ({ queueUrl }: { queueUrl?: string } = {}) => { - const resolvedQueueUrl = queueUrl ?? process.env.SQS_QUEUE_URL_V2; + const resolvedQueueUrl = queueUrl ?? runtimeEnv.SQS_QUEUE_URL_V2; const endpoint = extractLocalEndpoint({ queueUrl: resolvedQueueUrl }); const region = extractRegionFromQueueUrl({ queueUrl: resolvedQueueUrl }) || @@ -70,7 +71,7 @@ export const recreateSqsClient = ({ const nextClient = new SQSClient(getSqsClientConfig({ queueUrl })); sqsClientsByCacheKey.set(cacheKey, nextClient); - if (!queueUrl || queueUrl === process.env.SQS_QUEUE_URL_V2) { + if (!queueUrl || queueUrl === runtimeEnv.SQS_QUEUE_URL_V2) { sqsClient = nextClient; } @@ -92,4 +93,4 @@ export const getSqsClient = ({ return nextClient; }; -export const QUEUE_URL = process.env.SQS_QUEUE_URL_V2 || ""; +export const QUEUE_URL = runtimeEnv.SQS_QUEUE_URL_V2 || ""; diff --git a/server/src/queue/initWorkers.ts b/server/src/queue/initWorkers.ts index 0d493ce90..280a4d82d 100644 --- a/server/src/queue/initWorkers.ts +++ b/server/src/queue/initWorkers.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; await import("../sentry.js"); import { ms } from "@autumn/shared"; @@ -40,7 +41,7 @@ const MAX_MESSAGES_BEFORE_RECYCLE = 50_000; // Idle self-kill — exit if worker processes 0 messages for this many consecutive intervals const IDLE_SELF_KILL_THRESHOLD = 5; // ~5 min of 0 messages (5 * 60s) -const shouldIdleSelfKill = process.env.NODE_ENV !== "development"; +const shouldIdleSelfKill = runtimeEnv.NODE_ENV !== "development"; // Per-message processing timeout — must be under VisibilityTimeout (30s) const MESSAGE_TIMEOUT_MS = 25_000; @@ -171,7 +172,7 @@ export const startPollingLoop = async ({ consecutiveZeroMessageIntervals >= IDLE_SELF_KILL_THRESHOLD && totalMessagesProcessed > 0 && activeMigrationJobs === 0 && - process.env.NODE_ENV !== "development" + runtimeEnv.NODE_ENV !== "development" ) { console.log( `${prefix} Idle self-kill: 0 messages for ${consecutiveZeroMessageIntervals} intervals after processing ${totalMessagesProcessed} total. Exiting for cluster respawn.`, @@ -438,7 +439,7 @@ export const initWorkers = async ({ controller.abort(); } - const isProd = process.env.NODE_ENV === "production"; + const isProd = runtimeEnv.NODE_ENV === "production"; if (isProd) { const shutdownTimeout = setTimeout(() => process.exit(0), 5000); if (shutdownTimeout.unref) { @@ -465,11 +466,11 @@ export const initWorkers = async ({ }, { queueId: JOB_QUEUE_IDS.track, - queueUrl: process.env.TRACK_SQS_QUEUE_URL, + queueUrl: runtimeEnv.TRACK_SQS_QUEUE_URL, }, { queueId: JOB_QUEUE_IDS.trackAsync, - queueUrl: process.env.TRACK_ASYNC_SQS_QUEUE_URL, + queueUrl: runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL, }, ]) { if (!queueUrl) continue; diff --git a/server/src/queue/processMessage.ts b/server/src/queue/processMessage.ts index d0a80132e..001358a6f 100644 --- a/server/src/queue/processMessage.ts +++ b/server/src/queue/processMessage.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { Message } from "@aws-sdk/client-sqs"; import * as Sentry from "@sentry/bun"; import chalk from "chalk"; @@ -371,7 +372,7 @@ export const processMessage = async ({ done: true, }); - if (process.env.NODE_ENV === "development") { + if (runtimeEnv.NODE_ENV === "development") { finalLogger.debug( `FINISHED PROCESSING JOB ${job.name}, EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`, ); diff --git a/server/src/queue/queueUtils.ts b/server/src/queue/queueUtils.ts index dab1cc43e..438cef788 100644 --- a/server/src/queue/queueUtils.ts +++ b/server/src/queue/queueUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { ApiVersion, AppEnv, @@ -127,7 +128,7 @@ export const addTaskToQueue = async ({ delayMs?: number; queueUrl?: string; }) => { - const resolvedQueueUrl = queueUrl || process.env.SQS_QUEUE_URL_V2; + const resolvedQueueUrl = queueUrl || runtimeEnv.SQS_QUEUE_URL_V2; if (resolvedQueueUrl) { const sqsClient = getSqsClient({ queueUrl: resolvedQueueUrl }); diff --git a/server/src/routers/chatProxyRouter.ts b/server/src/routers/chatProxyRouter.ts index 54657f48d..52265aac7 100644 --- a/server/src/routers/chatProxyRouter.ts +++ b/server/src/routers/chatProxyRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type Context, Hono } from "hono"; import type { HonoEnv } from "../honoUtils/HonoEnv.js"; @@ -18,8 +19,8 @@ const proxyChatRequest = }; export const createChatProxyRouter = ( - chatServerUrl = process.env.CHAT_SERVER_URL ?? - (process.env.NODE_ENV === "production" + chatServerUrl = runtimeEnv.CHAT_SERVER_URL ?? + (runtimeEnv.NODE_ENV === "production" ? "https://chat.useautumn.com" : "http://localhost:3099"), ) => { diff --git a/server/src/routers/internalRouter.ts b/server/src/routers/internalRouter.ts index 0ceb1b3cb..cc0db4796 100644 --- a/server/src/routers/internalRouter.ts +++ b/server/src/routers/internalRouter.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { autumnHandler } from "autumn-js/backend/hono"; import { Hono } from "hono"; import { internalTrmnlRouter } from "@/internal/misc/trmnl/trmnlRouter.js"; @@ -55,7 +56,7 @@ internalRouter.route("", migrationRpcRouter); internalRouter.route("/workbench", workbenchRouter); // Autumn SDK handler (requires session auth) -if (process.env.AUTUMN_SECRET_KEY) { +if (runtimeEnv.AUTUMN_SECRET_KEY) { internalRouter.use( "/api/autumn/*", autumnHandler({ diff --git a/server/src/sentry.ts b/server/src/sentry.ts index d47c0f9c2..d8d7788c0 100644 --- a/server/src/sentry.ts +++ b/server/src/sentry.ts @@ -1,8 +1,9 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import * as Sentry from "@sentry/bun"; -if (process.env.SENTRY_DSN) { +if (runtimeEnv.SENTRY_DSN) { Sentry.init({ - dsn: process.env.SENTRY_DSN, + dsn: runtimeEnv.SENTRY_DSN, sendDefaultPii: true, skipOpenTelemetrySetup: true, }); diff --git a/server/src/trigger/configureTrigger.ts b/server/src/trigger/configureTrigger.ts index 0462edc79..1e6751fc5 100644 --- a/server/src/trigger/configureTrigger.ts +++ b/server/src/trigger/configureTrigger.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { configure } from "@trigger.dev/sdk/v3"; /** @@ -13,6 +14,6 @@ import { configure } from "@trigger.dev/sdk/v3"; * `@/trigger/migrations/...`, which re-exports from this file's siblings, * so the configure happens before any `.trigger()` call. */ -if (process.env.TRIGGER_SERVER_SECRET_KEY) { - configure({ secretKey: process.env.TRIGGER_SERVER_SECRET_KEY }); +if (runtimeEnv.TRIGGER_SERVER_SECRET_KEY) { + configure({ secretKey: runtimeEnv.TRIGGER_SERVER_SECRET_KEY }); } diff --git a/server/src/trigger/utils/createTriggerContext.ts b/server/src/trigger/utils/createTriggerContext.ts index 67096e164..b33b6704c 100644 --- a/server/src/trigger/utils/createTriggerContext.ts +++ b/server/src/trigger/utils/createTriggerContext.ts @@ -7,6 +7,7 @@ import { } from "@/external/logtail/logtailUtils.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { createWorkerContext } from "@/queue/createWorkerContext.js"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { addTriggerToLogs } from "@/utils/logging/addContextToLogs.js"; /** @@ -25,7 +26,7 @@ export const createTriggerContext = async ({ customerId?: string; }): Promise<{ ctx: AutumnContext; logger: Logger }> => { const logger = addTriggerToLogs({ - logger: createDualLogger(), + logger: createDualLogger(runtimeEnv), triggerContext: { run_id: triggerCtx.run.id, task_id: triggerCtx.task.id, diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index c4a51e66f..ee2c05a3c 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -1,4 +1,4 @@ -import "dotenv/config"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { ALL_SCOPES, ac, invitation, roles, schemas } from "@autumn/shared"; import { oauthProvider } from "@better-auth/oauth-provider"; import { passkey } from "@better-auth/passkey"; @@ -30,8 +30,8 @@ import { ADMIN_USER_IDs } from "./constants.js"; // emulate.dev Google: rewrite outbound Google OAuth host so agent worktrees // can use any redirect URI without registering it in the real Google console. // Real Google's oauth2.googleapis.com/token maps to emulate's /oauth2/token path. -if (process.env.EMULATE_GOOGLE_URL && process.env.NODE_ENV !== "production") { - const emulate = process.env.EMULATE_GOOGLE_URL.replace(/\/$/, ""); +if (runtimeEnv.EMULATE_GOOGLE_URL && runtimeEnv.NODE_ENV !== "production") { + const emulate = runtimeEnv.EMULATE_GOOGLE_URL.replace(/\/$/, ""); const originalFetch = globalThis.fetch; globalThis.fetch = ((input: any, init?: any) => { const url = @@ -57,15 +57,15 @@ if (process.env.EMULATE_GOOGLE_URL && process.env.NODE_ENV !== "production") { } const emulateGoogleUrl = - process.env.NODE_ENV !== "production" - ? process.env.EMULATE_GOOGLE_URL?.replace(/\/$/, "") + runtimeEnv.NODE_ENV !== "production" + ? runtimeEnv.EMULATE_GOOGLE_URL?.replace(/\/$/, "") : undefined; // HTTPS agent worktrees go through portless (e.g. wtN-api.localhost). The // OAuth flow leaves and returns via a third-party host (emulate.dev), so the // state cookie must be SameSite=None+Secure to survive the round trip. -const isHttpsBaseUrl = process.env.BETTER_AUTH_URL?.startsWith("https://"); -const isProductionAuth = process.env.NODE_ENV === "production"; +const isHttpsBaseUrl = runtimeEnv.BETTER_AUTH_URL?.startsWith("https://"); +const isProductionAuth = runtimeEnv.NODE_ENV === "production"; const parseMcpResourceUrl = (rawUrl: string) => { const resourceUrl = rawUrl.trim(); @@ -85,15 +85,15 @@ const parseMcpResourceUrl = (rawUrl: string) => { // The OAuth `resource` indicator is host-based, so every public host + path // must be a registered audience. MCP_RESOURCE_URLS is an explicit override. const mcpServerUrl = - process.env.MCP_SERVER_URL ?? + runtimeEnv.MCP_SERVER_URL ?? (isProductionAuth ? "https://mcp.useautumn.com" : "http://localhost:3099"); const chatServerUrl = - process.env.CHAT_SERVER_URL ?? + runtimeEnv.CHAT_SERVER_URL ?? (isProductionAuth ? "https://chat.useautumn.com" : "http://localhost:3099"); const mcpResourcePaths = ["/mcp"]; const mcpResourceBases = [ - process.env.BETTER_AUTH_URL, + runtimeEnv.BETTER_AUTH_URL, mcpServerUrl, chatServerUrl, ].filter((base): base is string => Boolean(base)); @@ -103,7 +103,7 @@ const mcpResourceUrls = [ ...mcpResourceBases.flatMap((base) => mcpResourcePaths.map((path) => new URL(path, base).href), ), - ...(process.env.MCP_RESOURCE_URLS?.split(",") + ...(runtimeEnv.MCP_RESOURCE_URLS?.split(",") .map(parseMcpResourceUrl) .filter((url): url is string => Boolean(url)) ?? []), ]), @@ -120,7 +120,7 @@ const mcpResourceUrls = [ * - origin: full URL with scheme. Multiple origins may be supplied for envs * that need to accept both Portless and direct localhost. */ -const passkeyFrontendUrl = process.env.CLIENT_URL ?? "http://localhost:3000"; +const passkeyFrontendUrl = runtimeEnv.CLIENT_URL ?? "http://localhost:3000"; const passkeyOrigins: string[] = [passkeyFrontendUrl]; const passkeyRpID = (() => { try { @@ -131,13 +131,13 @@ const passkeyRpID = (() => { })(); if ( - process.env.VITE_FRONTEND_URL && - process.env.VITE_FRONTEND_URL !== passkeyFrontendUrl + runtimeEnv.VITE_FRONTEND_URL && + runtimeEnv.VITE_FRONTEND_URL !== passkeyFrontendUrl ) { try { - const viteOrigin = new URL(process.env.VITE_FRONTEND_URL); + const viteOrigin = new URL(runtimeEnv.VITE_FRONTEND_URL); if (viteOrigin.hostname === passkeyRpID) { - passkeyOrigins.push(process.env.VITE_FRONTEND_URL); + passkeyOrigins.push(runtimeEnv.VITE_FRONTEND_URL); } } catch { // Invalid URL, ignore @@ -145,7 +145,7 @@ if ( } const options = { - baseURL: process.env.BETTER_AUTH_URL, + baseURL: runtimeEnv.BETTER_AUTH_URL, telemetry: { enabled: false, }, @@ -209,7 +209,7 @@ const options = { "https://staging.useautumn.com", "https://*.useautumn.com", ]; - if (process.env.NODE_ENV === "production") return origins; + if (runtimeEnv.NODE_ENV === "production") return origins; // Worktree ports follow worktreeOffset = (N-1)*100; accept any localhost // port the running stack might use as origin. @@ -220,8 +220,8 @@ const options = { ) { origins.push(origin); } - if (process.env.CLIENT_URL) origins.push(process.env.CLIENT_URL); - if (process.env.BETTER_AUTH_URL) origins.push(process.env.BETTER_AUTH_URL); + if (runtimeEnv.CLIENT_URL) origins.push(runtimeEnv.CLIENT_URL); + if (runtimeEnv.BETTER_AUTH_URL) origins.push(runtimeEnv.BETTER_AUTH_URL); return origins; }, emailAndPassword: { @@ -236,9 +236,9 @@ const options = { socialProviders: { google: { - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - redirectURI: `${process.env.BETTER_AUTH_URL}/api/auth/callback/google`, + clientId: runtimeEnv.GOOGLE_CLIENT_ID!, + clientSecret: runtimeEnv.GOOGLE_CLIENT_SECRET, + redirectURI: `${runtimeEnv.BETTER_AUTH_URL}/api/auth/callback/google`, ...(emulateGoogleUrl ? { // HS256-signed id_tokens from emulate fail real Google's RS256 JWKS check. @@ -266,12 +266,12 @@ const options = { jwt(), oauthProvider({ - loginPage: `${process.env.CLIENT_URL}/sign-in`, - consentPage: `${process.env.CLIENT_URL}/consent`, + loginPage: `${runtimeEnv.CLIENT_URL}/sign-in`, + consentPage: `${runtimeEnv.CLIENT_URL}/consent`, // Resource-based scopes with R/W actions (plus legacy CRUDL + // meta scopes — see shared/utils/scopeDefinitions.ts). scopes: [...ALL_SCOPES], - validAudiences: [process.env.BETTER_AUTH_URL, ...mcpResourceUrls].filter( + validAudiences: [runtimeEnv.BETTER_AUTH_URL, ...mcpResourceUrls].filter( Boolean, ) as string[], allowDynamicClientRegistration: true, @@ -288,7 +288,7 @@ const options = { // This makes consent org-scoped, not just user-scoped postLogin: { // Required: page to redirect to if shouldRedirect returns true - page: `${process.env.CLIENT_URL}/consent`, + page: `${runtimeEnv.CLIENT_URL}/consent`, // Required: whether to show post-login page (we don't need this, so always false) shouldRedirect: async () => false, // Optional: reference ID for consent (org ID makes consent org-scoped) @@ -315,7 +315,7 @@ const options = { email: string; organization: Organization; }) { - const inviteLink = `${process.env.CLIENT_URL}/accept?id=${data.id}`; + const inviteLink = `${runtimeEnv.CLIENT_URL}/accept?id=${data.id}`; await sendInvitationEmail({ email: data.email, orgName: (data.organization.name as string) ?? "an organization", diff --git a/server/src/utils/authUtils/afterSessionCreated.ts b/server/src/utils/authUtils/afterSessionCreated.ts index 558ba5311..49779c70c 100644 --- a/server/src/utils/authUtils/afterSessionCreated.ts +++ b/server/src/utils/authUtils/afterSessionCreated.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { GenericEndpointContext } from "@better-auth/core"; import type { BetterAuthOptions, Session } from "better-auth"; @@ -11,7 +12,7 @@ export const afterSessionCreated = async ( // In dev, the Vite dashboard sets this cookie client-side (localhost // doesn't support cross-port cookie sharing via Set-Cookie headers). - if (process.env.NODE_ENV !== "production") return; + if (runtimeEnv.NODE_ENV !== "production") return; // Set a non-httpOnly hint cookie on the root domain so the landing // page (useautumn.com) can detect that the user is logged in on app.useautumn.com. diff --git a/server/src/utils/authUtils/afterSessionDeleted.ts b/server/src/utils/authUtils/afterSessionDeleted.ts index c479d1632..20e966fa3 100644 --- a/server/src/utils/authUtils/afterSessionDeleted.ts +++ b/server/src/utils/authUtils/afterSessionDeleted.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import type { GenericEndpointContext } from "@better-auth/core"; import type { BetterAuthOptions, Session } from "better-auth"; @@ -10,7 +11,7 @@ export const afterSessionDeleted = async ( // Dev parity: the cookie is only set server-side in production, so // we don't need to clear it here in other environments. - if (process.env.NODE_ENV !== "production") return; + if (runtimeEnv.NODE_ENV !== "production") return; // Clear the landing-page hint cookie so signed-out users stop seeing // the "Dashboard" CTA on useautumn.com. Attributes must match the diff --git a/server/src/utils/constants.ts b/server/src/utils/constants.ts index a28ecc47c..24326c380 100644 --- a/server/src/utils/constants.ts +++ b/server/src/utils/constants.ts @@ -1,4 +1,4 @@ -import "dotenv/config"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { CusProductStatus } from "@autumn/shared"; const BREAK_API_VERSION = 0.2; @@ -27,7 +27,7 @@ export const dashboardOrigins = [ "https://app.useautumn.com", "https://staging.useautumn.com", "https://dev.useautumn.com", - process.env.CLIENT_URL!, + runtimeEnv.CLIENT_URL!, ]; export const WEBHOOK_EVENTS = [ diff --git a/server/src/utils/corsOrigins.ts b/server/src/utils/corsOrigins.ts index 89a36ad27..f6d54d7ac 100644 --- a/server/src/utils/corsOrigins.ts +++ b/server/src/utils/corsOrigins.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; export const ALLOWED_ORIGINS = [ "http://localhost:3000", "http://localhost:3001", @@ -21,7 +22,7 @@ export const ALLOWED_ORIGINS = [ export const isAllowedOrigin = (origin: string): string | undefined => { if (ALLOWED_ORIGINS.includes(origin)) return origin; if ( - process.env.NODE_ENV !== "production" && + runtimeEnv.NODE_ENV !== "production" && /^https?:\/\/(?:[a-zA-Z0-9-]+\.)*localhost(?::\d+)?$/.test(origin) ) { return origin; diff --git a/server/src/utils/encryptUtils.ts b/server/src/utils/encryptUtils.ts index a2d735838..491890075 100644 --- a/server/src/utils/encryptUtils.ts +++ b/server/src/utils/encryptUtils.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { AppEnv } from "@autumn/shared"; import crypto from "crypto"; import KSUID from "ksuid"; @@ -5,7 +6,7 @@ import KSUID from "ksuid"; const getKey = () => { return crypto .createHash("sha512") - .update(process.env.ENCRYPTION_PASSWORD!) + .update(runtimeEnv.ENCRYPTION_PASSWORD!) .digest("hex") .substring(0, 32); }; diff --git a/server/src/utils/envUtils.ts b/server/src/utils/envUtils.ts index 251c913e7..976b01587 100644 --- a/server/src/utils/envUtils.ts +++ b/server/src/utils/envUtils.ts @@ -1,10 +1,58 @@ -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { config } from "dotenv"; let hasLoadedLocalEnv = false; const shouldLogLocalEnvLoading = false; +type ProcessLike = { + env?: Record; +}; + +const getProcessEnv = () => + (globalThis as typeof globalThis & { process?: ProcessLike }).process?.env; + +let injectedEnv: Partial = (getProcessEnv() ?? {}) as Env; + +export const setRuntimeEnv = (env: Env) => { + injectedEnv = env; +}; + +export const setRuntimeEnvFromProcess = () => { + const processEnv = getProcessEnv(); + if (!processEnv) return; + setRuntimeEnv(processEnv as Env); +}; + +export const runtimeEnv = new Proxy({} as Env, { + get(_target, prop: string | symbol) { + if (typeof prop !== "string") return undefined; + return injectedEnv[prop]; + }, + set(_target, prop: string | symbol, value) { + if (typeof prop === "string") { + injectedEnv[prop] = value; + const processEnv = getProcessEnv(); + if (processEnv) processEnv[prop] = value; + } + return true; + }, + has(_target, prop: string | symbol) { + return typeof prop === "string" && prop in injectedEnv; + }, + ownKeys() { + return Reflect.ownKeys(injectedEnv); + }, + getOwnPropertyDescriptor(_target, prop: string | symbol) { + if (typeof prop !== "string" || !(prop in injectedEnv)) return undefined; + return { + configurable: true, + enumerable: true, + value: injectedEnv[prop], + writable: true, + }; + }, +}); + /** * Resolve the directory holding `.env`, robust to cwd: * - cwd already a `server/` dir @@ -14,18 +62,41 @@ const shouldLogLocalEnvLoading = false; */ const resolveServerDir = (): string => { const cwd = process.cwd(); - const candidates = [ - cwd, - join(cwd, "server"), - join(cwd, "autumn", "server"), - ]; + const candidates = [cwd, join(cwd, "server"), join(cwd, "autumn", "server")]; for (const dir of candidates) { if (existsSync(join(dir, "package.json"))) return dir; } - // Fall back to first guess so dotenv silently no-ops if missing. + // Fall back to first guess so local env loading silently no-ops if missing. return cwd.includes("server") ? cwd : join(cwd, "server"); }; +const parseDotenv = (contents: string): Record => { + const parsed: Record = {}; + + for (const rawLine of contents.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + + const normalized = line.startsWith("export ") ? line.slice(7).trim() : line; + const equalsIndex = normalized.indexOf("="); + if (equalsIndex <= 0) continue; + + const key = normalized.slice(0, equalsIndex).trim(); + let value = normalized.slice(equalsIndex + 1).trim(); + + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + parsed[key] = value; + } + + return parsed; +}; + export const loadLocalEnv = ({ force = false }: { force?: boolean } = {}) => { if (hasLoadedLocalEnv && !force) return; hasLoadedLocalEnv = true; @@ -34,25 +105,27 @@ export const loadLocalEnv = ({ force = false }: { force?: boolean } = {}) => { // Determine which env file to load based on ENV_FILE environment variable // Defaults to .env if not specified - const envFileName = process.env.ENV_FILE || ".env"; + const envFileName = runtimeEnv.ENV_FILE || ".env"; const envPath = join(serverDir, envFileName); + const processEnv = getProcessEnv(); // Load local .env file FIRST - these will take precedence over Infisical - const result = config({ path: envPath }); - if (result.parsed) { + if (existsSync(envPath)) { + const parsed = parseDotenv(readFileSync(envPath, "utf8")); if (shouldLogLocalEnvLoading) { // Use stderr so output doesn't pollute stdout for scripts using shell substitution console.error( - `📄 Loading ${Object.keys(result.parsed).length} variables from ${envFileName}`, + `Loading ${Object.keys(parsed).length} variables from ${envFileName}`, ); } - for (const [key, value] of Object.entries(result.parsed)) { - process.env[key] = value; + for (const [key, value] of Object.entries(parsed)) { + runtimeEnv[key] = value; + if (processEnv) processEnv[key] = value; } } else { if (shouldLogLocalEnvLoading) { console.error( - `ℹ️ No ${envFileName} file found (using only Infisical secrets)`, + `No ${envFileName} file found (using only Infisical secrets)`, ); } } diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts index 28577233f..ff95aaeea 100644 --- a/server/src/utils/initUtils.ts +++ b/server/src/utils/initUtils.ts @@ -1,13 +1,12 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { logger } from "@/external/logtail/logtailUtils.js"; -import "dotenv/config"; - export const checkEnvVars = () => { - if (!process.env.DATABASE_URL) { + if (!runtimeEnv.DATABASE_URL) { console.error(`DATABASE_URL is not set`); process.exit(1); } - if (!process.env.ENCRYPTION_IV || !process.env.ENCRYPTION_PASSWORD) { + if (!runtimeEnv.ENCRYPTION_IV || !runtimeEnv.ENCRYPTION_PASSWORD) { console.error( `ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`, ); @@ -15,33 +14,33 @@ export const checkEnvVars = () => { } if ( - !process.env.CACHE_URL && - !process.env.CACHE_URL_US_EAST && - !process.env.CACHE_BACKUP_URL?.trim() + !runtimeEnv.CACHE_URL && + !runtimeEnv.CACHE_URL_US_EAST && + !runtimeEnv.CACHE_BACKUP_URL?.trim() ) { logger.warn( "No Redis URL set (CACHE_URL, CACHE_URL_US_EAST, or CACHE_BACKUP_URL), running without Redis", ); } - if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) { + if (!runtimeEnv.BETTER_AUTH_SECRET || !runtimeEnv.BETTER_AUTH_URL) { console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`); process.exit(1); } - if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) { + if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) { logger.warn( "RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)", ); } - if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { + if (!runtimeEnv.SUPABASE_URL || !runtimeEnv.SUPABASE_SERVICE_KEY) { logger.warn( `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, some actions will be skipped`, ); } - if (!process.env.SVIX_API_KEY) { + if (!runtimeEnv.SVIX_API_KEY) { logger.warn(`SVIX_API_KEY is not set, some actions will be skipped`); return; } diff --git a/server/src/utils/logging/initLogger.ts b/server/src/utils/logging/initLogger.ts index 14ca34d64..48e9e23b6 100644 --- a/server/src/utils/logging/initLogger.ts +++ b/server/src/utils/logging/initLogger.ts @@ -165,12 +165,13 @@ export type InitLoggerOptions = { mode?: "default" | "dual"; }; -export const initLogger = (options: InitLoggerOptions = {}) => { +export const initLogger = (options: InitLoggerOptions, env: Env) => { const { mode = "default" } = options; const streams: pino.StreamEntry[] = []; - const isDev = process.env.NODE_ENV === "development"; - const isTest = process.env.NODE_ENV === "test"; + const nodeEnv = env.NODE_ENV as string | undefined; + const isDev = nodeEnv === "development"; + const isTest = nodeEnv === "test"; const isDevOrTest = isDev || isTest; if (mode === "dual") { @@ -183,14 +184,14 @@ export const initLogger = (options: InitLoggerOptions = {}) => { }) : createConsoleJsonStream(), }); - if (process.env.AXIOM_TOKEN) { + if (env.AXIOM_TOKEN) { streams.push({ level: "info", stream: pino.transport({ target: "@axiomhq/pino", options: { dataset: "express", - token: process.env.AXIOM_TOKEN, + token: env.AXIOM_TOKEN, }, }), }); @@ -204,14 +205,14 @@ export const initLogger = (options: InitLoggerOptions = {}) => { }); } - if (process.env.AXIOM_TOKEN) { + if (env.AXIOM_TOKEN) { streams.push({ level: "info", stream: pino.transport({ target: "@axiomhq/pino", options: { dataset: "express", - token: process.env.AXIOM_TOKEN, + token: env.AXIOM_TOKEN, }, }), }); diff --git a/server/src/utils/memoryMonitor.ts b/server/src/utils/memoryMonitor.ts index cca806d85..32f08a181 100644 --- a/server/src/utils/memoryMonitor.ts +++ b/server/src/utils/memoryMonitor.ts @@ -5,6 +5,7 @@ * Uses Axiom logger so metrics are queryable via type: "memory_log". */ +import { runtimeEnv } from "@/utils/envUtils.js"; import { monitorEventLoopDelay } from "node:perf_hooks"; import { logger } from "../external/logtail/logtailUtils.js"; @@ -27,7 +28,7 @@ function toMB(bytes: number): number { } function logMemoryUsage(label: string) { - if (process.env.NODE_ENV === "development") { + if (runtimeEnv.NODE_ENV === "development") { return; } diff --git a/server/src/utils/otel/FilteringSpanProcessor.ts b/server/src/utils/otel/FilteringSpanProcessor.ts index 6a52406c2..47a53b1aa 100644 --- a/server/src/utils/otel/FilteringSpanProcessor.ts +++ b/server/src/utils/otel/FilteringSpanProcessor.ts @@ -1,3 +1,4 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { type Context, SpanStatusCode } from "@opentelemetry/api"; import type { ReadableSpan, @@ -7,7 +8,7 @@ import type { import { recordSpanDurationMetric } from "./spanMetrics.js"; const REDIS_SUCCESS_SAMPLE_RATE = Number.parseFloat( - process.env.OTEL_REDIS_SUCCESS_SAMPLE_RATE ?? "0.01", + runtimeEnv.OTEL_REDIS_SUCCESS_SAMPLE_RATE ?? "0.01", ); const normalizedRedisSuccessSampleRate = Number.isFinite( diff --git a/server/src/utils/posthog.ts b/server/src/utils/posthog.ts index 232a6cd15..e938a4fca 100644 --- a/server/src/utils/posthog.ts +++ b/server/src/utils/posthog.ts @@ -1,8 +1,9 @@ +import { runtimeEnv } from "@/utils/envUtils.js"; import { PostHog } from "posthog-node"; -const posthogClient = process.env.POSTHOG_API_KEY - ? new PostHog(process.env.POSTHOG_API_KEY, { - host: process.env.POSTHOG_HOST || "https://us.i.posthog.com", +const posthogClient = runtimeEnv.POSTHOG_API_KEY + ? new PostHog(runtimeEnv.POSTHOG_API_KEY, { + host: runtimeEnv.POSTHOG_HOST || "https://us.i.posthog.com", }) : null; diff --git a/server/src/utils/scriptUtils/scriptUtils.ts b/server/src/utils/scriptUtils/scriptUtils.ts index de19029b0..fc35a0710 100644 --- a/server/src/utils/scriptUtils/scriptUtils.ts +++ b/server/src/utils/scriptUtils/scriptUtils.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import fs from "node:fs"; import { ApiVersion, ApiVersionClass, type AppEnv } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; @@ -10,6 +9,7 @@ import { createLogger } from "@/external/logtail/logtailUtils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { ProductService } from "@/internal/products/ProductService.js"; +import { runtimeEnv } from "@/utils/envUtils.js"; import { timeout } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { createReadOnlyStripeCli } from "./readOnlyStripe.js"; @@ -246,7 +246,7 @@ export const initScript = async ({ const stripeCli: Stripe = createStripeCli({ org, env }); - const logger = createLogger(); + const logger = createLogger(runtimeEnv); const req: ExtendedRequest = { orgId, @@ -302,7 +302,7 @@ export const initReadScript = async ({ const stripeCliRaw: Stripe = createStripeCli({ org, env }); const stripeCli = createReadOnlyStripeCli(stripeCliRaw); - const logger = createLogger(); + const logger = createLogger(runtimeEnv); const req: ExtendedRequest = { orgId, diff --git a/server/src/workers.ts b/server/src/workers.ts index bd0f9c67a..69278ae5c 100644 --- a/server/src/workers.ts +++ b/server/src/workers.ts @@ -1,4 +1,4 @@ -import "dotenv/config"; +import { runtimeEnv } from "@/utils/envUtils.js"; import cluster from "node:cluster"; import { initInfisical } from "./external/infisical/initInfisical.js"; @@ -14,7 +14,7 @@ import "./internal/misc/cacheV2Ramp/cacheV2RampStore.js"; import "./internal/misc/jobQueues/jobQueueStore.js"; // Number of worker processes (defaults to CPU cores) -const NUM_PROCESSES = process.env.NODE_ENV === "development" ? 3 : 4; +const NUM_PROCESSES = runtimeEnv.NODE_ENV === "development" ? 3 : 4; // Track if we're shutting down let isShuttingDown = false; @@ -28,7 +28,7 @@ if (cluster.isPrimary) { // await initHatchetWorker(); console.log(`Starting ${NUM_PROCESSES} worker processes`); - console.log(`SQS URL: ${process.env.SQS_QUEUE_URL_V2}`); + console.log(`SQS URL: ${runtimeEnv.SQS_QUEUE_URL_V2}`); // Fork workers for (let i = 0; i < NUM_PROCESSES; i++) { @@ -85,7 +85,7 @@ if (cluster.isPrimary) { `⚠️ Worker ${worker.process.pid} died unexpectedly (${signal || code}). Restarting...`, ); - if (process.env.NODE_ENV === "development") { + if (runtimeEnv.NODE_ENV === "development") { process.exit(1); } else { cluster.fork(); diff --git a/server/tests/clearMasterOrg.ts b/server/tests/clearMasterOrg.ts index 01ea540ff..b6f9773a8 100644 --- a/server/tests/clearMasterOrg.ts +++ b/server/tests/clearMasterOrg.ts @@ -1,9 +1,5 @@ #!/usr/bin/env bun -import dotenv from "dotenv"; - -dotenv.config(); - import { AppEnv, type Organization, organizations } from "@autumn/shared"; import { PurgeQueueCommand } from "@aws-sdk/client-sqs"; import chalk from "chalk"; diff --git a/server/tests/setup/v2Features.ts b/server/tests/setup/v2Features.ts index 56b117301..e84fc7a7e 100644 --- a/server/tests/setup/v2Features.ts +++ b/server/tests/setup/v2Features.ts @@ -1,7 +1,3 @@ -import dotenv from "dotenv"; - -dotenv.config(); - import { AppEnv, FeatureUsageType } from "@autumn/shared"; import { constructAiCreditSystem, diff --git a/server/tests/unit/logging/init-logger-env.test.ts b/server/tests/unit/logging/init-logger-env.test.ts new file mode 100644 index 000000000..6b2c12f32 --- /dev/null +++ b/server/tests/unit/logging/init-logger-env.test.ts @@ -0,0 +1,50 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; +import { afterEach, describe, expect, test } from "bun:test"; +import { initLogger } from "@/utils/logging/initLogger.js"; + +const makeEnv = (overrides: Partial): Env => overrides as Env; +const envDefaultPattern = /\benv\s*:\s*Env\s*=/; + +const collectTypeScriptFiles = (dir: string): string[] => + readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + if (entry.isDirectory()) return collectTypeScriptFiles(path); + if (!entry.isFile()) return []; + return /\.(ts|tsx)$/.test(entry.name) ? [path] : []; + }); + +describe("initLogger env configuration", () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + if (originalNodeEnv === undefined) { + delete process.env.NODE_ENV; + } else { + process.env.NODE_ENV = originalNodeEnv; + } + }); + + test("uses the passed Env instead of process.env to resolve dev mode", () => { + process.env.NODE_ENV = "production"; + + const logger = initLogger( + {}, + makeEnv({ + NODE_ENV: "development", + AXIOM_TOKEN: "", + }), + ); + + expect(logger.level).toBe("debug"); + }); + + test("does not allow Env parameters to define fallback defaults", () => { + const sourceRoot = join(process.cwd(), "src"); + const offenders = collectTypeScriptFiles(sourceRoot) + .filter((file) => envDefaultPattern.test(readFileSync(file, "utf8"))) + .map((file) => relative(process.cwd(), file)); + + expect(offenders).toEqual([]); + }); +}); diff --git a/server/tests/utils/browserPool/browserConfig.ts b/server/tests/utils/browserPool/browserConfig.ts index 60d1c9fde..6d49824ac 100644 --- a/server/tests/utils/browserPool/browserConfig.ts +++ b/server/tests/utils/browserPool/browserConfig.ts @@ -1,4 +1,3 @@ -import "dotenv/config"; import { chromium } from "playwright-core"; // ============================================================================ diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 691b16af4..4642c7834 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -1,5 +1,3 @@ -import "dotenv/config"; - import { BillingInterval, type Customer, diff --git a/server/worker-configuration.d.ts b/server/worker-configuration.d.ts index 559ba5416..ac08cf1af 100644 --- a/server/worker-configuration.d.ts +++ b/server/worker-configuration.d.ts @@ -2,26 +2,28 @@ // Generated by Wrangler by running `wrangler types` (hash: c7c44a1732f99ccf3e3734d18cbbd6f7) // Runtime types generated with workerd@1.20260611.1 2026-06-16 nodejs_compat interface __BaseEnv_Env { - ANTHROPIC_API_KEY: ""; - AXIOM_TOKEN: ""; - BETTER_AUTH_SECRET: ""; - BETTER_AUTH_URL: "http://localhost:8080"; - CLIENT_URL: "http://localhost:3000"; - CRITICAL_DATABASE_URL: ""; - DATABASE_URL: ""; - ENCRYPTION_IV: ""; - ENCRYPTION_PASSWORD: ""; - GOOGLE_CLIENT_ID: ""; - GOOGLE_CLIENT_SECRET: ""; - LOCALTUNNEL_RESERVED_KEY: ""; - NODE_ENV: "production"; - REDIS_URL: "redis://localhost:6379"; - RESEND_API_KEY: ""; - RESEND_DOMAIN: ""; - STRIPE_WEBHOOK_URL: ""; - SUPABASE_SERVICE_KEY: ""; - SUPABASE_URL: ""; - SVIX_API_KEY: ""; + [key: string]: string | undefined; + ANTHROPIC_API_KEY: string; + AWS_REGION: string; + AXIOM_TOKEN: string; + BETTER_AUTH_SECRET: string; + BETTER_AUTH_URL: string; + CLIENT_URL: string; + CRITICAL_DATABASE_URL: string; + DATABASE_URL: string; + ENCRYPTION_IV: string; + ENCRYPTION_PASSWORD: string; + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + LOCALTUNNEL_RESERVED_KEY: string; + NODE_ENV: string; + REDIS_URL: string; + RESEND_API_KEY: string; + RESEND_DOMAIN: string; + STRIPE_WEBHOOK_URL: string; + SUPABASE_SERVICE_KEY: string; + SUPABASE_URL: string; + SVIX_API_KEY: string; } declare namespace Cloudflare { interface GlobalProps { @@ -31,10 +33,10 @@ declare namespace Cloudflare { } interface Env extends __BaseEnv_Env {} type StringifyValues> = { - [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; + [Binding in keyof EnvType]?: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } declare module "*.lua" { const value: string; diff --git a/server/wrangler.jsonc b/server/wrangler.jsonc index cc6176159..862efb9ef 100644 --- a/server/wrangler.jsonc +++ b/server/wrangler.jsonc @@ -21,6 +21,7 @@ }, "vars": { "ANTHROPIC_API_KEY": "", + "AWS_REGION": "", "AXIOM_TOKEN": "", "BETTER_AUTH_SECRET": "", "BETTER_AUTH_URL": "http://localhost:8080",