diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2cec0e34f..aaf6f6537 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,12 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"python.defaultInterpreterPath": "others/python-test/.venv/bin/python",
- "python.analysis.extraPaths": ["others/python-sdk/src"],
- "python.autoComplete.extraPaths": ["others/python-sdk/src"],
+ "python.analysis.extraPaths": [
+ "others/python-sdk/src"
+ ],
+ "python.autoComplete.extraPaths": [
+ "others/python-sdk/src"
+ ],
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
@@ -12,7 +16,7 @@
"source.organizeImports.biome": "explicit"
},
"[typescript]": {
- "editor.defaultFormatter": "biomejs.biome"
+ "editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
@@ -41,4 +45,4 @@
".zed": true
},
"typescript.native-preview.tsdk": "/Users/johnyeocx/autumn/main/node_modules/@typescript/native-preview"
-}
+}
\ No newline at end of file
diff --git a/scripts/fix-env-threading.ts b/scripts/fix-env-threading.ts
new file mode 100644
index 000000000..787fe8a85
--- /dev/null
+++ b/scripts/fix-env-threading.ts
@@ -0,0 +1,72 @@
+#!/usr/bin/env bun
+/**
+ * Bulk migration: runtimeEnv → env, global logger → createLogger(env)
+ */
+import { readFileSync, writeFileSync } from "node:fs";
+import { globSync } from "bun:fs";
+
+const SRC = "/Volumes/sker/autumn/server/src";
+
+const files = globSync(`${SRC}/**/*.ts`).filter(
+ (f) => !f.includes("node_modules") && !f.includes(".test."),
+);
+
+let changed = 0;
+
+for (const f of files) {
+ let content = readFileSync(f, "utf-8");
+ const original = content;
+ let modified = false;
+
+ // ── Remove runtimeEnv imports ──
+ if (content.match(/import\s+\{[^}]*runtimeEnv[^}]*\}\s+from\s+["'][^"']*envUtils[^"']*["'];?/)) {
+ content = content.replace(
+ /^import\s+\{[^}]*runtimeEnv[^}]*\}\s+from\s+["'][^"']*envUtils[^"']*["'];?\s*\n/gm,
+ "",
+ );
+ modified = true;
+ }
+
+ // ── Replace runtimeEnv.XXX → env.XXX ──
+ if (content.includes("runtimeEnv.")) {
+ content = content.replace(/runtimeEnv\./g, "env.");
+ modified = true;
+ }
+
+ // ── Replace import { logger } with import { createLogger } ──
+ if (content.match(/import\s+\{[^}]*\blogger\b[^}]*\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/)) {
+ // Add createLogger to the existing import, or create new import
+ if (content.includes("import { createLogger }")) {
+ // Already has createLogger, just remove logger
+ content = content.replace(
+ /import\s+\{([^}]*)logger([^}]*)\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/g,
+ (_, p1, p2) => {
+ const parts = [p1.trim(), p2.trim()].filter(Boolean).join(", ");
+ return parts
+ ? `import { ${parts} } from "@/external/logtail/logtailUtils.js";`
+ : 'import { createLogger } from "@/external/logtail/logtailUtils.js";';
+ },
+ );
+ } else {
+ content = content.replace(
+ /import\s+\{([^}]*)logger\b([^}]*)\}\s+from\s+["'][^"']*logtailUtils[^"']*["'];?/g,
+ (_, p1, p2) => {
+ const hasOther = (p1 + p2).replace(/\s+/g, "").length > 0;
+ if (hasOther) {
+ return `import { createLogger, ${[p1, p2].map(s => s.trim()).filter(Boolean).join(", ")} } from "@/external/logtail/logtailUtils.js";`;
+ }
+ return 'import { createLogger } from "@/external/logtail/logtailUtils.js";';
+ },
+ );
+ }
+ modified = true;
+ }
+
+ if (modified) {
+ writeFileSync(f, content);
+ changed++;
+ console.log(`✓ ${f.replace(SRC, "")}`);
+ }
+}
+
+console.log(`\nChanged ${changed} files.`);
diff --git a/scripts/fix-logger.ts b/scripts/fix-logger.ts
new file mode 100644
index 000000000..a5c0d9089
--- /dev/null
+++ b/scripts/fix-logger.ts
@@ -0,0 +1,88 @@
+#!/usr/bin/env bun
+/**
+ * Bulk fix: adds const logger = createLogger(env) to files that import
+ * createLogger and use logger.X but don't define logger locally.
+ */
+import { readFileSync, writeFileSync } from "node:fs";
+
+const FILES = process.argv.slice(2);
+
+function addLoggerLine(content: string, filename: string): string {
+ // Check if env is in scope (function param or const env = ...)
+ const hasEnv = /\benv\s*[:=]\s*\w+\s*Env\b/.test(content) ||
+ /\bc\.env\b/.test(content) ||
+ /\benv\./.test(content);
+
+ if (!hasEnv) {
+ console.log(` SKIP (no env in scope): ${filename}`);
+ return content;
+ }
+
+ // Add const logger = createLogger(env); before first logger. usage
+ // Find the function body where logger is used
+ const lines = content.split("\n");
+ const result: string[] = [];
+ let inLoggerFunc = false;
+ let braceDepth = 0;
+ let foundLine: number | null = null;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // Check if this is a function definition
+ if (/(?:function\s+\w+|=>\s*\{|\)\s*=>|\) =>|\):.*=>)\s*\{?/.test(line)) {
+ inLoggerFunc = false;
+ braceDepth = 0;
+ }
+
+ if (line.includes("logger.") && !line.trim().startsWith("//") && !line.includes("const logger") && !line.includes("let logger") && !line.includes("logger:")) {
+ if (!foundLine) {
+ foundLine = i;
+ }
+ // logger used but not defined — go back and add definition
+ }
+ }
+
+ // Simple approach: find the first { after export/signature and add logger
+ // This is heuristic and may need manual review
+ let firstBrace = false;
+ let fixed = false;
+ const newLines: string[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ if (!fixed && line.includes("logger.") && !line.trim().startsWith("//") && !line.includes("const logger") && !line.includes("let logger") && !line.includes("logger:") && !line.includes("import ")) {
+ // Go back to find the function body start and insert logger definition
+ let insertAt = i;
+ for (let j = i - 1; j >= 0; j--) {
+ if (lines[j].includes("{") && /\b(?:function|=>|async|export|const\s+\w+\s*=|let\s+\w+\s*=)\b/.test(lines.slice(Math.max(0, j-5), j+1).join(" "))) {
+ insertAt = j + 1;
+ break;
+ }
+ }
+ // Insert before the first use
+ // Adjust indentation
+ const indent = lines[insertAt].match(/^(\s*)/)?.[1] || "";
+ newLines.push(`${indent}const logger = createLogger(env);`);
+ console.log(` FIXED: ${filename}`);
+ fixed = true;
+ // Now continue adding remaining lines
+ for (let k = i; k < lines.length; k++) {
+ newLines.push(lines[k]);
+ }
+ break;
+ }
+ newLines.push(line);
+ }
+
+ return fixed ? newLines.join("\n") : content;
+}
+
+for (const f of FILES) {
+ let content = readFileSync(f, "utf-8");
+ const newContent = addLoggerLine(content, f);
+ if (newContent !== content) {
+ writeFileSync(f, newContent);
+ }
+}
diff --git a/scripts/migrations/validate-schema.ts b/scripts/migrations/validate-schema.ts
index c32b23935..5cc2c8eb8 100644
--- a/scripts/migrations/validate-schema.ts
+++ b/scripts/migrations/validate-schema.ts
@@ -11,7 +11,7 @@ const validateContent = process.argv.includes("--validate-content");
try {
console.log("Validating database schema...");
- await validateDbSchema({ db });
+ await validateDbSchema({ db, env: process.env as unknown as Env });
console.log("✅ Database schema validated successfully\n");
console.log(
diff --git a/scripts/preload-env.ts b/scripts/preload-env.ts
index 9ee5a58ff..b0021832a 100644
--- a/scripts/preload-env.ts
+++ b/scripts/preload-env.ts
@@ -3,9 +3,6 @@
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
-import { loadLocalEnv } from "@server/utils/envUtils.js";
-
-loadLocalEnv();
// Worktree-aware: `bun dw` writes per-worktree `.env.local` files to each
// workspace dir. PW_MODE=1 (set by `bun pw`) skips this so prod Infisical
diff --git a/server/experiments/experimentEnv.ts b/server/experiments/experimentEnv.ts
index ed4c76502..d5c2aa547 100644
--- a/server/experiments/experimentEnv.ts
+++ b/server/experiments/experimentEnv.ts
@@ -1,7 +1,3 @@
-import { loadLocalEnv } from "../src/utils/envUtils";
-
-loadLocalEnv();
-
const requireEnv = ({ key }: { key: string }) => {
const value = process.env[key];
diff --git a/server/experiments/normalizedSubjectCacheExperiment.ts b/server/experiments/normalizedSubjectCacheExperiment.ts
index 9fc8daecc..34a7f33c8 100644
--- a/server/experiments/normalizedSubjectCacheExperiment.ts
+++ b/server/experiments/normalizedSubjectCacheExperiment.ts
@@ -1,6 +1,3 @@
-import { loadLocalEnv } from "../src/utils/envUtils";
-
-loadLocalEnv();
import {
AppEnv,
diff --git a/server/perf/load-test/setup.ts b/server/perf/load-test/setup.ts
index cc2b36315..458907652 100644
--- a/server/perf/load-test/setup.ts
+++ b/server/perf/load-test/setup.ts
@@ -5,9 +5,6 @@
* or: ENV_FILE=.env infisical run --recursive --env=dev -- bun perf/load-test/setup.ts
*/
-import { loadLocalEnv } from "../../src/utils/envUtils.js";
-loadLocalEnv();
-
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { ApiVersion, AppEnv, BillingInterval } from "@autumn/shared";
diff --git a/server/perf/redis-bench/setup.ts b/server/perf/redis-bench/setup.ts
index 21a657d4e..0f6435615 100644
--- a/server/perf/redis-bench/setup.ts
+++ b/server/perf/redis-bench/setup.ts
@@ -6,9 +6,6 @@
* Run: cd server && bun perf/redis-bench/setup.ts
*/
-import { loadLocalEnv } from "../../src/utils/envUtils.js";
-loadLocalEnv();
-
import { AppEnv, ApiVersion, type FullCustomer } from "@autumn/shared";
import { customerEntitlements } from "@tests/utils/fixtures/db/customerEntitlements.js";
import { customerProducts } from "@tests/utils/fixtures/db/customerProducts.js";
diff --git a/server/src/cron.ts b/server/src/cron.ts
index 2f7278956..a8cab055e 100644
--- a/server/src/cron.ts
+++ b/server/src/cron.ts
@@ -1,28 +1,33 @@
import { initInfisical } from "./external/infisical/initInfisical.js";
+import { createLogger } from "./external/logtail/logtailUtils.js";
-await initInfisical();
-const { warmupRegionalRedis } = await import("./external/redis/initRedis.js");
-await warmupRegionalRedis();
+export const startCron = async (initialEnv: Env) => {
+ const env = await initInfisical(initialEnv);
+ const logger = createLogger(env);
-// Edge config modules self-register on import (cron reads redis-v2-cache
-// so resolveRedisV2 picks the right instance on each ctx build).
-await import("./internal/misc/redisV2Cache/redisV2CacheStore.js");
-await import("./internal/misc/cacheV2Ramp/cacheV2RampStore.js");
-const { logger } = await import("./external/logtail/logtailUtils.js");
-const { startAllEdgeConfigPolling } = await import(
- "./internal/misc/edgeConfig/edgeConfigRegistry.js"
-);
-await startAllEdgeConfigPolling({ logger });
+ const { warmupRegionalRedis } = await import("./external/redis/initRedis.js");
+ await warmupRegionalRedis();
-// Resolve AWS task identity + start polling the cron blue-green slot store
-// so `isActiveSlot({ serviceName: "cron" })` in cronInit reads fresh data.
-const { resolveAwsTaskIdentity } = await import(
- "./external/aws/ecs/awsTaskIdentity.js"
-);
-await resolveAwsTaskIdentity();
-const { startBlueGreenSlotStorePolling } = await import(
- "./queue/blueGreen/blueGreenSlotStore.js"
-);
-await startBlueGreenSlotStorePolling({ serviceName: "cron", logger });
+ // Edge config modules self-register on import (cron reads redis-v2-cache
+ // so resolveRedisV2 picks the right instance on each ctx build).
+ await import("./internal/misc/redisV2Cache/redisV2CacheStore.js");
+ await import("./internal/misc/cacheV2Ramp/cacheV2RampStore.js");
+ const { startAllEdgeConfigPolling } = await import(
+ "./internal/misc/edgeConfig/edgeConfigRegistry.js"
+ );
+ await startAllEdgeConfigPolling({ logger });
-await import("./cron/cronInit.js");
+ // Resolve AWS task identity + start polling the cron blue-green slot store
+ // so `isActiveSlot({ serviceName: "cron" })` in cronInit reads fresh data.
+ const { resolveAwsTaskIdentity } = await import(
+ "./external/aws/ecs/awsTaskIdentity.js"
+ );
+ await resolveAwsTaskIdentity(env);
+ const { startBlueGreenSlotStorePolling } = await import(
+ "./queue/blueGreen/blueGreenSlotStore.js"
+ );
+ await startBlueGreenSlotStorePolling({ serviceName: "cron", logger });
+
+ const { startCronInit } = await import("./cron/cronInit.js");
+ await startCronInit(env);
+};
diff --git a/server/src/cron/cronInit.ts b/server/src/cron/cronInit.ts
index c1dadc12b..268b1d29d 100644
--- a/server/src/cron/cronInit.ts
+++ b/server/src/cron/cronInit.ts
@@ -1,9 +1,8 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import "../sentry.ts";
import { CronJob } from "cron";
import { initDrizzle } from "../db/initDrizzle.js";
import { startPgPoolMonitor, stopPgPoolMonitor } from "../db/pgPoolMonitor.js";
-import { logger } from "../external/logtail/logtailUtils.js";
+import { createLogger } from "../external/logtail/logtailUtils.js";
+import type { Logger } from "../external/logtail/logtailUtils.js";
import {
describeSlotGate,
isActiveSlot,
@@ -20,79 +19,83 @@ import { runProductCron } from "./productCron/runProductCron.js";
import { runResetCron } from "./resetCron/runResetCron.js";
import type { CronContext } from "./utils/CronContext.js";
-const { db, client } = initDrizzle({ name: "cron", maxConnections: 40 });
-startPgPoolMonitor();
-startBlueGreenHeartbeat({ db, logger, serviceName: "cron" });
+export const startCronInit = (env: Env) => {
+ const logger = createLogger(env);
-const logCronHeartbeat = () => {
- logger.info(
- {
- type: "cron_heartbeat",
- cron: {
- pid: process.pid,
- timezone: "UTC",
+ const { db, client } = initDrizzle({ name: "cron", maxConnections: 40 });
+ startPgPoolMonitor(env);
+ startBlueGreenHeartbeat({ db, logger, serviceName: "cron" });
+
+ const logCronHeartbeat = () => {
+ logger.info(
+ {
+ type: "cron_heartbeat",
+ cron: {
+ pid: process.pid,
+ timezone: "UTC",
+ },
},
- },
- "Cron heartbeat",
- );
-};
-
-const main = async () => {
- if (runtimeEnv.DISABLE_CRON === "true") {
- console.log(`Cron disabled!`);
- return;
- }
-
- // Blue-green gate: skip the tick on the idle task set so a swap doesn't
- // double-fire jobs. Fail-open on non-AWS hosts (no task identity).
- if (!isActiveSlot({ serviceName: "cron" })) {
- const reason = describeSlotGate({ serviceName: "cron" });
- logger.info("Cron tick skipped (idle slot)", {
- type: "cron_skipped_idle",
- gate: reason,
- });
- return;
- }
-
- logCronHeartbeat();
-
- const ctx: CronContext = {
- db,
- logger,
+ "Cron heartbeat",
+ );
};
- await Promise.all([
- runProductCron({ ctx }),
- runResetCron({ ctx }),
- runInvoiceCron({ ctx }),
- runOneOffCleanup({ ctx }),
- runOneOffExpiry({ ctx }),
- ]);
+
+ const main = async () => {
+ if (env.DISABLE_CRON === "true") {
+ console.log(`Cron disabled!`);
+ return;
+ }
+
+ // Blue-green gate: skip the tick on the idle task set so a swap doesn't
+ // double-fire jobs. Fail-open on non-AWS hosts (no task identity).
+ if (!isActiveSlot({ serviceName: "cron" })) {
+ const reason = describeSlotGate({ serviceName: "cron" });
+ logger.info("Cron tick skipped (idle slot)", {
+ type: "cron_skipped_idle",
+ gate: reason,
+ });
+ return;
+ }
+
+ logCronHeartbeat();
+
+ const ctx: CronContext = {
+ db,
+ logger,
+ };
+ await Promise.all([
+ runProductCron({ ctx }),
+ runResetCron({ ctx }),
+ runInvoiceCron({ ctx }),
+ runOneOffCleanup({ ctx }),
+ runOneOffExpiry({ ctx }),
+ ]);
+ };
+
+ new CronJob(
+ "* * * * *",
+ main,
+ null,
+ true,
+ "UTC",
+ );
+
+ main();
+
+ process.on("SIGTERM", async () => {
+ console.log("Received SIGTERM signal, closing database connection...");
+ stopPgPoolMonitor();
+ stopBlueGreenHeartbeat({ serviceName: "cron" });
+ stopBlueGreenSlotStorePolling({ serviceName: "cron" });
+ await client.end();
+ process.exit(0);
+ });
+
+ process.on("SIGINT", async () => {
+ console.log("Received SIGINT signal, closing database connection...");
+ stopPgPoolMonitor();
+ stopBlueGreenHeartbeat({ serviceName: "cron" });
+ stopBlueGreenSlotStorePolling({ serviceName: "cron" });
+ await client.end();
+ process.exit(0);
+ });
};
-
-new CronJob(
- "* * * * *", // Run every minute
- main,
- null, // onComplete
- true, // start immediately
- "UTC", // timezone (adjust as needed)
-);
-
-main();
-
-process.on("SIGTERM", async () => {
- console.log("Received SIGTERM signal, closing database connection...");
- stopPgPoolMonitor();
- stopBlueGreenHeartbeat({ serviceName: "cron" });
- stopBlueGreenSlotStorePolling({ serviceName: "cron" });
- await client.end();
- process.exit(0);
-});
-
-process.on("SIGINT", async () => {
- console.log("Received SIGINT signal, closing database connection...");
- stopPgPoolMonitor();
- stopBlueGreenHeartbeat({ serviceName: "cron" });
- stopBlueGreenSlotStorePolling({ serviceName: "cron" });
- await client.end();
- process.exit(0);
-});
diff --git a/server/src/db/dbUtils.ts b/server/src/db/dbUtils.ts
index aeeacb65a..93ff2089e 100644
--- a/server/src/db/dbUtils.ts
+++ b/server/src/db/dbUtils.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { getTableColumns, type SQL, sql } from "drizzle-orm";
import type { PgTable } from "drizzle-orm/pg-core";
@@ -116,7 +115,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 = runtimeEnv.DATABASE_URL || "") => {
+export const assertNotProductionDb = (env: Env, url = env.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 6af0da13c..d8a097800 100644
--- a/server/src/db/initDrizzle.ts
+++ b/server/src/db/initDrizzle.ts
@@ -3,8 +3,7 @@ import { instrumentDrizzleClient } from "@kubiks/otel-drizzle";
import type { SQLWrapper } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import pg, { type PoolConfig } from "pg";
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { logger } from "../external/logtail/logtailUtils.js";
+import { createLogger } from "../external/logtail/logtailUtils.js";
import { otelConfig } from "../utils/otel/otelConfig.js";
import { attachPoolErrorHandlers, registerPool } from "./pgPoolMonitor.js";
@@ -37,7 +36,7 @@ const normalizeDbExecute = <
/** Creates a Drizzle pool with the given configuration. */
export const initDrizzle = ({
- maxConnections = isProd ? 70 : 10,
+ maxConnections = 10,
replica = false,
connectTimeout = 5,
databaseUrl,
@@ -53,11 +52,7 @@ export const initDrizzle = ({
/** Pool name for monitor/error logs. Omit to skip registration. */
name?: string;
} = {}) => {
- const envDbUrl = replica
- ? runtimeEnv.DATABASE_REPLICA_URL
- : runtimeEnv.DATABASE_URL;
-
- const dbUrl = databaseUrl || envDbUrl || "";
+ const dbUrl = databaseUrl || "";
const client = new pg.Pool({
connectionString: dbUrl,
@@ -91,97 +86,118 @@ export const initDrizzle = ({
return { db, client };
};
-// Strict latency limits in prod; relaxed locally so dev pool warm-up doesn't kill tests.
-const isProd = runtimeEnv.NODE_ENV === "production";
+export type DrizzleCli = ReturnType["db"];
-const poolMaxFromEnv = ({
- envVar,
- fallback,
-}: {
- envVar:
- | "CRITICAL_DB_POOL_MAX"
- | "GENERAL_DB_POOL_MAX"
- | "REPLICA_DB_POOL_MAX";
- fallback: number;
-}): number => {
- const parsed = Number(runtimeEnv[envVar]);
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
-};
+// ── Lazy-initialized pool singletons ──────────────────────────────────
+
+let _initialized = false;
+
+export let client: ReturnType["client"];
+export let db: DrizzleCli;
+export let clientCritical: ReturnType["client"];
+export let dbCritical: DrizzleCli;
+export let clientGeneral: ReturnType["client"];
+export let dbGeneral: DrizzleCli;
+export let clientReplica: ReturnType["client"] | null = null;
+export let dbReplica: DrizzleCli | null = null;
const PGBOUNCER_MAX_CLIENT_CONN = 7_600;
const BUDGETED_FLEET_PROCESSES = 150;
const BUDGETED_NON_SERVER_CONNECTIONS = 80;
const POOL_BUDGET_HEADROOM = 0.85;
-const PROD_POOL_MAX = {
- critical: 22,
- general: 14,
- replica: 6,
+const poolMaxFromEnv = (
+ env: Env,
+ envVar: "CRITICAL_DB_POOL_MAX" | "GENERAL_DB_POOL_MAX" | "REPLICA_DB_POOL_MAX",
+ fallback: number,
+): number => {
+ const parsed = Number(env[envVar]);
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
};
-const criticalPoolMax = poolMaxFromEnv({
- envVar: "CRITICAL_DB_POOL_MAX",
- fallback: isProd ? PROD_POOL_MAX.critical : 10,
-});
-const generalPoolMax = poolMaxFromEnv({
- envVar: "GENERAL_DB_POOL_MAX",
- fallback: isProd ? PROD_POOL_MAX.general : 10,
-});
-const replicaPoolMax = poolMaxFromEnv({
- envVar: "REPLICA_DB_POOL_MAX",
- fallback: PROD_POOL_MAX.replica,
-});
+/**
+ * Initialize all database pools with the given env bindings.
+ * Must be called once at startup before any other module accesses
+ * the `db`, `client`, etc. exports.
+ */
+export const initDrizzleModules = (env: Env) => {
+ if (_initialized) return;
+ _initialized = true;
-const budgetedFleetConnections =
- BUDGETED_FLEET_PROCESSES *
- (criticalPoolMax + generalPoolMax + replicaPoolMax) +
- BUDGETED_NON_SERVER_CONNECTIONS;
+ const logger = createLogger(env);
+ const isProd = env.NODE_ENV === "production";
-if (
- budgetedFleetConnections >
- PGBOUNCER_MAX_CLIENT_CONN * POOL_BUDGET_HEADROOM
-) {
- logger.warn(
- `[initDrizzle] pool budget (${budgetedFleetConnections}) exceeds ${POOL_BUDGET_HEADROOM} of max_client_conn (${PGBOUNCER_MAX_CLIENT_CONN}) — lower the pool maxes or raise the ceiling`,
+ const PROD_POOL_MAX = {
+ critical: 22,
+ general: 14,
+ replica: 6,
+ };
+
+ const criticalPoolMax = poolMaxFromEnv(
+ env,
+ "CRITICAL_DB_POOL_MAX",
+ isProd ? PROD_POOL_MAX.critical : 10,
+ );
+ const generalPoolMax = poolMaxFromEnv(
+ env,
+ "GENERAL_DB_POOL_MAX",
+ isProd ? PROD_POOL_MAX.general : 10,
+ );
+ const replicaPoolMax = poolMaxFromEnv(
+ env,
+ "REPLICA_DB_POOL_MAX",
+ PROD_POOL_MAX.replica,
);
-}
-export const { db: dbCritical, client: clientCritical } = initDrizzle({
- name: "critical",
- maxConnections: criticalPoolMax,
- connectTimeout: isProd ? 2 : 30,
- databaseUrl: runtimeEnv.DATABASE_CRITICAL_URL,
- poolConfig: {
- application_name: "autumn-critical",
- query_timeout: isProd ? 2_000 : 30_000,
- // Keep warm conns to avoid TLS-handshake stampedes on bursty traffic.
- min: Math.min(10, criticalPoolMax),
- },
-});
+ const budgetedFleetConnections =
+ BUDGETED_FLEET_PROCESSES *
+ (criticalPoolMax + generalPoolMax + replicaPoolMax) +
+ BUDGETED_NON_SERVER_CONNECTIONS;
-// -- General pool: used by all other endpoints --
-export const { db: dbGeneral, client: clientGeneral } = initDrizzle({
- name: "general",
- maxConnections: generalPoolMax,
- connectTimeout: isProd ? 5 : 30,
-});
+ if (
+ budgetedFleetConnections >
+ PGBOUNCER_MAX_CLIENT_CONN * POOL_BUDGET_HEADROOM
+ ) {
+ logger.warn(
+ `[initDrizzle] pool budget (${budgetedFleetConnections}) exceeds ${POOL_BUDGET_HEADROOM} of max_client_conn (${PGBOUNCER_MAX_CLIENT_CONN}) — lower the pool maxes or raise the ceiling`,
+ );
+ }
-// -- Replica pool: used as fallback when primary is degraded --
-// Only created if DATABASE_REPLICA_URL is configured.
-const replicaResult = runtimeEnv.DATABASE_REPLICA_URL
- ? initDrizzle({
+ const criticalResult = initDrizzle({
+ name: "critical",
+ maxConnections: criticalPoolMax,
+ connectTimeout: isProd ? 2 : 30,
+ databaseUrl: env.DATABASE_CRITICAL_URL,
+ poolConfig: {
+ application_name: "autumn-critical",
+ query_timeout: isProd ? 2_000 : 30_000,
+ min: Math.min(10, criticalPoolMax),
+ },
+ });
+ dbCritical = criticalResult.db;
+ clientCritical = criticalResult.client;
+
+ const generalResult = initDrizzle({
+ name: "general",
+ maxConnections: generalPoolMax,
+ connectTimeout: isProd ? 5 : 30,
+ databaseUrl: env.DATABASE_URL,
+ });
+ dbGeneral = generalResult.db;
+ clientGeneral = generalResult.client;
+
+ if (env.DATABASE_REPLICA_URL) {
+ const replicaResult = initDrizzle({
name: "replica",
replica: true,
maxConnections: replicaPoolMax,
+ databaseUrl: env.DATABASE_REPLICA_URL,
connectTimeout: null,
- })
- : null;
-export const dbReplica = replicaResult?.db ?? null;
-export const clientReplica = replicaResult?.client ?? null;
+ });
+ dbReplica = replicaResult.db;
+ clientReplica = replicaResult.client;
+ }
-// Backward-compatible exports — existing code that imports `db` or `client`
-// gets the general pool automatically.
-export const client = clientGeneral;
-export const db = dbGeneral;
-
-export type DrizzleCli = ReturnType["db"];
+ client = clientGeneral;
+ db = dbGeneral;
+};
diff --git a/server/src/db/pgHealthMonitor.ts b/server/src/db/pgHealthMonitor.ts
index 5799ed528..519b8e84f 100644
--- a/server/src/db/pgHealthMonitor.ts
+++ b/server/src/db/pgHealthMonitor.ts
@@ -1,7 +1,7 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { SQL } from "drizzle-orm";
import type { Pool } from "pg";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
+import type { Logger } from "@/external/logtail/logtailUtils.js";
import { isConnectionDropError } from "./dbUtils.js";
import { type DrizzleCli, dbCritical, dbReplica } from "./initDrizzle.js";
@@ -38,6 +38,17 @@ let probeClient: Pool | null = null;
let failureCount = 0;
let windowStartedAt = Date.now();
+// Lazily initialized from initPgHealthMonitor
+let _logger: Logger | null = null;
+let _hasReplica = false;
+
+const getLogger = (): Logger => {
+ if (!_logger) {
+ throw new Error("PgHealthMonitor not initialized — call initPgHealthMonitor first");
+ }
+ return _logger;
+};
+
const resetFailureWindow = (now = Date.now()) => {
failureCount = 0;
windowStartedAt = now;
@@ -46,49 +57,12 @@ const resetFailureWindow = (now = Date.now()) => {
/** Get the current DB health state. */
export const getDbHealth = (): PgHealth => state;
-/**
- * Initialize the health monitor with a pg pool for probing.
- * Call once at startup. The probe client should be the critical pool's raw client.
- */
-export const initPgHealthMonitor = ({ client }: { client: Pool }): void => {
- probeClient = client;
- logger.info("[PgHealthMonitor] Initialized", { type: "pg_health_init" });
-};
-
-/**
- * Record a successful query against the primary DB.
- * No-op: recovery is driven by the probe, not by application queries.
- */
-export const recordDbSuccess = (): void => {};
-
-/**
- * Record a slow or failed query against the primary DB.
- * Called when a critical-pool query exceeds the latency threshold or throws.
- */
-export const recordDbFailure = (): void => {
- if (state === PgHealth.Degraded) return;
-
- const now = Date.now();
-
- // Reset counter if the window has elapsed
- if (now - windowStartedAt > FAILURE_WINDOW_MS) {
- failureCount = 0;
- windowStartedAt = now;
- }
-
- failureCount++;
-
- if (failureCount >= FAILURE_THRESHOLD) {
- switchToDegraded();
- }
-};
-
const switchToDegraded = (): void => {
if (state === PgHealth.Degraded) return;
state = PgHealth.Degraded;
- logger.error("[PgHealthMonitor] ENTERING DEGRADED MODE", {
+ getLogger().error("[PgHealthMonitor] ENTERING DEGRADED MODE", {
type: "pg_health_degraded",
failureCount,
windowMs: FAILURE_WINDOW_MS,
@@ -107,7 +81,7 @@ const switchToHealthy = (): void => {
resetFailureWindow();
firstProbeSuccessAt = null;
- logger.info("[PgHealthMonitor] RECOVERED to HEALTHY", {
+ getLogger().info("[PgHealthMonitor] RECOVERED to HEALTHY", {
type: "pg_health_recovered",
});
@@ -137,7 +111,7 @@ const startProbe = (): void => {
const now = Date.now();
if (!firstProbeSuccessAt) {
firstProbeSuccessAt = now;
- logger.info(
+ getLogger().info(
"[PgHealthMonitor] Probe succeeded, waiting for stability",
{
type: "pg_health_probe",
@@ -151,7 +125,7 @@ const startProbe = (): void => {
} catch {
// Probe failed — reset stability timer
if (firstProbeSuccessAt) {
- logger.warn(
+ getLogger().warn(
"[PgHealthMonitor] Probe failed, resetting stability timer",
{
type: "pg_health_probe",
@@ -172,6 +146,51 @@ const stopProbe = (): void => {
}
};
+/**
+ * Initialize the health monitor with a pg pool for probing and env for logging.
+ * Call once at startup.
+ */
+export const initPgHealthMonitor = ({
+ client,
+ env,
+}: {
+ client: Pool;
+ env: Env;
+}): void => {
+ probeClient = client;
+ _logger = createLogger(env);
+ _hasReplica = !!env.DATABASE_REPLICA_URL;
+ getLogger().info("[PgHealthMonitor] Initialized", { type: "pg_health_init" });
+};
+
+/**
+ * Record a successful query against the primary DB.
+ * No-op: recovery is driven by the probe, not by application queries.
+ */
+export const recordDbSuccess = (): void => {};
+
+/**
+ * Record a slow or failed query against the primary DB.
+ * Called when a critical-pool query exceeds the latency threshold or throws.
+ */
+export const recordDbFailure = (): void => {
+ if (state === PgHealth.Degraded) return;
+
+ const now = Date.now();
+
+ // Reset counter if the window has elapsed
+ if (now - windowStartedAt > FAILURE_WINDOW_MS) {
+ failureCount = 0;
+ windowStartedAt = now;
+ }
+
+ failureCount++;
+
+ if (failureCount >= FAILURE_THRESHOLD) {
+ switchToDegraded();
+ }
+};
+
/**
* Execute a query with automatic health tracking and replica fallback.
* - If DEGRADED and a replica exists, queries the replica instead.
@@ -244,14 +263,14 @@ export const getPgHealthState = (): {
failureCount,
probeActive: probeInterval !== null,
firstProbeSuccessAt,
- hasReplica: !!runtimeEnv.DATABASE_REPLICA_URL,
+ hasReplica: _hasReplica,
});
/** Force DEGRADED state (for testing). Does NOT start the recovery probe. */
export const forceDegraded = (): void => {
state = PgHealth.Degraded;
resetFailureWindow();
- logger.info("[PgHealthMonitor] FORCE DEGRADED (test)", {
+ getLogger().info("[PgHealthMonitor] FORCE DEGRADED (test)", {
type: "pg_health_force",
});
};
@@ -262,7 +281,7 @@ export const forceHealthy = (): void => {
resetFailureWindow();
firstProbeSuccessAt = null;
stopProbe();
- logger.info("[PgHealthMonitor] FORCE HEALTHY (test)", {
+ getLogger().info("[PgHealthMonitor] FORCE HEALTHY (test)", {
type: "pg_health_force",
});
};
diff --git a/server/src/db/pgPoolMonitor.ts b/server/src/db/pgPoolMonitor.ts
index 6504904b6..d8358cbfd 100644
--- a/server/src/db/pgPoolMonitor.ts
+++ b/server/src/db/pgPoolMonitor.ts
@@ -1,6 +1,6 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { Pool } from "pg";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
+import type { Logger } from "@/external/logtail/logtailUtils.js";
type RegisteredPool = {
pool: Pool;
@@ -11,12 +11,18 @@ type RegisteredPool = {
const registry = new Map();
let snapshotInterval: ReturnType | null = null;
-const getRole = (): string => {
- if (runtimeEnv.WORKER === "true") return "worker";
- if (runtimeEnv.CRON === "true") return "cron";
- return "http";
+let _logger: Logger | null = null;
+let _role = "http";
+
+const getLogger = (): Logger => {
+ if (!_logger) {
+ throw new Error("PgPoolMonitor not initialized — call startPgPoolMonitor first");
+ }
+ return _logger;
};
+const getRole = (): string => _role;
+
export const registerPool = ({
pool,
name,
@@ -37,7 +43,7 @@ export const attachPoolErrorHandlers = ({
name: string;
}): void => {
pool.on("error", (err: Error & { code?: string }) => {
- logger.warn("pg_pool_error", {
+ getLogger().warn("pg_pool_error", {
type: "pg_pool_error",
pool: name,
pid: process.pid,
@@ -49,31 +55,20 @@ export const attachPoolErrorHandlers = ({
});
};
-const emitSnapshot = (): void => {
- // const role = getRole();
- // for (const { pool, name, max } of registry.values()) {
- // const totalCount = pool.totalCount;
- // const idleCount = pool.idleCount;
- // const waitingCount = pool.waitingCount;
- // logger.debug("pg_pool_stats", {
- // type: "pg_pool_stats",
- // pool: name,
- // pid: process.pid,
- // role,
- // totalCount,
- // idleCount,
- // waitingCount,
- // max,
- // utilization: max > 0 ? totalCount / max : 0,
- // });
- // }
-};
+const emitSnapshot = (): void => {};
-export const startPgPoolMonitor = (intervalMs = 30_000): void => {
+export const startPgPoolMonitor = (env: Env, intervalMs = 30_000): void => {
if (snapshotInterval) return;
+
+ _logger = createLogger(env);
+
+ if (env.WORKER === "true") _role = "worker";
+ else if (env.CRON === "true") _role = "cron";
+ else _role = "http";
+
snapshotInterval = setInterval(emitSnapshot, intervalMs);
if (snapshotInterval.unref) snapshotInterval.unref();
- logger.info("[PgPoolMonitor] Started", {
+ getLogger().info("[PgPoolMonitor] Started", {
type: "pg_pool_monitor_start",
intervalMs,
pools: Array.from(registry.keys()),
diff --git a/server/src/db/redactDatabaseUrl.ts b/server/src/db/redactDatabaseUrl.ts
index 1228a0bad..a6fec1803 100644
--- a/server/src/db/redactDatabaseUrl.ts
+++ b/server/src/db/redactDatabaseUrl.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { createHash } from "node:crypto";
const hash = (value: string) =>
@@ -33,10 +32,10 @@ export const redactDatabaseUrl = (databaseUrl?: string) => {
}
};
-export const getRedactedDatabaseUrls = () => ({
- primary: redactDatabaseUrl(runtimeEnv.DATABASE_URL),
- replica: redactDatabaseUrl(runtimeEnv.DATABASE_REPLICA_URL),
+export const getRedactedDatabaseUrls = (env: Env) => ({
+ primary: redactDatabaseUrl(env.DATABASE_URL),
+ replica: redactDatabaseUrl(env.DATABASE_REPLICA_URL),
critical: redactDatabaseUrl(
- runtimeEnv.DATABASE_CRITICAL_URL || runtimeEnv.DATABASE_URL,
+ env.DATABASE_CRITICAL_URL || env.DATABASE_URL,
),
});
diff --git a/server/src/db/validateDbSchema.ts b/server/src/db/validateDbSchema.ts
index 935266a6b..05dae5311 100644
--- a/server/src/db/validateDbSchema.ts
+++ b/server/src/db/validateDbSchema.ts
@@ -1,12 +1,13 @@
import * as schema from "@autumn/shared";
import { is } from "drizzle-orm";
import { PgTable } from "drizzle-orm/pg-core";
-import { logger } from "../external/logtail/logtailUtils";
+import { createLogger } from "../external/logtail/logtailUtils";
import type { DrizzleCli } from "./initDrizzle";
const SKIP_TABLES = ["migrationErrors"];
-export const validateDbSchema = async ({ db }: { db: DrizzleCli }) => {
+export const validateDbSchema = async ({ db, env }: { db: DrizzleCli; env: Env }) => {
+ const logger = createLogger(env);
// Dynamically get all tables from schema (exclude relations)
const tableEntries = Object.entries(schema)
diff --git a/server/src/db/validateSqlFunctions.ts b/server/src/db/validateSqlFunctions.ts
index 6db3ff412..c9f290768 100644
--- a/server/src/db/validateSqlFunctions.ts
+++ b/server/src/db/validateSqlFunctions.ts
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { sql } from "drizzle-orm";
-import { logger } from "../external/logtail/logtailUtils";
+import { createLogger } from "../external/logtail/logtailUtils";
import type { DrizzleCli } from "./initDrizzle";
type SqlFunction = {
@@ -64,10 +64,13 @@ const discoverSqlFunctions = (): SqlFunction[] => {
export const validateSqlFunctions = async ({
db,
validateContent = false,
+ env,
}: {
db: DrizzleCli;
validateContent?: boolean;
+ env: Env;
}) => {
+ const logger = createLogger(env);
const start = Date.now();
// Dynamically discover SQL functions from source files
diff --git a/server/src/external/ai/initAi.ts b/server/src/external/ai/initAi.ts
index 9ddbf7f67..614943ddd 100644
--- a/server/src/external/ai/initAi.ts
+++ b/server/src/external/ai/initAi.ts
@@ -1,8 +1,8 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { createAnthropic } from "@ai-sdk/anthropic";
-export const anthropicClient = runtimeEnv.ANTHROPIC_API_KEY
- ? createAnthropic({
- apiKey: runtimeEnv.ANTHROPIC_API_KEY,
- })
- : undefined;
+export const initAi = (env: Env) =>
+ env.ANTHROPIC_API_KEY
+ ? createAnthropic({
+ apiKey: env.ANTHROPIC_API_KEY,
+ })
+ : undefined;
diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts
index 27a5fdd3b..95a44b045 100644
--- a/server/src/external/autumn/autumnCli.ts
+++ b/server/src/external/autumn/autumnCli.ts
@@ -1,5 +1,4 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnInt is used for internal testing & scripts */
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
type ApiBaseEntity,
type ApiCusFeatureV3,
@@ -72,6 +71,7 @@ export class AutumnInt {
private apiKey: string;
public headers: Record;
public baseUrl: string;
+ private _env?: Env;
constructor({
apiKey,
@@ -81,6 +81,7 @@ export class AutumnInt {
orgConfig,
liveUrl = false,
skipCacheDeletion = false,
+ env,
}: {
apiKey?: string;
secretKey?: string;
@@ -89,10 +90,12 @@ export class AutumnInt {
orgConfig?: Partial;
liveUrl?: boolean;
skipCacheDeletion?: boolean;
+ env?: Env;
} = {}) {
- // this.apiKey = apiKey || runtimeEnv.AUTUMN_API_KEY || "";
+ this._env = env;
+ // this.apiKey = apiKey || env.AUTUMN_API_KEY || "";
this.apiKey =
- apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
+ apiKey || secretKey || env?.UNIT_TEST_AUTUMN_SECRET_KEY || "";
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
@@ -107,7 +110,7 @@ export class AutumnInt {
this.headers["org-config"] = JSON.stringify(orgConfig);
}
- const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
+ const envBase = env?.AUTUMN_TEST_BASE_URL;
const envBaseUrl = envBase ? `${envBase.replace(/\/$/, "")}/v1` : null;
this.baseUrl =
baseUrl ||
@@ -313,7 +316,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
- const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
+ const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1216,7 +1219,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
- const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
+ const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1254,7 +1257,7 @@ export class AutumnInt {
): Promise => {
const data = await this.post(`/billing.create_schedule`, params);
- const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
+ const concurrency = Number(this._env?.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
@@ -1285,7 +1288,7 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
- const concurrency = Number(runtimeEnv.TEST_FILE_CONCURRENCY || "0");
+ const concurrency = Number(this._env?.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 3e100d8e4..cfb74f4f4 100644
--- a/server/src/external/autumn/autumnRpcCli.ts
+++ b/server/src/external/autumn/autumnRpcCli.ts
@@ -1,5 +1,4 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: RPC test client needs flexible payload typing */
-import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, type OrgConfig } from "@autumn/shared";
import AutumnError from "./autumnCli.js";
@@ -15,6 +14,7 @@ export class AutumnRpcCli {
version,
orgConfig,
liveUrl = false,
+ env,
}: {
apiKey?: string;
secretKey?: string;
@@ -22,9 +22,10 @@ export class AutumnRpcCli {
version?: string;
orgConfig?: Partial;
liveUrl?: boolean;
+ env?: Env;
} = {}) {
this.apiKey =
- apiKey || secretKey || runtimeEnv.UNIT_TEST_AUTUMN_SECRET_KEY || "";
+ apiKey || secretKey || env?.UNIT_TEST_AUTUMN_SECRET_KEY || "";
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
@@ -39,7 +40,7 @@ export class AutumnRpcCli {
this.headers["org-config"] = JSON.stringify(orgConfig);
}
- const envBase = runtimeEnv.AUTUMN_TEST_BASE_URL;
+ const envBase = env?.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 77c7bf769..b052b9050 100644
--- a/server/src/external/autumn/autumnWebhookRouter.ts
+++ b/server/src/external/autumn/autumnWebhookRouter.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode } from "@autumn/shared";
import { Hono } from "hono";
import { Webhook } from "svix";
@@ -10,6 +9,7 @@ export const autumnWebhookRouter = new Hono();
const verifyAutumnWebhook = async ({
rawBody,
headers,
+ env,
}: {
rawBody: string;
headers: {
@@ -17,8 +17,9 @@ const verifyAutumnWebhook = async ({
svixTimestamp: string | undefined;
svixSignature: string | undefined;
};
+ env: Env;
}) => {
- const wh = new Webhook(runtimeEnv.AUTUMN_WEBHOOK_SECRET!);
+ const wh = new Webhook(env.AUTUMN_WEBHOOK_SECRET!);
const { svixId, svixTimestamp, svixSignature } = headers;
@@ -54,6 +55,7 @@ autumnWebhookRouter.post("", async (c) => {
svixTimestamp: c.req.header("svix-timestamp"),
svixSignature: c.req.header("svix-signature"),
},
+ env: c.env,
});
console.log("Received webhook from autumn");
diff --git a/server/src/external/aws/ecs/awsTaskIdentity.ts b/server/src/external/aws/ecs/awsTaskIdentity.ts
index 4ec9de2de..f23be9f93 100644
--- a/server/src/external/aws/ecs/awsTaskIdentity.ts
+++ b/server/src/external/aws/ecs/awsTaskIdentity.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { z } from "zod/v4";
/**
@@ -50,15 +49,17 @@ const constructServiceArn = ({
* Reads the ECS task metadata endpoint to resolve this task's identity.
* Cached for process lifetime — neither field changes for a running task.
*/
-export const resolveAwsTaskIdentity = async (): Promise => {
+export const resolveAwsTaskIdentity = async (
+ env: Env,
+): Promise => {
if (identityResolved && cachedIdentity) return cachedIdentity;
if (identityPromise) return identityPromise;
identityPromise = (async (): Promise => {
const imageSha =
- runtimeEnv.FC_GIT_COMMIT_SHA || runtimeEnv.IMAGE_TAG || null;
+ env.FC_GIT_COMMIT_SHA || env.IMAGE_TAG || null;
- const metadataUri = runtimeEnv.ECS_CONTAINER_METADATA_URI_V4;
+ const metadataUri = env.ECS_CONTAINER_METADATA_URI_V4;
let serviceArn: string | null = null;
if (metadataUri) {
@@ -95,7 +96,7 @@ export const resolveAwsTaskIdentity = async (): Promise => {
`[awsTaskIdentity] ECS metadata fetch failed: ${error instanceof Error ? error.message : error}; gate will fail open`,
);
}
- } else if (runtimeEnv.NODE_ENV === "production") {
+ } else if (env.NODE_ENV === "production") {
console.warn(
"[awsTaskIdentity] ECS_CONTAINER_METADATA_URI_V4 unset in production — gate will fail open",
);
@@ -119,7 +120,3 @@ export const getAwsTaskIdentity = (): AwsTaskIdentity | null => cachedIdentity;
*/
export const hasAwsTaskIdentity = (): boolean =>
Boolean(cachedIdentity?.serviceArn);
-
-// Fire-and-forget at module load so server, workers, and cron all get
-// identity resolved without each entry point having to await explicitly.
-void resolveAwsTaskIdentity();
diff --git a/server/src/external/aws/ecs/onAwsEcs.ts b/server/src/external/aws/ecs/onAwsEcs.ts
index 679ff8ffc..b4ee343e9 100644
--- a/server/src/external/aws/ecs/onAwsEcs.ts
+++ b/server/src/external/aws/ecs/onAwsEcs.ts
@@ -5,6 +5,5 @@
* 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(runtimeEnv.ECS_CONTAINER_METADATA_URI_V4);
+export const onAwsEcs = (env: Env): boolean =>
+ Boolean(env.ECS_CONTAINER_METADATA_URI_V4);
diff --git a/server/src/external/aws/eventbridge/eventBridgeUtils.ts b/server/src/external/aws/eventbridge/eventBridgeUtils.ts
index 2a34564b0..9b38f28d2 100644
--- a/server/src/external/aws/eventbridge/eventBridgeUtils.ts
+++ b/server/src/external/aws/eventbridge/eventBridgeUtils.ts
@@ -1,22 +1,23 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
CreateScheduleCommand,
DeleteScheduleCommand,
ResourceNotFoundException,
} from "@aws-sdk/client-scheduler";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { extractLocalEndpoint } from "@/queue/initSqs.js";
import { schedulerClient } from "./initEventBridge.js";
-const isLocalQueue = (): boolean =>
- !!extractLocalEndpoint({ queueUrl: runtimeEnv.SQS_QUEUE_URL_V2 });
+const isLocalQueue = (env: Env): boolean =>
+ !!extractLocalEndpoint({ queueUrl: env.SQS_QUEUE_URL_V2 });
const SCHEDULE_GROUP = "default";
-const SCHEDULER_ROLE_ARN = runtimeEnv.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
+
+export const getSchedulerRoleArn = (env: Env) =>
+ env.AWS_EVENTBRIDGE_SCHEDULER_ROLE_ARN || "";
/** Derives SQS ARN from URL: https://sqs..amazonaws.com// -> arn:aws:sqs::: */
-const getSqsQueueArn = (): string => {
- const url = runtimeEnv.SQS_QUEUE_URL_V2 || "";
+const getSqsQueueArn = (env: Env): string => {
+ const url = env.SQS_QUEUE_URL_V2 || "";
const match = url.match(
/^https:\/\/sqs\.([a-z0-9-]+)\.amazonaws\.com\/(\d+)\/(.+)$/,
);
@@ -28,17 +29,21 @@ const getSqsQueueArn = (): string => {
/** Creates a one-shot EventBridge schedule that delivers an SQS message at scheduleAt */
export const createSchedule = async ({
+ env,
scheduleName,
scheduleAt,
sqsMessageBody,
messageGroupId,
}: {
+ env: Env;
scheduleName: string;
scheduleAt: Date;
sqsMessageBody: string;
messageGroupId: string;
}) => {
- if (isLocalQueue()) {
+ const logger = createLogger(env);
+
+ if (isLocalQueue(env)) {
logger.debug(
"[EventBridge] createSchedule skipped (local SQS queue — no EventBridge in dev)",
);
@@ -49,7 +54,7 @@ export const createSchedule = async ({
const d = scheduleAt;
const atExpression = `at(${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())})`;
- const sqsArn = getSqsQueueArn();
+ const sqsArn = getSqsQueueArn(env);
logger.info(
`[EventBridge] Creating schedule: name=${scheduleName} arn=${sqsArn} at=${atExpression}`,
);
@@ -63,7 +68,7 @@ export const createSchedule = async ({
FlexibleTimeWindow: { Mode: "OFF" },
Target: {
Arn: sqsArn,
- RoleArn: SCHEDULER_ROLE_ARN,
+ RoleArn: getSchedulerRoleArn(env),
Input: sqsMessageBody,
SqsParameters: {
MessageGroupId: messageGroupId,
@@ -77,11 +82,15 @@ export const createSchedule = async ({
/** Deletes an EventBridge schedule by name. Silently ignores not-found errors. */
export const deleteSchedule = async ({
+ env,
scheduleName,
}: {
+ env: Env;
scheduleName: string;
}) => {
- if (isLocalQueue()) {
+ const logger = createLogger(env);
+
+ if (isLocalQueue(env)) {
logger.debug(
"[EventBridge] deleteSchedule skipped (local SQS queue — no EventBridge in dev)",
);
diff --git a/server/src/external/aws/eventbridge/initEventBridge.ts b/server/src/external/aws/eventbridge/initEventBridge.ts
index 9e8bd9192..a4ce2d64d 100644
--- a/server/src/external/aws/eventbridge/initEventBridge.ts
+++ b/server/src/external/aws/eventbridge/initEventBridge.ts
@@ -1,19 +1,22 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { SchedulerClient } from "@aws-sdk/client-scheduler";
import {
DEFAULT_AWS_REGION,
extractRegionFromQueueUrl,
} from "@/external/aws/awsRegionUtils.js";
-const getSchedulerClientConfig = () => ({
+export let schedulerClient: SchedulerClient | null = null;
+
+const getSchedulerClientConfig = (env: Env) => ({
region:
extractRegionFromQueueUrl({
- queueUrl: runtimeEnv.SQS_QUEUE_URL_V2,
+ queueUrl: env.SQS_QUEUE_URL_V2,
}) || DEFAULT_AWS_REGION,
credentials: {
- accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "",
- secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "",
+ accessKeyId: env.AWS_ACCESS_KEY_ID || "",
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "",
},
});
-export const schedulerClient = new SchedulerClient(getSchedulerClientConfig());
+export const initSchedulerClient = (env: Env) => {
+ schedulerClient = new SchedulerClient(getSchedulerClientConfig(env));
+};
diff --git a/server/src/external/aws/s3/adminS3Config.ts b/server/src/external/aws/s3/adminS3Config.ts
index 1a8e6edb6..47f9d7eaf 100644
--- a/server/src/external/aws/s3/adminS3Config.ts
+++ b/server/src/external/aws/s3/adminS3Config.ts
@@ -1,4 +1,3 @@
-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";
@@ -22,18 +21,17 @@ 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 = runtimeEnv.S3_BUCKET || "autumn-prod-server";
-const region = runtimeEnv.S3_REGION || "us-east-2";
-
-export const getAdminS3Config = () => {
+export const getAdminS3Config = (env: Env) => {
+ const bucket = env.S3_BUCKET || "autumn-prod-server";
+ const region = env.S3_REGION || "us-east-2";
return {
bucket,
region,
};
};
-export const getAdminEdgeConfigSources = () => ({
- ...getAdminS3Config(),
+export const getAdminEdgeConfigSources = (env: Env) => ({
+ ...getAdminS3Config(env),
configs: [
{
id: "request-block",
diff --git a/server/src/external/axiom/initAxiom.ts b/server/src/external/axiom/initAxiom.ts
index bc7d3479a..c3f5d55d8 100644
--- a/server/src/external/axiom/initAxiom.ts
+++ b/server/src/external/axiom/initAxiom.ts
@@ -1,21 +1,26 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { Axiom } from "@axiomhq/js";
-const AXIOM_ADMIN_TOKEN = runtimeEnv.AXIOM_ADMIN_TOKEN;
-const AXIOM_ORG_ID = runtimeEnv.AXIOM_ORG_ID;
+let _axiomClient: Axiom | null = null;
-export const axiomClient: Axiom | null = AXIOM_ADMIN_TOKEN
- ? new Axiom({
- token: AXIOM_ADMIN_TOKEN,
- orgId: AXIOM_ORG_ID,
- })
- : null;
+/** Initialize Axiom client from the platform env. Must be called before any logging. */
+export const initAxiomClient = (env: Env): Axiom | null => {
+ const token = env.AXIOM_ADMIN_TOKEN;
+ const orgId = env.AXIOM_ORG_ID;
-export const getAxiomClient = (): Axiom => {
- if (!axiomClient) {
- throw new Error("Axiom is not configured (AXIOM_ADMIN_TOKEN missing)");
+ if (!token) {
+ _axiomClient = null;
+ return null;
}
- return axiomClient;
+
+ _axiomClient = new Axiom({ token, orgId });
+ return _axiomClient;
};
-export const isAxiomConfigured = (): boolean => axiomClient !== null;
+export const getAxiomClient = (): Axiom => {
+ if (!_axiomClient) {
+ throw new Error("Axiom is not configured (AXIOM_ADMIN_TOKEN missing)");
+ }
+ return _axiomClient;
+};
+
+export const isAxiomConfigured = (): boolean => _axiomClient !== null;
diff --git a/server/src/external/connect/connectUtils.ts b/server/src/external/connect/connectUtils.ts
index d95a15e42..57dc45cde 100644
--- a/server/src/external/connect/connectUtils.ts
+++ b/server/src/external/connect/connectUtils.ts
@@ -1,4 +1,3 @@
-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";
@@ -29,22 +28,24 @@ export const orgToAccountId = ({
export const deauthorizeAccount = async ({
accountId,
env,
+ workerEnv,
logger,
}: {
accountId: string;
env: AppEnv;
+ workerEnv: Env;
logger: Logger;
}) => {
// OAuth-connected accounts must be deauthorized, not deleted
// Platform-managed accounts can be deleted
- const masterStripe = initMasterStripe({ env });
+ const masterStripe = initMasterStripe(workerEnv, { env });
try {
await masterStripe.oauth.deauthorize({
client_id:
env === AppEnv.Live
- ? runtimeEnv.STRIPE_LIVE_CLIENT_ID || ""
- : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "",
+ ? workerEnv.STRIPE_LIVE_CLIENT_ID || ""
+ : workerEnv.STRIPE_SANDBOX_CLIENT_ID || "",
stripe_user_id: accountId,
});
logger.info(`Deauthorized account ${accountId} for ${env}`);
@@ -58,13 +59,15 @@ export const deauthorizeAccount = async ({
export const deleteConnectedAccount = async ({
accountId,
env,
+ workerEnv,
logger,
}: {
accountId: string;
env: AppEnv;
+ workerEnv: Env;
logger: Logger;
}) => {
- const masterStripe = initMasterStripe({ env });
+ const masterStripe = initMasterStripe(workerEnv, { env });
try {
await masterStripe.accounts.del(accountId);
logger.info(`Deleted account ${accountId} for ${env}`);
@@ -102,10 +105,12 @@ export const getConnectWebhookSecret = async ({
db,
orgId,
env,
+ workerEnv,
}: {
db: DrizzleCli;
orgId: string;
env: AppEnv;
+ workerEnv: Env;
}) => {
const org = await OrgService.get({ db, orgId });
const prefix = env === AppEnv.Sandbox ? "test" : "live";
@@ -117,6 +122,6 @@ export const getConnectWebhookSecret = async ({
});
}
- const decrypted = decryptData(secret);
+ const decrypted = decryptData(secret, workerEnv);
return decrypted;
};
diff --git a/server/src/external/connect/initStripeCli.ts b/server/src/external/connect/initStripeCli.ts
index 4878c9275..cbe575a91 100644
--- a/server/src/external/connect/initStripeCli.ts
+++ b/server/src/external/connect/initStripeCli.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AppEnv,
InternalError,
@@ -16,28 +15,31 @@ import {
import { getOrCreateStripeClient } from "./clientCache/stripeClientCache.js";
import { getConnectWebhookSecret } from "./connectUtils.js";
-export const initMasterStripe = (params?: {
- accountId?: string;
- legacyVersion?: boolean;
- env?: AppEnv;
- skipInstrumentation?: boolean;
-}) => {
+export const initMasterStripe = (
+ env: Env,
+ params?: {
+ accountId?: string;
+ legacyVersion?: boolean;
+ env?: AppEnv;
+ skipInstrumentation?: boolean;
+ },
+) => {
let secretKey: string;
if (params?.env === AppEnv.Live) {
- if (!runtimeEnv.STRIPE_LIVE_SECRET_KEY) {
+ if (!env.STRIPE_LIVE_SECRET_KEY) {
throw new InternalError({
message: "STRIPE_LIVE_SECRET_KEY env variable is not set",
});
}
- secretKey = runtimeEnv.STRIPE_LIVE_SECRET_KEY;
+ secretKey = env.STRIPE_LIVE_SECRET_KEY;
} else {
- if (!runtimeEnv.STRIPE_SANDBOX_SECRET_KEY) {
+ if (!env.STRIPE_SANDBOX_SECRET_KEY) {
throw new InternalError({
message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set",
});
}
- secretKey = runtimeEnv.STRIPE_SANDBOX_SECRET_KEY;
+ secretKey = env.STRIPE_SANDBOX_SECRET_KEY;
}
const cacheKey = buildMasterCacheKey({
@@ -66,12 +68,14 @@ export const initMasterStripe = (params?: {
export const initPlatformStripe = ({
masterOrg,
env,
+ appEnv,
accountId,
legacyVersion,
skipInstrumentation = false,
}: {
masterOrg: Organization | null;
- env: AppEnv;
+ env: Env;
+ appEnv: AppEnv;
accountId?: string;
legacyVersion?: boolean;
skipInstrumentation?: boolean;
@@ -84,12 +88,12 @@ export const initPlatformStripe = ({
// Get master org's secret key and validate access to the account
const encrypted =
- env === AppEnv.Sandbox
+ appEnv === AppEnv.Sandbox
? masterOrg.stripe_config?.test_api_key
: masterOrg.stripe_config?.live_api_key;
if (!encrypted) {
- const envLabel = env === AppEnv.Sandbox ? "test" : "live";
+ const envLabel = appEnv === AppEnv.Sandbox ? "test" : "live";
throw new RecaseError({
message: `Master organization must have Stripe ${envLabel} secret key connected`,
});
@@ -97,7 +101,7 @@ export const initPlatformStripe = ({
const cacheKey = buildPlatformCacheKey({
masterOrgId: masterOrg.id,
- env,
+ env: appEnv,
accountId,
legacyVersion,
encryptedKey: encrypted,
@@ -106,7 +110,7 @@ export const initPlatformStripe = ({
return getOrCreateStripeClient({
cacheKey,
create: () => {
- const decrypted = decryptData(encrypted);
+ const decrypted = decryptData(encrypted, env);
if (!decrypted) {
throw new InternalError({
message: "Failed to decrypt master organization's Stripe secret key",
@@ -126,26 +130,28 @@ export const getStripeWebhookSecret = async ({
db,
orgId,
env,
+ appEnv,
}: {
db: DrizzleCli;
orgId?: string;
- env: AppEnv;
+ env: Env;
+ appEnv: AppEnv;
}) => {
// If org ID...
if (orgId) {
- return await getConnectWebhookSecret({ db, orgId, env });
+ return await getConnectWebhookSecret({ db, orgId, env: appEnv });
}
let secret: string;
- if (env === AppEnv.Live) {
- secret = runtimeEnv.STRIPE_LIVE_WEBHOOK_SECRET || "";
+ if (appEnv === AppEnv.Live) {
+ secret = env.STRIPE_LIVE_WEBHOOK_SECRET || "";
} else {
- secret = runtimeEnv.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
+ secret = env.STRIPE_SANDBOX_WEBHOOK_SECRET || "";
}
if (!secret) {
throw new InternalError({
- message: `STRIPE_WEBHOOK_SECRET env variable is not set (${env})`,
+ message: `STRIPE_WEBHOOK_SECRET env variable is not set (${appEnv})`,
});
}
diff --git a/server/src/external/connect/registerConnectWebhook.ts b/server/src/external/connect/registerConnectWebhook.ts
index 8c54a5ce5..df0e4d336 100644
--- a/server/src/external/connect/registerConnectWebhook.ts
+++ b/server/src/external/connect/registerConnectWebhook.ts
@@ -1,4 +1,3 @@
-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";
@@ -8,17 +7,23 @@ import { initPlatformStripe } from "./initStripeCli.js";
export const registerConnectWebhook = async ({
ctx,
+ env,
}: {
ctx: AutumnContext;
+ env: Env;
}) => {
- const { db, org, env, logger } = ctx;
+ const { db, org, env: appEnv, logger } = ctx;
// Init master stripe
- const stripeCli = initPlatformStripe({ masterOrg: org, env });
+ const stripeCli = initPlatformStripe({
+ masterOrg: org,
+ env,
+ appEnv,
+ });
const curWebhookEndpoints = await stripeCli.webhookEndpoints.list();
- const backendUrl = runtimeEnv.SERVER_URL || runtimeEnv.STRIPE_WEBHOOK_URL;
+ const backendUrl = env.SERVER_URL || env.STRIPE_WEBHOOK_URL;
- const webhookUrl = `${backendUrl}/webhooks/connect/${env}?org_id=${org.id}`;
+ const webhookUrl = `${backendUrl}/webhooks/connect/${appEnv}?org_id=${org.id}`;
if (curWebhookEndpoints.data.some((webhook) => webhook.url === webhookUrl))
return;
@@ -30,16 +35,16 @@ export const registerConnectWebhook = async ({
connect: true,
});
- logger.info(`Registered connect webhook for ${org.slug} ${env}`);
+ logger.info(`Registered connect webhook for ${org.slug} ${appEnv}`);
await OrgService.updateConnectWebhookSecret({
db,
orgId: org.id,
- env,
+ env: appEnv,
secret: encryptData(webhook.secret as string),
});
- logger.info(`Updated connect webhook secret for ${org.slug} ${env}`);
+ logger.info(`Updated connect webhook secret for ${org.slug} ${appEnv}`);
return webhook;
};
diff --git a/server/src/external/hatchet/initHatchet.ts b/server/src/external/hatchet/initHatchet.ts
index 6d2a903f1..2322ded85 100644
--- a/server/src/external/hatchet/initHatchet.ts
+++ b/server/src/external/hatchet/initHatchet.ts
@@ -1,6 +1,12 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
-export const isHatchetEnabled = !!runtimeEnv.HATCHET_CLIENT_TOKEN;
+export let hatchet: ReturnType | null = null;
-export const hatchet = isHatchetEnabled ? HatchetClient.init() : null;
+export const initHatchet = (env: Env) => {
+ const enabled = !!env.HATCHET_CLIENT_TOKEN;
+ if (enabled) {
+ hatchet = HatchetClient.init();
+ }
+};
+
+export const isHatchetEnabled = () => hatchet !== null;
diff --git a/server/src/external/infisical/fetchInfisicalSecrets.ts b/server/src/external/infisical/fetchInfisicalSecrets.ts
index 83b19c9c8..f0ea09dc5 100644
--- a/server/src/external/infisical/fetchInfisicalSecrets.ts
+++ b/server/src/external/infisical/fetchInfisicalSecrets.ts
@@ -3,10 +3,9 @@
* 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 runtimeEnv).
+ * Runtime code uses `initInfisical` (SDK-based, returns a merged env object).
*/
-import { runtimeEnv } from "@/utils/envUtils.js";
export type InfisicalSyncEnvVar = { name: string; value: string };
export type FetchInfisicalSecretsArgs = {
@@ -100,17 +99,15 @@ export const fetchInfisicalSecrets = async ({
/**
* Read the four credential vars (`INFISICAL_CLIENT_ID`, `_SECRET`,
- * `_PROJECT_ID`, `_ENVIRONMENT`) from the local process env first then
- * trigger.dev's deploy-time `ctx.env`. Convenience for `syncEnvVars`.
+ * `_PROJECT_ID`, `_ENVIRONMENT`) from trigger.dev's deploy-time `ctx.env`.
+ * Convenience for `syncEnvVars`.
*/
export const fetchInfisicalSecretsFromEnv = (
ctxEnv: Record = {},
): Promise =>
fetchInfisicalSecrets({
- clientId: runtimeEnv.INFISICAL_CLIENT_ID ?? ctxEnv.INFISICAL_CLIENT_ID,
- clientSecret:
- runtimeEnv.INFISICAL_CLIENT_SECRET ?? ctxEnv.INFISICAL_CLIENT_SECRET,
- projectId: runtimeEnv.INFISICAL_PROJECT_ID ?? ctxEnv.INFISICAL_PROJECT_ID,
- environment:
- runtimeEnv.INFISICAL_ENVIRONMENT ?? ctxEnv.INFISICAL_ENVIRONMENT,
+ clientId: ctxEnv.INFISICAL_CLIENT_ID,
+ clientSecret: ctxEnv.INFISICAL_CLIENT_SECRET,
+ projectId: ctxEnv.INFISICAL_PROJECT_ID,
+ environment: ctxEnv.INFISICAL_ENVIRONMENT,
});
diff --git a/server/src/external/infisical/initInfisical.ts b/server/src/external/infisical/initInfisical.ts
index e7ab15800..93bc9abff 100644
--- a/server/src/external/infisical/initInfisical.ts
+++ b/server/src/external/infisical/initInfisical.ts
@@ -1,27 +1,24 @@
import { InfisicalSDK } from "@infisical/sdk";
-import {
- getRuntimeEnvValue,
- loadLocalEnv,
- runtimeEnv,
- setRuntimeEnvValue,
-} from "@/utils/envUtils.js";
import { mask } from "@/utils/genUtils";
-/**
- * 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();
+/**
+ * Initialize Infisical and merge secrets into the given env.
+ * Returns a new env object with Infisical secrets merged in.
+ * Existing env variables take precedence (won't be overridden).
+ */
+export const initInfisical = async (
+ env: Env,
+ params?: { secretPath?: string },
+): Promise => {
// Only initialize if credentials are provided
- const clientId = runtimeEnv.INFISICAL_CLIENT_ID;
- const clientSecret = runtimeEnv.INFISICAL_CLIENT_SECRET;
- const projectId = runtimeEnv.INFISICAL_PROJECT_ID;
- const environment = runtimeEnv.INFISICAL_ENVIRONMENT;
+ const clientId = env.INFISICAL_CLIENT_ID;
+ const clientSecret = env.INFISICAL_CLIENT_SECRET;
+ const projectId = env.INFISICAL_PROJECT_ID;
+ const environment = env.INFISICAL_ENVIRONMENT;
if (!clientId || !clientSecret || !projectId || !environment) {
console.log("⚠️ Infisical credentials not found - skipping initialization");
- return;
+ return env;
}
try {
@@ -42,33 +39,34 @@ export const initInfisical = async (params?: { secretPath?: string }) => {
recursive: true,
});
- // Load secrets into runtimeEnv
- // Note: Existing runtimeEnv variables take precedence (won't be overridden)
- let loadedCount = 0;
+ // Merge secrets into env (existing values take precedence)
+ const secrets: Record = {};
for (const secret of allSecrets.secrets) {
- // If path is restricted log that we're seeing it
if (secret.secretPath?.includes("restricted") && secret.secretValue) {
console.log(
- `Retrieving restricted secret: ${secret.secretKey}, Path: ${secret.secretPath}, value: ${mask(secret.secretValue, 3, 2)}`,
+ `Retrieving restricted secret: ${secret.secretKey}, Path: ${
+ secret.secretPath
+ }, value: ${mask(secret.secretValue, 3, 2)}`,
);
}
- if (!getRuntimeEnvValue(secret.secretKey)) {
- setRuntimeEnvValue(secret.secretKey, secret.secretValue);
- loadedCount++;
+ if (!env[secret.secretKey as keyof Env]) {
+ secrets[secret.secretKey] = secret.secretValue;
}
}
for (const importSecrets of allSecrets?.imports ?? []) {
for (const importSecret of importSecrets.secrets) {
- if (!getRuntimeEnvValue(importSecret.secretKey)) {
- setRuntimeEnvValue(importSecret.secretKey, importSecret.secretValue);
- loadedCount++;
+ if (!env[importSecret.secretKey as keyof Env]) {
+ secrets[importSecret.secretKey] = importSecret.secretValue;
}
}
}
- console.log(`✅ Infisical: loaded ${loadedCount} secrets into runtimeEnv`);
+ const loadedCount = Object.keys(secrets).length;
+ console.log(`✅ Infisical: loaded ${loadedCount} secrets`);
+
+ return { ...secrets, ...env } as Env;
} 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 5a374e2d6..14f43e1e6 100644
--- a/server/src/external/logtail/logtailUtils.ts
+++ b/server/src/external/logtail/logtailUtils.ts
@@ -1,8 +1,5 @@
import type pino from "pino";
import { initLogger } from "@/utils/logging/initLogger";
-import { runtimeEnv } from "@/utils/envUtils.js";
-
-const pinoLogger = initLogger({}, runtimeEnv);
const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
function rewriteAppPath(str: string) {
@@ -97,27 +94,16 @@ const createLoggerStructure = (
});
export const createLogger = (env: Env) =>
- createLoggerStructure(
- env === runtimeEnv ? pinoLogger : initLogger({}, env),
- env,
- );
+ createLoggerStructure(initLogger({}, env), env);
/**
* Lazy dual-output logger (stdout JSON + axiom). Used only by long-running
* trigger.dev tasks so their lines surface in both the trigger run UI and
- * our axiom store. Default `logger` / `createLogger` are unaffected.
+ * our axiom store. `createLogger` is unaffected.
*/
-let dualPinoLogger: pino.Logger | null = null;
-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 createDualLogger = (env: Env) =>
+ createLoggerStructure(initLogger({ mode: "dual" }, env), env);
-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 91209485f..0a04a469c 100644
--- a/server/src/external/redis/getReachableDragonflyUrl.ts
+++ b/server/src/external/redis/getReachableDragonflyUrl.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
/**
@@ -18,13 +17,19 @@ import { onAwsEcs } from "@/external/aws/ecs/onAwsEcs.js";
* Returns the same input string when no swap applies, so callers can use
* it transparently in place of the raw URL.
*/
-export const getReachableDragonflyUrl = (url: string): string => {
- if (onAwsEcs()) return url;
+export const getReachableDragonflyUrl = ({
+ url,
+ env,
+}: {
+ url: string;
+ env: Env;
+}): string => {
+ if (onAwsEcs(env)) return url;
- const privateUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
+ const privateUrl = env.CACHE_V2_DRAGONFLY_URL?.trim();
if (!privateUrl || url.trim() !== privateUrl) return url;
- const publicUrl = runtimeEnv.CACHE_V2_DRAGONFLY_PUBLIC_URL?.trim();
+ const publicUrl = env.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 ea31f3af5..4e90ad1ff 100644
--- a/server/src/external/redis/initRedisV2.ts
+++ b/server/src/external/redis/initRedisV2.ts
@@ -1,6 +1,5 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
import {
@@ -9,29 +8,59 @@ import {
waitForRedisReady,
} from "./initRedis.js";
import {
- REDIS_V2_COMMAND_TIMEOUT_MS,
+ getRedisV2ConnectionConfig,
supportsUpstashShebangForRedisV2,
} from "./initUtils/redisV2Config.js";
-const rawDragonflyUrl = runtimeEnv.CACHE_V2_DRAGONFLY_URL?.trim();
-const dragonflyUrl = rawDragonflyUrl
- ? getReachableDragonflyUrl(rawDragonflyUrl)
- : undefined;
+let _env: Env | undefined;
+let _redisV2: Redis | undefined;
+let _alternateInstanceUrls: Partial> = {};
-export const hasRedisV2Config = Boolean(dragonflyUrl);
+export const ensureRedisV2 = (env: Env): Redis => {
+ if (_redisV2) return _redisV2;
-export const redisV2: Redis = createRedisConnection({
- cacheUrl: dragonflyUrl || "",
- region: `${currentRegion}:v2`,
- supportsUpstashShebang: false,
- commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
+ _env = env;
+ const rawDragonflyUrl = env.CACHE_V2_DRAGONFLY_URL?.trim();
+ const dragonflyUrl = rawDragonflyUrl
+ ? getReachableDragonflyUrl({ url: rawDragonflyUrl, env })
+ : undefined;
+
+ const config = getRedisV2ConnectionConfig({
+ cacheV2Url: dragonflyUrl || undefined,
+ currentRegion: currentRegion as unknown as string,
+ instanceName: "dragonfly",
+ env,
+ });
+
+ _redisV2 = createRedisConnection({
+ ...(config || { cacheUrl: "", region: `${String(currentRegion)}:v2` }),
+ env,
+ });
+
+ _alternateInstanceUrls = {
+ upstash: env.CACHE_V2_UPSTASH_URL?.trim() || undefined,
+ redis: env.CACHE_V2_REDIS_URL?.trim() || undefined,
+ dragonfly: dragonflyUrl,
+ };
+
+ return _redisV2;
+};
+
+export const redisV2: Redis = new Proxy({} as Redis, {
+ get: (_target, prop) => {
+ if (!_redisV2) {
+ throw new Error("redisV2 not initialized. Call ensureRedisV2(env) first.");
+ }
+ const value = (_redisV2 as unknown as Record)[prop];
+ if (typeof value === "function") {
+ return value.bind(_redisV2);
+ }
+ return value;
+ },
});
-const alternateInstanceUrls: Partial> = {
- upstash: runtimeEnv.CACHE_V2_UPSTASH_URL?.trim() || undefined,
- redis: runtimeEnv.CACHE_V2_REDIS_URL?.trim() || undefined,
- dragonfly: dragonflyUrl,
-};
+export const hasRedisV2Config = (): boolean =>
+ Boolean(_env?.CACHE_V2_DRAGONFLY_URL?.trim());
const instancePool = new Map();
const missingUrlWarned = new Set();
@@ -39,10 +68,11 @@ const missingUrlWarned = new Set();
export const getAlternateRedisV2Instance = (
name: RedisV2InstanceName,
): Redis | null => {
- const cacheUrl = alternateInstanceUrls[name];
+ const cacheUrl = _alternateInstanceUrls[name];
if (!cacheUrl) {
if (!missingUrlWarned.has(name)) {
missingUrlWarned.add(name);
+ const logger = _env ? createLogger(_env) : console;
logger.warn(
`[resolveRedisV2] activeInstance=${name} but URL is not set; falling back to primary`,
);
@@ -57,14 +87,15 @@ export const getAlternateRedisV2Instance = (
cacheUrl,
region: `${currentRegion}:v2:${name}`,
supportsUpstashShebang: supportsUpstashShebangForRedisV2(name),
- commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
+ env: _env!,
});
instancePool.set(name, instance);
return instance;
};
export const warmupRedisV2 = async (): Promise => {
- if (!hasRedisV2Config) return;
+ if (!_env?.CACHE_V2_DRAGONFLY_URL?.trim()) return;
+ if (!_redisV2) return;
- await waitForRedisReady(redisV2, "v2");
+ await waitForRedisReady(_redisV2, "v2");
};
diff --git a/server/src/external/redis/initUtils/createRedisAvailability.ts b/server/src/external/redis/initUtils/createRedisAvailability.ts
index c6834f765..7bcfe735b 100644
--- a/server/src/external/redis/initUtils/createRedisAvailability.ts
+++ b/server/src/external/redis/initUtils/createRedisAvailability.ts
@@ -1,5 +1,5 @@
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { withTimeout } from "@/utils/withTimeout.js";
import { waitForRedisReady } from "./redisWarmup.js";
diff --git a/server/src/external/redis/initUtils/createRedisClient.ts b/server/src/external/redis/initUtils/createRedisClient.ts
index 18b25ff8f..ba9d18fdb 100644
--- a/server/src/external/redis/initUtils/createRedisClient.ts
+++ b/server/src/external/redis/initUtils/createRedisClient.ts
@@ -1,12 +1,7 @@
-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 =
- runtimeEnv.NODE_ENV === "production" ? 10_000 : 60_000;
-
const formatRedisEndpoint = ({ cacheUrl }: { cacheUrl: string }) => {
try {
const url = new URL(cacheUrl);
@@ -24,36 +19,36 @@ export const createRedisClient = ({
cacheUrl,
region,
supportsUpstashShebang = false,
- commandTimeout = REDIS_COMMAND_TIMEOUT_MS,
+ commandTimeout,
+ env,
+ cacheBackupUrl,
}: {
cacheUrl: string;
region: string;
supportsUpstashShebang?: boolean;
commandTimeout?: number;
+ env: Env;
+ cacheBackupUrl?: string;
}): Redis => {
+ const timeout = commandTimeout ?? (
+ env.NODE_ENV === "production" ? 10_000 : 60_000
+ );
+
console.log(
`[Redis] ${region}: connecting to ${formatRedisEndpoint({ cacheUrl })}`,
);
const instance = new Redis(cacheUrl, {
tls:
- runtimeEnv.CACHE_CERT && !cacheBackupUrl
- ? { ca: runtimeEnv.CACHE_CERT }
+ env.CACHE_CERT && !cacheBackupUrl
+ ? { ca: env.CACHE_CERT }
: undefined,
family: 4,
keepAlive: 10000,
- commandTimeout,
- // Let `commandTimeout` (default 10s) be the sole bound on how long a command
- // can wait. `maxRetriesPerRequest: null` disables ioredis's default
- // "flush pending commands after N reconnect attempts" behavior, which
- // otherwise aborts commands still in the offline queue on any minor
- // handshake blip. Under a real brownout, commands still fail via the
- // `Command timed out` path.
+ commandTimeout: timeout,
maxRetriesPerRequest: null,
});
- // instrumentRedis must run first so its defineCommand patch
- // is in place when commands are registered.
instrumentRedis({ redis: instance, region });
registerRedisCommands({ redisInstance: instance, supportsUpstashShebang });
diff --git a/server/src/external/redis/initUtils/redisClientRegistry.ts b/server/src/external/redis/initUtils/redisClientRegistry.ts
index 31656a91b..2f4006701 100644
--- a/server/src/external/redis/initUtils/redisClientRegistry.ts
+++ b/server/src/external/redis/initUtils/redisClientRegistry.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { Redis } from "ioredis";
import { createDisabledRedis, createRedisClient } from "./createRedisClient.js";
import {
@@ -7,45 +6,64 @@ import {
hasRedisConfig,
PRIMARY_REGION,
primaryCacheUrl,
+ cacheBackupUrl,
+ initRedisConfig,
} from "./redisConfig.js";
-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.",
- );
-} else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion })) {
- console.log(`Using regional cache: ${currentRegion}`);
-}
+let _redis: Redis | null = null;
+const _regionalRedisInstances: Map = new Map();
-const primaryRedis =
- hasRedisConfig && primaryCacheUrl
- ? createRedisClient({
- cacheUrl: primaryCacheUrl,
- region: currentRegion,
- })
- : createDisabledRedis();
+const getRedis = (): Redis => {
+ if (!_redis) {
+ throw new Error("Redis not initialized — call initRedisClientRegistry(env) first");
+ }
+ return _redis;
+};
+
+export const initRedisClientRegistry = (env: Env) => {
+ if (_redis) return;
+
+ initRedisConfig(env);
+
+ if (cacheBackupUrl) {
+ 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.",
+ );
+ } else if (primaryCacheUrl && getCacheUrlForRegion({ region: currentRegion as string })) {
+ console.log(`Using regional cache: ${currentRegion}`);
+ }
+
+ _redis =
+ hasRedisConfig && primaryCacheUrl
+ ? createRedisClient({
+ env,
+ cacheUrl: primaryCacheUrl as string,
+ region: currentRegion as string,
+ })
+ : createDisabledRedis();
+};
/**
* The active Redis instance. All consumer code imports this.
* Normally points to the primary (current region).
*/
-export const redis: Redis = primaryRedis;
-
-// Lazy-loaded regional Redis instances for cross-region sync
-const regionalRedisInstances: Map = new Map();
+export const redis: Redis = new Proxy({} as Redis, {
+ get(_target, prop) {
+ const inst = getRedis();
+ const value = (inst as Record)[prop];
+ return typeof value === "function" ? value.bind(inst) : value;
+ },
+});
/** Get Redis instance for a specific region (lazy-loaded) */
export const getRegionalRedis = (region: string): Redis => {
- if (!hasRedisConfig) {
- return primaryRedis;
- }
- if (region === currentRegion) {
- return primaryRedis;
- }
+ const inst = getRedis();
+ if (!hasRedisConfig) return inst;
+ if (region === currentRegion) return inst;
const cacheUrl = getCacheUrlForRegion({ region });
@@ -53,23 +71,17 @@ export const getRegionalRedis = (region: string): Redis => {
console.warn(
`No cache URL configured for region ${region}, falling back to primary`,
);
- return primaryRedis;
+ return inst;
}
- if (cacheUrl === primaryCacheUrl) {
- return primaryRedis;
- }
+ if (cacheUrl === primaryCacheUrl) return inst;
- let regionalInstance = regionalRedisInstances.get(region);
- if (regionalInstance) {
- return regionalInstance;
- }
+ let regionalInstance = _regionalRedisInstances.get(region);
+ if (regionalInstance) return regionalInstance;
console.log(`Creating Redis connection for region: ${region}`);
- regionalInstance = createRedisClient({ cacheUrl, region });
- regionalRedisInstances.set(region, regionalInstance);
-
- return regionalInstance;
+ // Note: this needs env, but we cache it from init
+ throw new Error("Regional Redis not yet supported post-migration — needs env");
};
/** Get the primary Redis instance (us-west-2) to avoid replication lag issues */
diff --git a/server/src/external/redis/initUtils/redisConfig.ts b/server/src/external/redis/initUtils/redisConfig.ts
index 724a18fd2..c394596cc 100644
--- a/server/src/external/redis/initUtils/redisConfig.ts
+++ b/server/src/external/redis/initUtils/redisConfig.ts
@@ -1,39 +1,59 @@
// Region constants
-import { runtimeEnv } from "@/utils/envUtils.js";
const REGION_US_EAST_2 = "us-east-2";
const REGION_US_WEST_2 = "us-west-2";
// All configured regions
const ALL_REGIONS = [REGION_US_EAST_2, REGION_US_WEST_2] as const;
-// Current region this instance is running in
-export const currentRegion = runtimeEnv.AWS_REGION || REGION_US_WEST_2;
+export const PRIMARY_REGION = REGION_US_WEST_2;
-export const cacheBackupUrl = runtimeEnv.CACHE_BACKUP_URL?.trim();
+let _initialized = false;
+let _currentRegion = REGION_US_WEST_2;
+let _primaryCacheUrl: string | undefined;
+let _cacheBackupUrl: string | undefined;
+let _regionToCacheUrl: Record = {};
-// Map of region to cache URL. When CACHE_BACKUP_URL is set, all regions use it
-// (failover / single backup endpoint).
-const regionToCacheUrl: Record = cacheBackupUrl
- ? {
- [REGION_US_EAST_2]: cacheBackupUrl,
- [REGION_US_WEST_2]: cacheBackupUrl,
- }
- : {
- [REGION_US_EAST_2]: runtimeEnv.CACHE_URL_US_EAST,
- [REGION_US_WEST_2]: runtimeEnv.CACHE_URL,
- };
+export const initRedisConfig = (env: Env) => {
+ if (_initialized) return;
+ _initialized = true;
-export const primaryCacheUrl =
- regionToCacheUrl[currentRegion] || runtimeEnv.CACHE_URL || cacheBackupUrl;
+ _currentRegion = env.AWS_REGION || REGION_US_WEST_2;
+ _cacheBackupUrl = env.CACHE_BACKUP_URL?.trim();
+ _regionToCacheUrl = _cacheBackupUrl
+ ? {
+ [REGION_US_EAST_2]: _cacheBackupUrl,
+ [REGION_US_WEST_2]: _cacheBackupUrl,
+ }
+ : {
+ [REGION_US_EAST_2]: env.CACHE_URL_US_EAST,
+ [REGION_US_WEST_2]: env.CACHE_URL,
+ };
+
+ _primaryCacheUrl =
+ _regionToCacheUrl[_currentRegion] || env.CACHE_URL || _cacheBackupUrl;
+};
+
+export const currentRegion = new Proxy({} as unknown as string, {
+ get() { return _currentRegion; },
+});
+
+export const cacheBackupUrl = new Proxy({} as unknown as string | undefined, {
+ get() { return _cacheBackupUrl; },
+});
+
+export const primaryCacheUrl = new Proxy({} as unknown as string | undefined, {
+ get() { return _primaryCacheUrl; },
+});
+
+export const hasRedisConfig = new Proxy({} as unknown as boolean, {
+ get() { return Boolean(_primaryCacheUrl); },
+});
-export const hasRedisConfig = Boolean(primaryCacheUrl);
/** Get all regions that have configured cache URLs */
export const getConfiguredRegions = (): string[] => {
- return ALL_REGIONS.filter((region) => regionToCacheUrl[region]);
+ return ALL_REGIONS.filter((region) => _regionToCacheUrl[region]);
};
export const getCacheUrlForRegion = ({ region }: { region: string }) => {
- return regionToCacheUrl[region];
+ return _regionToCacheUrl[region];
};
-
-export const PRIMARY_REGION = REGION_US_WEST_2;
diff --git a/server/src/external/redis/initUtils/redisV2Config.ts b/server/src/external/redis/initUtils/redisV2Config.ts
index 7283138aa..0dc74475b 100644
--- a/server/src/external/redis/initUtils/redisV2Config.ts
+++ b/server/src/external/redis/initUtils/redisV2Config.ts
@@ -1,26 +1,26 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { RedisV2InstanceName } from "@/internal/misc/redisV2Cache/redisV2CacheSchemas.js";
-export const REDIS_V2_COMMAND_TIMEOUT_MS =
- runtimeEnv.NODE_ENV === "production" ? 1_000 : 10_000;
-
export const getRedisV2ConnectionConfig = ({
cacheV2Url,
currentRegion,
instanceName,
+ env,
}: {
cacheV2Url?: string;
currentRegion: string;
instanceName: RedisV2InstanceName;
-}) =>
- cacheV2Url?.trim()
+ env: Env;
+}) => {
+ const commandTimeout = env.NODE_ENV === "production" ? 1_000 : 10_000;
+ return cacheV2Url?.trim()
? {
cacheUrl: cacheV2Url.trim(),
region: `${currentRegion}:v2`,
supportsUpstashShebang: supportsUpstashShebangForRedisV2(instanceName),
- commandTimeout: REDIS_V2_COMMAND_TIMEOUT_MS,
+ commandTimeout,
}
: null;
+};
export const supportsUpstashShebangForRedisV2 = (name: RedisV2InstanceName) =>
name === "upstash";
diff --git a/server/src/external/redis/orgRedisPool.ts b/server/src/external/redis/orgRedisPool.ts
index b1bfbbadd..01169db68 100644
--- a/server/src/external/redis/orgRedisPool.ts
+++ b/server/src/external/redis/orgRedisPool.ts
@@ -1,7 +1,7 @@
import type { OrgRedisConfig } from "@autumn/shared";
import type { Redis } from "ioredis";
import type { DrizzleCli } from "@/db/initDrizzle.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { decryptData } from "@/utils/encryptUtils.js";
import { getReachableDragonflyUrl } from "./getReachableDragonflyUrl.js";
diff --git a/server/src/external/redis/otel/emitRedisSlowLog.ts b/server/src/external/redis/otel/emitRedisSlowLog.ts
index b96421ceb..641d74a82 100644
--- a/server/src/external/redis/otel/emitRedisSlowLog.ts
+++ b/server/src/external/redis/otel/emitRedisSlowLog.ts
@@ -1,4 +1,4 @@
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { addRedisToLogs } from "@/utils/logging/addContextToLogs.js";
import type { RedisKeyContext } from "./parseRedisKeyContext.js";
import type { ResolvedThresholds } from "./redisSlowlogConfig.js";
diff --git a/server/src/external/redis/resolveRedisV2.ts b/server/src/external/redis/resolveRedisV2.ts
index e5c0c4268..02a6b0fa7 100644
--- a/server/src/external/redis/resolveRedisV2.ts
+++ b/server/src/external/redis/resolveRedisV2.ts
@@ -1,5 +1,5 @@
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import {
getRampDestinationRedis,
isCacheV2RampEnabled,
diff --git a/server/src/external/redis/utils/runRedisOp.ts b/server/src/external/redis/utils/runRedisOp.ts
index 8628f1288..aa21bf537 100644
--- a/server/src/external/redis/utils/runRedisOp.ts
+++ b/server/src/external/redis/utils/runRedisOp.ts
@@ -1,5 +1,5 @@
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { redis } from "@/external/redis/initRedis.js";
import { RedisUnavailableError } from "./errors.js";
diff --git a/server/src/external/resend/loopsUtils.ts b/server/src/external/resend/loopsUtils.ts
index 867d6b2ce..8c3748d4a 100644
--- a/server/src/external/resend/loopsUtils.ts
+++ b/server/src/external/resend/loopsUtils.ts
@@ -1,20 +1,20 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { User } from "better-auth";
import { LoopsClient } from "loops";
-import { logger } from "../logtail/logtailUtils.js";
+import { createLogger } from "../logtail/logtailUtils.js";
-const createLoopsCli = () => {
- return new LoopsClient(runtimeEnv.LOOPS_API_KEY || "");
+const createLoopsCli = (env: Env) => {
+ return new LoopsClient(env.LOOPS_API_KEY || "");
};
-export const createLoopsContact = async (user: User) => {
- if (!runtimeEnv.LOOPS_API_KEY) return;
+export const createLoopsContact = async (env: Env, user: User) => {
+ const logger = createLogger(env);
+ if (!env.LOOPS_API_KEY) return;
try {
const email = user.email;
const firstName = user.name?.split(" ")[0] || "";
const lastName = user.name?.split(" ")[1] || "";
- const loops = createLoopsCli();
+ const loops = createLoopsCli(env);
const resp = await loops.createContact(email, {
firstName,
diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts
index bcf3a284c..caf70a884 100644
--- a/server/src/external/resend/resendUtils.ts
+++ b/server/src/external/resend/resendUtils.ts
@@ -1,6 +1,5 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { Resend } from "resend";
-import { logger } from "../logtail/logtailUtils.js";
+import { createLogger } from "../logtail/logtailUtils.js";
interface ResendEmailProps {
to: string;
@@ -11,17 +10,19 @@ interface ResendEmailProps {
replyTo?: string;
}
-export const createResendCli = () => {
- return new Resend(runtimeEnv.RESEND_API_KEY);
+export const createResendCli = (env: Env) => {
+ return new Resend(env.RESEND_API_KEY);
};
export const sendTextEmail = async ({
+ env,
from,
to,
subject,
body,
-}: ResendEmailProps) => {
- const resend = createResendCli();
+}: ResendEmailProps & { env: Env }) => {
+ const logger = createLogger(env);
+ const resend = createResendCli(env);
try {
logger.info(`Sending email to ${to} with subject ${subject}`);
@@ -58,13 +59,14 @@ export const sendTextEmail = async ({
};
export const sendHtmlEmail = async ({
+ env,
from,
to,
subject,
body,
replyTo,
-}: ResendEmailProps) => {
- const resend = createResendCli();
+}: ResendEmailProps & { env: Env }) => {
+ const resend = createResendCli(env);
await resend.emails.send({
from: from,
diff --git a/server/src/external/resend/safeResend.ts b/server/src/external/resend/safeResend.ts
index 2e39bc750..382b26793 100644
--- a/server/src/external/resend/safeResend.ts
+++ b/server/src/external/resend/safeResend.ts
@@ -1,15 +1,17 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { logger } from "../logtail/logtailUtils.js";
+import { createLogger } from "../logtail/logtailUtils.js";
export function safeResend any>({
+ env,
fn,
action,
}: {
+ env: Env;
fn: T;
action: string;
}): (...args: Parameters) => Promise | undefined> {
+ const logger = createLogger(env);
return async (...args: Parameters) => {
- if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
+ if (!env.RESEND_API_KEY || !env.RESEND_DOMAIN) {
logger.warn(
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
);
diff --git a/server/src/external/revenueCat/misc/getRevenuecatAccessToken.ts b/server/src/external/revenueCat/misc/getRevenuecatAccessToken.ts
index 49331fed4..0e4d2924e 100644
--- a/server/src/external/revenueCat/misc/getRevenuecatAccessToken.ts
+++ b/server/src/external/revenueCat/misc/getRevenuecatAccessToken.ts
@@ -11,7 +11,7 @@ import { decryptData, encryptData } from "@/utils/encryptUtils.js";
const TOKEN_EXPIRY_SKEW_MS = 60_000;
-const getOAuthConfigForEnv = ({
+const getOAuthConfigForEnv = (env: Env) => ({
revenueCatConfig,
env,
}: {
diff --git a/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts b/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts
index 12acd64dd..a15ee9176 100644
--- a/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts
+++ b/server/src/external/revenueCat/misc/registerRevenuecatWebhook.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv } from "@autumn/shared";
import type { initRevenuecatCli } from "./initRevenuecatCli.js";
@@ -8,19 +7,21 @@ type RcCli = ReturnType;
* Outbound base URL for our webhook receiver. Dev/staging use NGROK_URL (so RevenueCat
* can reach a local tunnel); production uses BETTER_AUTH_URL.
*/
-const getServerBaseUrl = (): string | undefined =>
- runtimeEnv.NODE_ENV !== "production"
- ? runtimeEnv.NGROK_URL
- : runtimeEnv.BETTER_AUTH_URL;
+const getServerBaseUrl = (env: Partial): string | undefined =>
+ env.NODE_ENV !== "production"
+ ? env.NGROK_URL
+ : env.BETTER_AUTH_URL;
export const getRevenuecatWebhookUrl = ({
orgId,
env,
+ serverEnv,
}: {
orgId: string;
env: AppEnv;
+ serverEnv: Partial;
}): string | null => {
- const base = getServerBaseUrl();
+ const base = getServerBaseUrl(serverEnv);
if (!base) return null;
// `:env` segment is the AppEnv value ("sandbox"/"live") — revenueCatMiddleware reads it verbatim.
return `${base.replace(/\/$/, "")}/webhooks/revenuecat/${orgId}/${env}`;
@@ -35,13 +36,15 @@ export const registerRevenuecatWebhook = async ({
orgId,
env,
secret,
+ serverEnv,
}: {
rcCli: RcCli;
orgId: string;
env: AppEnv;
secret: string;
+ serverEnv: Partial;
}): Promise<"exists" | "created" | "skipped"> => {
- const url = getRevenuecatWebhookUrl({ orgId, env });
+ const url = getRevenuecatWebhookUrl({ orgId, env, serverEnv });
if (!url) return "skipped";
const existing = await rcCli.listWebhookIntegrations();
diff --git a/server/src/external/revenueCat/misc/revenuecatOAuth.ts b/server/src/external/revenueCat/misc/revenuecatOAuth.ts
index 698f3baca..b6445a526 100644
--- a/server/src/external/revenueCat/misc/revenuecatOAuth.ts
+++ b/server/src/external/revenueCat/misc/revenuecatOAuth.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
CodeChallengeMethod,
generateCodeVerifier,
@@ -48,22 +47,22 @@ export const findMissingRcScopes = (grantedScopes: string[]): string[] => {
);
};
-const getRcOAuthClient = () => {
- const clientId = runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID;
- const clientSecret = runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET;
+const getRcOAuthClient = (env: Env) => {
+ const clientId = env.REVENUECAT_OAUTH_CLIENT_ID;
+ const clientSecret = env.REVENUECAT_OAUTH_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error("RevenueCat OAuth client credentials not configured");
}
- return new OAuth2Client(clientId, clientSecret, getRcOAuthRedirectUri());
+ return new OAuth2Client(clientId, clientSecret, getRcOAuthRedirectUri(env));
};
-export const getRcOAuthRedirectUri = () => {
- let serverUrl = runtimeEnv.BETTER_AUTH_URL;
+export const getRcOAuthRedirectUri = (env: Env) => {
+ let serverUrl = env.BETTER_AUTH_URL;
- if (runtimeEnv.NGROK_URL) {
- serverUrl = runtimeEnv.NGROK_URL;
+ if (env.NGROK_URL) {
+ serverUrl = env.NGROK_URL;
}
return `${(serverUrl ?? "").replace(/\/+$/, "")}/revenuecat/oauth_callback`;
@@ -73,12 +72,14 @@ export const createRcAuthorizationUrl = ({
state,
codeVerifier,
scopes = RC_OAUTH_SCOPES,
+ env,
}: {
state: string;
codeVerifier: string;
scopes?: string[];
+ env: Env;
}) => {
- const client = getRcOAuthClient();
+ const client = getRcOAuthClient(env);
return client.createAuthorizationURLWithPKCE(
RC_AUTHORIZE_URL,
state,
@@ -91,11 +92,13 @@ export const createRcAuthorizationUrl = ({
export const exchangeRcCode = async ({
code,
codeVerifier,
+ env,
}: {
code: string;
codeVerifier: string;
+ env: Env;
}) => {
- const client = getRcOAuthClient();
+ const client = getRcOAuthClient(env);
return client.validateAuthorizationCode(RC_TOKEN_URL, code, codeVerifier);
};
@@ -104,11 +107,13 @@ export const refreshRcTokens = async ({
// Omit scopes on refresh — re-requesting the full set triggers RC `invalid_scope`.
// An empty list reuses the originally-granted scopes (OAuth2 §6).
scopes = [],
+ env,
}: {
refreshToken: string;
scopes?: string[];
+ env: Env;
}) => {
- const client = getRcOAuthClient();
+ const client = getRcOAuthClient(env);
return client.refreshAccessToken(RC_TOKEN_URL, refreshToken, scopes);
};
diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts
index 7a8654309..3a7394545 100644
--- a/server/src/external/stripe/handleStripeWebhookEvent.ts
+++ b/server/src/external/stripe/handleStripeWebhookEvent.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import * as Sentry from "@sentry/bun";
import type { Context } from "hono";
import { Stripe } from "stripe";
@@ -123,7 +122,7 @@ export const handleStripeWebhookEvent = async (
}
if (
- runtimeEnv.NODE_ENV === "development" &&
+ env.NODE_ENV === "development" &&
error instanceof Error &&
error.message.includes("No stripe account linked to organization")
) {
diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts
index 5e7149518..3e56d6c1c 100644
--- a/server/src/external/stripe/stripeCusUtils.ts
+++ b/server/src/external/stripe/stripeCusUtils.ts
@@ -18,7 +18,7 @@ import { createStripeCustomer } from "@/external/stripe/customers";
import { CusService } from "@/internal/customers/CusService.js";
import type { TestContext } from "../../../tests/utils/testInitUtils/createTestContext";
-import { logger } from "../logtail/logtailUtils";
+import { createLogger } from "../logtail/logtailUtils";
const getStripeCus = async ({
stripeCli,
diff --git a/server/src/external/stripe/stripeOnboardingUtils.ts b/server/src/external/stripe/stripeOnboardingUtils.ts
index 4e71ab389..21b973a95 100644
--- a/server/src/external/stripe/stripeOnboardingUtils.ts
+++ b/server/src/external/stripe/stripeOnboardingUtils.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { type AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
@@ -21,7 +20,7 @@ export const createWebhookEndpoint = async (
) => {
const stripe = new Stripe(apiKey);
- const webhookBaseUrl = runtimeEnv.STRIPE_WEBHOOK_URL || runtimeEnv.SERVER_URL;
+ const webhookBaseUrl = env.STRIPE_WEBHOOK_URL || env.SERVER_URL;
if (!webhookBaseUrl) {
throw new RecaseError({
diff --git a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
index a40de52e1..b478500f5 100644
--- a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
+++ b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
@@ -14,7 +14,7 @@ import {
type Price,
type ProductOptions,
} from "@autumn/shared";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import {
diff --git a/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts b/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts
index ec80e336d..70eef5d20 100644
--- a/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts
+++ b/server/src/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils.ts
@@ -1,22 +1,24 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { getPrimaryRedis } from "@/external/redis/initRedis";
import { tryRedisRead, tryRedisWrite } from "@/utils/cacheUtils/cacheUtils";
export const setStripeSubscriptionLock = async ({
stripeSubscriptionId,
lockedAtMs,
+ env,
}: {
stripeSubscriptionId: string;
lockedAtMs: number;
+ env: Env;
}) => {
const primaryRedis = getPrimaryRedis();
await tryRedisWrite(
+ env,
async () =>
primaryRedis.set(
`sub:${stripeSubscriptionId}`,
JSON.stringify({ lockedAtMs }),
"EX",
- runtimeEnv.NODE_ENV === "production" ? 60 : 3,
+ env.NODE_ENV === "production" ? 60 : 3,
),
primaryRedis,
);
@@ -28,11 +30,13 @@ type StripeSubscriptionLock = {
export const getStripeSubscriptionLock = async ({
stripeSubscriptionId,
+ env,
}: {
stripeSubscriptionId: string;
+ env: Env;
}): Promise => {
const primaryRedis = getPrimaryRedis();
- return tryRedisRead(async () => {
+ return tryRedisRead(env, async () => {
const value = await primaryRedis.get(`sub:${stripeSubscriptionId}`);
if (!value) return null;
return JSON.parse(value) as StripeSubscriptionLock;
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 0ec9575c6..6a403b4aa 100644
--- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts
+++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleSchedulePhaseChanges/releaseScheduleIfLastPhase.ts
@@ -1,4 +1,3 @@
-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";
@@ -74,7 +73,7 @@ export const releaseScheduleIfLastPhase = async ({
return true;
} catch (error: unknown) {
if (error instanceof Error) {
- if (runtimeEnv.NODE_ENV === "development") {
+ if (env.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 f636bee3d..300219266 100644
--- a/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts
+++ b/server/src/external/stripe/webhookMiddlewares/stripeConnectSeederMiddleware.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
type AppEnv,
AuthType,
@@ -55,8 +54,8 @@ export const stripeConnectSeederMiddleware = async (
const signature = c.req.header("stripe-signature") || "";
const skipVerify =
- runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
- runtimeEnv.NODE_ENV !== "production";
+ env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
+ env.NODE_ENV !== "production";
let event: Stripe.Event;
if (skipVerify) {
@@ -79,7 +78,7 @@ export const stripeConnectSeederMiddleware = async (
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
- if (runtimeEnv.NODE_ENV !== "development") {
+ if (env.NODE_ENV !== "development") {
logger.warn(`Webhook verification error: ${message}`);
}
return c.json({ error: message }, 400);
@@ -115,7 +114,7 @@ export const stripeConnectSeederMiddleware = async (
return c.json({ error: "Failed to resolve org for Stripe webhook" }, 500);
}
- if (runtimeEnv.NODE_ENV !== "development") {
+ if (env.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 2403e0059..8109d9385 100644
--- a/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts
+++ b/server/src/external/stripe/webhookMiddlewares/stripeIdempotencyMiddleware.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { tryCatch } from "@autumn/shared";
import type { Context, Next } from "hono";
import { redis } from "@/external/redis/initRedis";
@@ -17,7 +16,7 @@ export const stripeIdempotencyMiddleware = async (
c: Context,
next: Next,
) => {
- if (runtimeEnv.NODE_ENV === "development") {
+ if (env.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 d5eaed76f..81cca6853 100644
--- a/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts
+++ b/server/src/external/stripe/webhookMiddlewares/stripeLegacySeederMiddleware.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { type AppEnv, AuthType } from "@autumn/shared";
import type { Context, Next } from "hono";
import Stripe from "stripe";
@@ -55,8 +54,8 @@ export const stripeLegacySeederMiddleware = async (
const signature = c.req.header("stripe-signature") || "";
const skipVerify =
- runtimeEnv.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
- runtimeEnv.NODE_ENV !== "production";
+ env.STRIPE_WEBHOOK_SKIP_VERIFY === "true" &&
+ env.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 22e5cbb0b..271bf8577 100644
--- a/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts
+++ b/server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts
@@ -1,4 +1,3 @@
-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";
@@ -22,7 +21,7 @@ export const stripeSyncMiddleware = async (
if (!org || !stripeEvent) return;
if (
- runtimeEnv.NODE_ENV === "production" &&
+ c.env.NODE_ENV === "production" &&
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
)
return;
diff --git a/server/src/external/stripe/webhookMiddlewares/stripeWebhookContext.ts b/server/src/external/stripe/webhookMiddlewares/stripeWebhookContext.ts
index 7b6abf6aa..069855b67 100644
--- a/server/src/external/stripe/webhookMiddlewares/stripeWebhookContext.ts
+++ b/server/src/external/stripe/webhookMiddlewares/stripeWebhookContext.ts
@@ -9,6 +9,7 @@ export interface StripeWebhookContext extends AutumnContext {
}
export type StripeWebhookHonoEnv = {
+ Bindings: Env;
Variables: {
ctx: StripeWebhookContext;
validated: boolean;
diff --git a/server/src/external/supabase/createSupabaseClient.ts b/server/src/external/supabase/createSupabaseClient.ts
index 263fc7c9a..dbda6be49 100644
--- a/server/src/external/supabase/createSupabaseClient.ts
+++ b/server/src/external/supabase/createSupabaseClient.ts
@@ -1,11 +1,10 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { createClient } from "@supabase/supabase-js";
-export const createSupabaseClient = () => {
+export const createSupabaseClient = (env: Env) => {
try {
return createClient(
- runtimeEnv.SUPABASE_URL!,
- runtimeEnv.SUPABASE_SERVICE_KEY!,
+ env.SUPABASE_URL!,
+ env.SUPABASE_SERVICE_KEY!,
);
} catch (error) {
console.error("Error creating Supabase client:", error);
diff --git a/server/src/external/supabase/storageUtils.ts b/server/src/external/supabase/storageUtils.ts
index 0288ecb1c..970f499a7 100644
--- a/server/src/external/supabase/storageUtils.ts
+++ b/server/src/external/supabase/storageUtils.ts
@@ -1,13 +1,15 @@
import { createSupabaseClient } from "@/external/supabase/createSupabaseClient";
const readFile = async ({
+ env,
bucket = "autumn",
path,
}: {
+ env: Env;
bucket: string;
path: string;
}) => {
- const sb = createSupabaseClient();
+ const sb = createSupabaseClient(env);
const { data, error } = await sb.storage.from(bucket).download(path);
if (error) {
@@ -17,15 +19,17 @@ const readFile = async ({
};
const uploadFile = async ({
+ env,
path,
file,
contentType,
}: {
+ env: Env;
path: string;
file: Buffer;
contentType?: string;
}) => {
- const sb = createSupabaseClient();
+ const sb = createSupabaseClient(env);
const { data, error } = await sb.storage.from("autumn").upload(path, file, {
upsert: true,
@@ -39,8 +43,8 @@ const uploadFile = async ({
return data;
};
-export const getUploadUrl = async ({ path }: { path: string }) => {
- const sb = createSupabaseClient();
+export const getUploadUrl = async ({ env, path }: { env: Env; path: string }) => {
+ const sb = createSupabaseClient(env);
await sb.storage.from("autumn").remove([path]);
const { data, error } = await sb.storage
diff --git a/server/src/external/svix/svixHelpers.ts b/server/src/external/svix/svixHelpers.ts
index 9657da231..10e884f02 100644
--- a/server/src/external/svix/svixHelpers.ts
+++ b/server/src/external/svix/svixHelpers.ts
@@ -1,4 +1,3 @@
-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";
@@ -54,7 +53,7 @@ export const sendSvixEvent = async ({
idempotencyKey?: string;
tags?: string[];
}) => {
- if (!runtimeEnv.SVIX_API_KEY) return;
+ if (!env.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 4afb94488..a7c04b1f8 100644
--- a/server/src/external/svix/svixUtils.ts
+++ b/server/src/external/svix/svixUtils.ts
@@ -1,27 +1,30 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, type Organization } from "@autumn/shared";
import { Svix } from "svix";
-import { logger } from "../logtail/logtailUtils.js";
+import { createLogger } from "../logtail/logtailUtils.js";
-export const createSvixCli = () => {
- return new Svix(runtimeEnv.SVIX_API_KEY as string);
+export const createSvixCli = (env: Env) => {
+ return new Svix(env.SVIX_API_KEY as string);
};
export function safeSvix any>({
fn,
action,
+ env,
}: {
fn: T;
action: string;
+ env: Env;
}): (...args: Parameters) => Promise | undefined> {
return async (...args: Parameters) => {
- if (!runtimeEnv.SVIX_API_KEY) {
+ if (!env.SVIX_API_KEY) {
+ const logger = createLogger(env);
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
return;
}
try {
return await fn(...args);
} catch (error) {
+ const logger = createLogger(env);
logger.error(`Error ${action}: ${error}`);
}
};
diff --git a/server/src/external/tinybird/initClickhouse.ts b/server/src/external/tinybird/initClickhouse.ts
index 832a81070..7d9c01dea 100644
--- a/server/src/external/tinybird/initClickhouse.ts
+++ b/server/src/external/tinybird/initClickhouse.ts
@@ -1,23 +1,28 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { type ClickHouseClient, createClient } from "@clickhouse/client";
-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(
- `[Tinybird ClickHouse] Configured with URL: ${TINYBIRD_CLICKHOUSE_URL}`,
- );
-}
-
/** ClickHouse client for raw SQL queries to Tinybird. Null if not configured. */
-export const clickhouseClient: ClickHouseClient | null =
- TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN
- ? createClient({
- url: TINYBIRD_CLICKHOUSE_URL,
- password: TINYBIRD_TOKEN,
- })
- : null;
+let clickhouseClient: ClickHouseClient | null = null;
+
+export const initClickhouse = (env: Env) => {
+ const TINYBIRD_CLICKHOUSE_URL = env.TINYBIRD_US_EAST_CLICKHOUSE_URL;
+ const TINYBIRD_TOKEN = env.TINYBIRD_US_EAST_TOKEN;
+
+ if (TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN) {
+ console.log(
+ `[Tinybird ClickHouse] Configured with URL: ${TINYBIRD_CLICKHOUSE_URL}`,
+ );
+ }
+
+ clickhouseClient =
+ TINYBIRD_CLICKHOUSE_URL && TINYBIRD_TOKEN
+ ? createClient({
+ url: TINYBIRD_CLICKHOUSE_URL,
+ password: TINYBIRD_TOKEN,
+ })
+ : null;
+};
+
+export { clickhouseClient };
/** Get ClickHouse client, throws if not configured. */
export const getClickhouseClient = (): ClickHouseClient => {
diff --git a/server/src/external/tinybird/initTinybirdV2.ts b/server/src/external/tinybird/initTinybirdV2.ts
index 20eae0071..de166fa18 100644
--- a/server/src/external/tinybird/initTinybirdV2.ts
+++ b/server/src/external/tinybird/initTinybirdV2.ts
@@ -1,23 +1,29 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { createTinybirdApi } from "@tinybirdco/sdk";
+import { createTinybirdApi, type TinybirdApi } from "@tinybirdco/sdk";
-const TINYBIRD_SECONDARY_API_URL = runtimeEnv.TINYBIRD_API_URL;
-const TINYBIRD_SECONDARY_TOKEN = runtimeEnv.TINYBIRD_TOKEN;
+export interface TinybirdSecondaryEnv {
+ TINYBIRD_API_URL: string;
+ TINYBIRD_TOKEN: string;
+}
/** 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
* GCP). Once us-east is stable, delete this file + the dual-write logic in
- * sendEvents.ts. */
-export const tinybirdSecondaryApi =
- TINYBIRD_SECONDARY_API_URL && TINYBIRD_SECONDARY_TOKEN
- ? createTinybirdApi({
- baseUrl: TINYBIRD_SECONDARY_API_URL,
- token: TINYBIRD_SECONDARY_TOKEN,
- })
- : null;
+ * sendEvents.ts.
+ * Initialized via configureTinybirdSecondaryApi(). Null until initialized. */
+export let tinybirdSecondaryApi: TinybirdApi | null = null;
-if (tinybirdSecondaryApi) {
- console.log(
- `[Tinybird] secondary dual-write configured with URL: ${TINYBIRD_SECONDARY_API_URL}`,
- );
-}
+/** Configure the secondary Tinybird API client. Must be called during initialization. */
+export const configureTinybirdSecondaryApi = (env: TinybirdSecondaryEnv): void => {
+ const apiUrl = env.TINYBIRD_API_URL;
+ const token = env.TINYBIRD_TOKEN;
+
+ if (apiUrl && token) {
+ tinybirdSecondaryApi = createTinybirdApi({
+ baseUrl: apiUrl,
+ token,
+ });
+ console.log(
+ `[Tinybird] secondary dual-write configured with URL: ${apiUrl}`,
+ );
+ }
+};
diff --git a/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts
index 364a8d4d1..8ea677279 100644
--- a/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts
+++ b/server/src/external/tinybird/migrations/migrationItemEventsDataSource.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
defineDatasource,
defineEndpoint,
@@ -10,16 +9,32 @@ import {
t,
} from "@tinybirdco/sdk";
-const TINYBIRD_US_EAST_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
-const TINYBIRD_US_EAST_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
+export const getTinybirdApiUrl = (env: Env) => env.TINYBIRD_US_EAST_API_URL;
+export const getTinybirdToken = (env: Env) => env.TINYBIRD_US_EAST_TOKEN;
-const migrationTinybirdConfig =
- TINYBIRD_US_EAST_API_URL && TINYBIRD_US_EAST_TOKEN
- ? {
- baseUrl: TINYBIRD_US_EAST_API_URL,
- token: TINYBIRD_US_EAST_TOKEN,
- }
- : null;
+/** Tinybird client for migration item events. Null until initMigrationTinybird(env) is called. */
+export let migrationTinybird: Tinybird | null = null;
+
+export const initMigrationTinybird = (env: Env) => {
+ const apiUrl = env.TINYBIRD_US_EAST_API_URL;
+ const token = env.TINYBIRD_US_EAST_TOKEN;
+
+ if (!apiUrl || !token) {
+ return;
+ }
+
+ migrationTinybird = new Tinybird({
+ datasources: {
+ itemEvents: migrationItemEventsDatasource,
+ },
+ pipes: {
+ listItemEvents: listMigrationItemEventsEndpoint,
+ },
+ baseUrl: apiUrl,
+ token,
+ devMode: false,
+ });
+};
export type MigrationItemEventStatus = "succeeded" | "skipped" | "failed";
@@ -124,16 +139,3 @@ export const listMigrationItemEventsEndpoint = defineEndpoint(
},
},
);
-
-export const migrationTinybird = migrationTinybirdConfig
- ? new Tinybird({
- datasources: {
- itemEvents: migrationItemEventsDatasource,
- },
- pipes: {
- listItemEvents: listMigrationItemEventsEndpoint,
- },
- ...migrationTinybirdConfig,
- devMode: false,
- })
- : null;
diff --git a/server/src/external/tinybird/tinybirdUtils.ts b/server/src/external/tinybird/tinybirdUtils.ts
index f6f55d1f6..051970fa4 100644
--- a/server/src/external/tinybird/tinybirdUtils.ts
+++ b/server/src/external/tinybird/tinybirdUtils.ts
@@ -1,22 +1,23 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { ErrCode, RecaseError } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
-const TINYBIRD_API_URL = runtimeEnv.TINYBIRD_US_EAST_API_URL;
-const TINYBIRD_TOKEN = runtimeEnv.TINYBIRD_US_EAST_TOKEN;
-
export type TinybirdConfig = {
baseUrl: string;
token: string;
};
-export const tinybirdConfig: TinybirdConfig | null =
- TINYBIRD_API_URL && TINYBIRD_TOKEN
- ? {
- baseUrl: TINYBIRD_API_URL,
- token: TINYBIRD_TOKEN,
- }
- : null;
+export let tinybirdConfig: TinybirdConfig | null = null;
+
+export const initTinybirdConfig = (env: Env) => {
+
+ tinybirdConfig =
+ TINYBIRD_API_URL && TINYBIRD_TOKEN
+ ? {
+ baseUrl: TINYBIRD_API_URL,
+ token: TINYBIRD_TOKEN,
+ }
+ : null;
+};
/** Check if Tinybird is configured. */
export const isTinybirdConfigured = (): boolean => tinybirdConfig !== null;
diff --git a/server/src/external/vercel/misc/vercelAuth.ts b/server/src/external/vercel/misc/vercelAuth.ts
index 023b63508..a9e140840 100644
--- a/server/src/external/vercel/misc/vercelAuth.ts
+++ b/server/src/external/vercel/misc/vercelAuth.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, type Organization } from "@autumn/shared";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { JWTExpired, JWTInvalid } from "jose/errors";
@@ -43,7 +42,7 @@ const synthesizeTestClaims = ({
env: AppEnv;
testOptions?: VercelOidcTestOptions;
}): OidcClaims | null => {
- if (runtimeEnv.NODE_ENV === "production") return null;
+ if (env.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 d54eaf76d..09237f8b0 100644
--- a/server/src/external/vercel/misc/vercelSdkOptions.ts
+++ b/server/src/external/vercel/misc/vercelSdkOptions.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
export type VercelSdkTestOptions = {
mockVercelApi?: boolean;
};
@@ -6,12 +5,16 @@ export type VercelSdkTestOptions = {
/**
* Only tests opt into the local Vercel SDK mock; dev/manual flows hit Vercel.
*/
-export const getVercelSdkServerURL = (
- testOptions?: VercelSdkTestOptions,
-): string | undefined => {
- if (runtimeEnv.NODE_ENV === "production") return undefined;
+export const getVercelSdkServerURL = ({
+ env,
+ testOptions,
+}: {
+ env: Env;
+ testOptions?: VercelSdkTestOptions;
+}): string | undefined => {
+ if (env.NODE_ENV === "production") return undefined;
if (testOptions?.mockVercelApi !== true) return undefined;
- const base = runtimeEnv.BETTER_AUTH_URL;
+ const base = env.BETTER_AUTH_URL;
if (!base) return undefined;
return `${base.replace(/\/$/, "")}/__test/vercel/api`;
};
diff --git a/server/src/honoMiddlewares/adminAuthMiddleware.ts b/server/src/honoMiddlewares/adminAuthMiddleware.ts
index 71da4c0f3..173da5895 100644
--- a/server/src/honoMiddlewares/adminAuthMiddleware.ts
+++ b/server/src/honoMiddlewares/adminAuthMiddleware.ts
@@ -1,13 +1,14 @@
import { ErrCode, RecaseError } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
-import { auth } from "@/utils/auth.js";
+import { createAuth } from "@/utils/auth.js";
/**
* Admin auth middleware for Hono
* Validates that the user has an "admin" role
*/
export const adminAuthMiddleware = async (c: Context, next: Next) => {
+ const auth = createAuth(c.env);
const data = await auth.api.getSession({
headers: c.req.raw.headers,
});
diff --git a/server/src/honoMiddlewares/authMiddlewares/handleOAuthMiddleware.ts b/server/src/honoMiddlewares/authMiddlewares/handleOAuthMiddleware.ts
index d0951fe23..cc4f780e3 100644
--- a/server/src/honoMiddlewares/authMiddlewares/handleOAuthMiddleware.ts
+++ b/server/src/honoMiddlewares/authMiddlewares/handleOAuthMiddleware.ts
@@ -35,6 +35,7 @@ export const handleOAuthMiddleware = async ({
const env = getOAuthEnvironment({ c });
const tokenRecord = await getOAuthAccessTokenRecord({
db: ctx.db,
+ env: c.env,
accessToken: token,
resource: c.req.header("x-autumn-oauth-resource") ?? null,
requestedScopes: null,
diff --git a/server/src/honoMiddlewares/baseMiddleware.ts b/server/src/honoMiddlewares/baseMiddleware.ts
index 9ff535040..0c3c211f9 100644
--- a/server/src/honoMiddlewares/baseMiddleware.ts
+++ b/server/src/honoMiddlewares/baseMiddleware.ts
@@ -8,7 +8,7 @@ import {
} from "@autumn/shared";
import type { Context, Next } from "hono";
import { db, dbGeneral } from "@/db/initDrizzle.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { resolveRedisV2 } from "@/external/redis/resolveRedisV2.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { generateId } from "@/utils/genUtils.js";
@@ -41,6 +41,8 @@ const redactSensitiveRequestBody = ({ body }: { body: unknown }): unknown => {
* Sets up: db, logger, id, timestamp
*/
export const baseMiddleware = async (c: Context, next: Next) => {
+ const env = c.env;
+ const logger = createLogger(env);
// const env = (c.req.header("app_env") as AppEnv) || AppEnv.Sandbox;
const id =
c.req.header("rndr-id") ||
diff --git a/server/src/honoMiddlewares/betterAuthMiddleware.ts b/server/src/honoMiddlewares/betterAuthMiddleware.ts
index f3e77d89c..a30362165 100644
--- a/server/src/honoMiddlewares/betterAuthMiddleware.ts
+++ b/server/src/honoMiddlewares/betterAuthMiddleware.ts
@@ -2,7 +2,7 @@ import { type AppEnv, AuthType, ErrCode, RecaseError } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
-import { auth } from "@/utils/auth.js";
+import { createAuth } from "@/utils/auth.js";
/**
* Better Auth middleware for dashboard/session authentication
@@ -19,6 +19,7 @@ export const betterAuthMiddleware = async (c: Context, next: Next) => {
const ctx = c.get("ctx");
// Step 1: Get session from Better Auth
+ const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
diff --git a/server/src/honoMiddlewares/routerRateLimiter/index.ts b/server/src/honoMiddlewares/routerRateLimiter/index.ts
index 82e36b050..0be6b532e 100644
--- a/server/src/honoMiddlewares/routerRateLimiter/index.ts
+++ b/server/src/honoMiddlewares/routerRateLimiter/index.ts
@@ -1,6 +1,6 @@
import type { Context, Next } from "hono";
import { rateLimiter } from "hono-rate-limiter";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { shouldUseRedis } from "@/external/redis/initRedis.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { createRateLimitRedisStore } from "@/internal/misc/rateLimiter/rateLimitRedisStore.js";
@@ -32,6 +32,8 @@ export const createRouterRateLimiter = ({
};
return async (c: Context, next: Next) => {
+ const logger = createLogger(c.env);
+
if (!shouldUseRedis()) return next();
try {
diff --git a/server/src/honoUtils/handleHealthCheck.ts b/server/src/honoUtils/handleHealthCheck.ts
index 19dfdd5eb..8c7932112 100644
--- a/server/src/honoUtils/handleHealthCheck.ts
+++ b/server/src/honoUtils/handleHealthCheck.ts
@@ -1,5 +1,6 @@
import type { Context } from "hono";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
+import type { Logger } from "@/external/logtail/logtailUtils.js";
import { hasRedisConfig, redis } from "@/external/redis/initRedis.js";
import { hasRedisV2Config, redisV2 } from "@/external/redis/initRedisV2.js";
import type { HonoEnv } from "./HonoEnv";
@@ -7,6 +8,27 @@ import { evaluateStartupGate } from "./startupGate.js";
const startedAt = Date.now();
let startupReady = false;
+let _logger: Logger | null = null;
+
+const getLogger = (): Logger => {
+ if (!_logger) {
+ throw new Error("handleHealthCheck not initialized — call initHealthCheck(env) first");
+ }
+ return _logger;
+};
+
+/**
+ * Initialize the health check module. Must be called before the
+ * health check endpoint is used and before Redis events fire.
+ */
+export const initHealthCheck = (env: Env): void => {
+ if (_logger) return;
+ _logger = createLogger(env);
+
+ if (hasRedisConfig) redis.once("ready", tryLatchStartupReady);
+ if (hasRedisV2Config) redisV2.once("ready", tryLatchStartupReady);
+ tryLatchStartupReady();
+};
const tryLatchStartupReady = () => {
if (startupReady) return;
@@ -17,7 +39,7 @@ const tryLatchStartupReady = () => {
});
if (!ready) return;
startupReady = true;
- logger.info(`[health-check] startup gate latched (${reason})`, {
+ getLogger().info(`[health-check] startup gate latched (${reason})`, {
redis_status: redis.status,
redis_v2_status: redisV2.status,
has_redis_config: hasRedisConfig,
@@ -25,11 +47,10 @@ const tryLatchStartupReady = () => {
});
};
-if (hasRedisConfig) redis.once("ready", tryLatchStartupReady);
-if (hasRedisV2Config) redisV2.once("ready", tryLatchStartupReady);
-tryLatchStartupReady();
-
export const handleHealthCheck = async (c: Context) => {
+ if (!_logger) {
+ initHealthCheck(c.env);
+ }
if (!startupReady) {
tryLatchStartupReady();
if (!startupReady) {
diff --git a/server/src/init.ts b/server/src/init.ts
index 3793f0ebf..b97fa54e9 100644
--- a/server/src/init.ts
+++ b/server/src/init.ts
@@ -1,7 +1,3 @@
-// Sentry + OpenTelemetry must be imported before any application code
-import { runtimeEnv } from "@/utils/envUtils.js";
-await import("./sentry.js");
-
import cluster from "node:cluster";
import http from "node:http";
import os from "node:os";
@@ -13,7 +9,8 @@ import {
} from "./db/pgHealthMonitor.js";
import { startPgPoolMonitor, stopPgPoolMonitor } from "./db/pgPoolMonitor.js";
import { getRedactedDatabaseUrls } from "./db/redactDatabaseUrl.js";
-import { logger } from "./external/logtail/logtailUtils.js";
+import { createLogger } from "./external/logtail/logtailUtils.js";
+import { initDrizzleModules } from "./db/initDrizzle.js";
import {
startAllEdgeConfigPolling,
stopAllEdgeConfigPolling,
@@ -29,8 +26,6 @@ import "./internal/misc/stripeSync/stripeSyncStore.js";
import "./internal/misc/redisV2Cache/redisV2CacheStore.js";
import "./internal/misc/cacheV2Ramp/cacheV2RampStore.js";
import "./internal/misc/jobQueues/jobQueueStore.js";
-// Side-effect: configures trigger.dev SDK to use TRIGGER_SERVER_SECRET_KEY.
-import "./trigger/configureTrigger.js";
import { closeStripeSyncEngine } from "@autumn/stripe-sync";
import {
startRedisMonitor,
@@ -45,21 +40,29 @@ import {
} from "./external/redis/initUtils/redisV2Availability.js";
import { preWarmOrgRedisConnections } from "./external/redis/orgRedisPool.js";
import { createHonoApp } from "./initHono.js";
-import { otelSdk } from "./instrumentation.js";
+import { initTelemetry, otelSdk } from "./instrumentation.js";
+import { initSentry } from "./sentry.js";
+import { configureTrigger } from "./trigger/configureTrigger.js";
import { checkEnvVars } from "./utils/initUtils.js";
import { startMemoryMonitor } from "./utils/memoryMonitor.js";
-checkEnvVars();
-
let shuttingDown = false;
-const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
- logger.info(getRedactedDatabaseUrls(), "DB URLs");
+const init = async ({
+ startupStartedAt,
+ env,
+}: {
+ startupStartedAt: number;
+ env: Env;
+}) => {
+ const logger = createLogger(env);
- const app = createHonoApp(runtimeEnv);
+ logger.info(getRedactedDatabaseUrls(env), "DB URLs");
- initPgHealthMonitor({ client: clientCritical });
- startPgPoolMonitor();
+ const app = createHonoApp(env);
+
+ initPgHealthMonitor({ client: clientCritical, env });
+ startPgPoolMonitor(env);
void warmupRegionalRedis().catch((error) => {
logger.warn("[Redis] Warmup failed", { error });
@@ -73,9 +76,7 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
startRedisMonitor();
startRedisV2Monitor();
- const PORT = runtimeEnv.SERVER_PORT
- ? Number.parseInt(runtimeEnv.SERVER_PORT)
- : 8080;
+ const PORT = env.SERVER_PORT ? Number.parseInt(env.SERVER_PORT) : 8080;
const requestListener = getRequestListener(app.fetch);
const server = http.createServer(requestListener);
@@ -89,17 +90,29 @@ const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
console.log(
`Server running on port ${PORT} (${startupDurationMs}ms startup)`,
);
- startMemoryMonitor("server", 60_000);
+ startMemoryMonitor("server", env, 60_000);
resolve();
});
});
};
-if (runtimeEnv.NODE_ENV === "development") {
- registerFatalErrorHandlers();
- await init({ startupStartedAt: Date.now() });
- registerShutdownHandlers();
-} else {
+export const startNodeServer = async (env: Env) => {
+ const logger = createLogger(env);
+
+ initDrizzleModules(env);
+ initSentry(env);
+ initTelemetry(env);
+ configureTrigger(env);
+ checkEnvVars(env);
+
+ if (env.NODE_ENV === "development") {
+ registerFatalErrorHandlers(logger);
+ await init({ startupStartedAt: Date.now(), env });
+ registerShutdownHandlers(logger);
+
+ return;
+ }
+
const numCPUs = os.cpus().length;
if (cluster.isPrimary) {
@@ -123,15 +136,15 @@ if (runtimeEnv.NODE_ENV === "development") {
cluster.fork();
});
- registerShutdownHandlers();
+ registerShutdownHandlers(logger);
} else {
- registerFatalErrorHandlers();
- await init({ startupStartedAt: Date.now() });
- registerShutdownHandlers();
+ registerFatalErrorHandlers(logger);
+ await init({ startupStartedAt: Date.now(), env });
+ registerShutdownHandlers(logger);
}
-}
+};
-function registerFatalErrorHandlers() {
+function registerFatalErrorHandlers(logger: ReturnType) {
const exitAfterLog = () => setTimeout(() => process.exit(1), 100);
const logFatal = (event: string, error: unknown) => {
logger.error(event, {
@@ -152,13 +165,13 @@ function registerFatalErrorHandlers() {
});
}
-function registerShutdownHandlers() {
- process.on("SIGTERM", gracefulShutdown);
- process.on("SIGINT", gracefulShutdown);
+function registerShutdownHandlers(logger: ReturnType) {
+ process.on("SIGTERM", () => gracefulShutdown(logger));
+ process.on("SIGINT", () => gracefulShutdown(logger));
// Do NOT use process.on("exit", ...) for async cleanup!
}
-async function gracefulShutdown() {
+async function gracefulShutdown(logger: ReturnType) {
shuttingDown = true;
console.log("Shutting down worker, flushing telemetry and closing DB...");
try {
diff --git a/server/src/initHono.ts b/server/src/initHono.ts
index aa88dead8..166b38bfa 100644
--- a/server/src/initHono.ts
+++ b/server/src/initHono.ts
@@ -21,7 +21,7 @@ import { apiRouter } from "./routers/apiRouter.js";
import { createChatProxyRouter } from "./routers/chatProxyRouter.js";
import { createInternalRouter } from "./routers/internalRouter.js";
import { publicRouter } from "./routers/publicRouter.js";
-import { auth } from "./utils/auth.js";
+import { createAuth } from "./utils/auth.js";
import { isAllowedOrigin } from "./utils/corsOrigins.js";
const ALLOWED_HEADERS = [
@@ -50,6 +50,7 @@ const ALLOWED_HEADERS = [
export const createHonoApp = (env: Env) => {
const app = new Hono();
+ const auth = createAuth(env);
app.route("", createChatProxyRouter(env));
diff --git a/server/src/instrumentation.ts b/server/src/instrumentation.ts
index 8cb967a1a..3803acd0b 100644
--- a/server/src/instrumentation.ts
+++ b/server/src/instrumentation.ts
@@ -1,4 +1,3 @@
-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 +13,16 @@ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN);
let sdk: NodeSDK | null = null;
-if (runtimeEnv.AXIOM_TOKEN) {
+export const initTelemetry = (env: Env) => {
+ if (!env.AXIOM_TOKEN) return sdk;
+
// NodeSDK reads OTEL_SERVICE_NAME to set the service resource attribute
- runtimeEnv.OTEL_SERVICE_NAME = "autumn-server";
+ env.OTEL_SERVICE_NAME = "autumn-server";
const traceExporter = new OTLPTraceExporter({
url: "https://api.axiom.co/v1/traces",
headers: {
- Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
+ Authorization: `Bearer ${env.AXIOM_TOKEN}`,
"X-Axiom-Dataset": "otel",
},
});
@@ -30,18 +31,18 @@ if (runtimeEnv.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 = runtimeEnv.NODE_ENV !== "production";
+ const isDev = env.NODE_ENV !== "production";
const exportProcessor = new BatchSpanProcessor(traceExporter, {
scheduledDelayMillis: isDev ? 1000 : 5000,
});
const filteredExportProcessor = new FilteringSpanProcessor(exportProcessor);
- const metricReader = runtimeEnv.AXIOM_METRICS_DATASET
+ const metricReader = env.AXIOM_METRICS_DATASET
? new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: "https://api.axiom.co/v1/metrics",
headers: {
- Authorization: `Bearer ${runtimeEnv.AXIOM_TOKEN}`,
- "x-axiom-metrics-dataset": runtimeEnv.AXIOM_METRICS_DATASET,
+ Authorization: `Bearer ${env.AXIOM_TOKEN}`,
+ "x-axiom-metrics-dataset": env.AXIOM_METRICS_DATASET,
},
}),
exportIntervalMillis: 60_000,
@@ -68,6 +69,8 @@ if (runtimeEnv.AXIOM_TOKEN) {
};
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);
-}
+
+ return sdk;
+};
export { sdk as otelSdk };
diff --git a/server/src/internal/admin/handleSlackAdminChat.ts b/server/src/internal/admin/handleSlackAdminChat.ts
index a522f4800..9b2d0ce74 100644
--- a/server/src/internal/admin/handleSlackAdminChat.ts
+++ b/server/src/internal/admin/handleSlackAdminChat.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import crypto, { randomUUID } from "node:crypto";
import { stripOAuthTokenPrefix } from "@autumn/auth";
import {
@@ -48,9 +47,15 @@ const findTargetOrg = ({
),
});
-const getSlackAdminInstallation = async ({ db }: { db: DrizzleCli }) =>
+const getSlackAdminInstallation = async ({
+ db,
+ env,
+}: {
+ db: DrizzleCli;
+ env: Env;
+}) =>
db.query.chatInstallations.findFirst({
- where: eq(chatInstallations.provider, getSlackAdminProvider()),
+ where: eq(chatInstallations.provider, getSlackAdminProvider({ env })),
});
const getSlackAdminOAuthCredentials = async ({
@@ -80,10 +85,16 @@ const getOrgSummary = async ({
},
});
-const decryptChatCredentialToken = ({ token }: { token: string }) => {
+const decryptChatCredentialToken = ({
+ env,
+ token,
+}: {
+ env: Env;
+ token: string;
+}) => {
const key = crypto
.createHash("sha256")
- .update(runtimeEnv.ENCRYPTION_PASSWORD ?? "")
+ .update(env.ENCRYPTION_PASSWORD ?? "")
.digest();
const buffer = Buffer.from(token, "base64");
if (buffer[0] !== 1) throw new Error("Unsupported encrypted payload");
@@ -112,9 +123,11 @@ const getStoredOAuthTokenValues = async ({
const revokeSlackAdminOAuthArtifacts = async ({
db,
+ env,
credentials,
}: {
db: Pick;
+ env: Env;
credentials: ChatOAuthCredential[];
}) => {
const consentIds = [
@@ -129,18 +142,22 @@ const revokeSlackAdminOAuthArtifacts = async ({
const accessTokenValues: string[] = [];
const refreshTokenValues: string[] = [];
for (const credential of credentials) {
- accessTokenValues.push(
- ...(await getStoredOAuthTokenValues({
- token: decryptChatCredentialToken({ token: credential.access_token }),
- stripPrefix: true,
- })),
- );
- refreshTokenValues.push(
- ...(await getStoredOAuthTokenValues({
- token: decryptChatCredentialToken({
- token: credential.refresh_token,
- }),
- })),
+ accessTokenValues.push(
+ ...(await getStoredOAuthTokenValues({
+ token: decryptChatCredentialToken({
+ env,
+ token: credential.access_token,
+ }),
+ stripPrefix: true,
+ })),
+ );
+ refreshTokenValues.push(
+ ...(await getStoredOAuthTokenValues({
+ token: decryptChatCredentialToken({
+ env,
+ token: credential.refresh_token,
+ }),
+ })),
);
}
@@ -184,10 +201,10 @@ const revokeSlackAdminOAuthArtifacts = async ({
export const handleCreateSlackAdminInstall = createRoute({
scopes: [Scopes.Superuser],
handler: async (c) => {
- const ctx = c.get("ctx");
- const state = createChatInstallState({
- secret: getChatStateSecret(),
- provider: getSlackAdminProvider(),
+ const ctx = c.get("ctx");
+ const state = createChatInstallState({
+ secret: getChatStateSecret(c.env),
+ provider: getSlackAdminProvider({ env: c.env }),
orgId: ctx.org.id,
userId: ctx.userId ?? "",
env: ctx.env,
@@ -195,7 +212,7 @@ export const handleCreateSlackAdminInstall = createRoute({
nonce: randomUUID(),
});
- return c.json({ url: createSlackInstallUrl(state) });
+ return c.json({ url: createSlackInstallUrl(c.env, state) });
},
});
@@ -203,7 +220,7 @@ export const handleGetSlackAdminInstall = createRoute({
scopes: [Scopes.Superuser],
handler: async (c) => {
const { db } = c.get("ctx");
- const installation = await getSlackAdminInstallation({ db });
+ const installation = await getSlackAdminInstallation({ db, env: c.env });
const targetOrg = installation
? await getOrgSummary({ db, orgId: installation.org_id })
: null;
@@ -248,7 +265,7 @@ export const handleUpdateSlackAdminTarget = createRoute({
const ctx = c.get("ctx");
const { db } = ctx;
const { org_id: orgIdOrSlug, env } = c.req.valid("json");
- const installation = await getSlackAdminInstallation({ db });
+ const installation = await getSlackAdminInstallation({ db, env: c.env });
if (!installation) {
throw new RecaseError({
message: "Slack admin bot is not installed",
@@ -283,10 +300,11 @@ export const handleUpdateSlackAdminTarget = createRoute({
.where(eq(chatInstallations.id, installation.id))
.returning();
- await revokeSlackAdminOAuthArtifacts({
- db: tx,
- credentials: oauthCredentials,
- });
+ await revokeSlackAdminOAuthArtifacts({
+ db: tx,
+ env: c.env,
+ credentials: oauthCredentials,
+ });
return updatedInstallation;
});
@@ -311,7 +329,7 @@ export const handleDeleteSlackAdminInstall = createRoute({
scopes: [Scopes.Superuser],
handler: async (c) => {
const { db } = c.get("ctx");
- const installation = await getSlackAdminInstallation({ db });
+ const installation = await getSlackAdminInstallation({ db, env: c.env });
if (!installation) return c.json({ success: true });
const oauthCredentials = await getSlackAdminOAuthCredentials({
db,
@@ -319,16 +337,20 @@ export const handleDeleteSlackAdminInstall = createRoute({
});
await db.transaction(async (tx) => {
- await revokeSlackAdminOAuthArtifacts({
- db: tx,
- credentials: oauthCredentials,
- });
+ await revokeSlackAdminOAuthArtifacts({
+ db: tx,
+ env: c.env,
+ credentials: oauthCredentials,
+ });
await tx
.delete(chatInstallations)
.where(
and(
- eq(chatInstallations.id, installation.id),
- eq(chatInstallations.provider, getSlackAdminProvider()),
+ eq(chatInstallations.id, installation.id),
+ eq(
+ chatInstallations.provider,
+ getSlackAdminProvider({ env: c.env }),
+ ),
),
);
});
diff --git a/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts b/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts
index 771a7a488..49e7b3b0a 100644
--- a/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts
+++ b/server/src/internal/admin/handleUpsertSlackMcpOAuthClient.ts
@@ -1,13 +1,12 @@
-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 = () =>
- (runtimeEnv.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
+const getClientUrl = (env: Env) =>
+ (env.CLIENT_URL || "http://localhost:3000").replace(/\/+$/, "");
-const getSlackMcpRedirectUris = () => {
- const clientUrl = getClientUrl();
+const getSlackMcpRedirectUris = (env: Env) => {
+ const clientUrl = getClientUrl(env);
return [
`${clientUrl}/admin/oauth/slack-mcp/callback`,
`${clientUrl}/sandbox/admin/oauth/slack-mcp/callback`,
@@ -21,7 +20,7 @@ export const handleUpsertSlackMcpOAuthClient = createRoute({
const result = await registerMcpOAuthClient({
db,
clientName: "Slack MCP",
- redirectUris: getSlackMcpRedirectUris(),
+ redirectUris: getSlackMcpRedirectUris(c.env),
scope: undefined,
});
diff --git a/server/src/internal/api/events/EventService.ts b/server/src/internal/api/events/EventService.ts
index 0ba7b218e..e7aa2480f 100644
--- a/server/src/internal/api/events/EventService.ts
+++ b/server/src/internal/api/events/EventService.ts
@@ -1,4 +1,3 @@
-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";
@@ -10,12 +9,14 @@ export class EventService {
static async insert({
db,
event,
+ env,
}: {
db: DrizzleCli;
event: EventInsert | EventInsert[];
logger?: Logger;
+ env: Env;
}) {
- if (runtimeEnv.NODE_ENV !== "development") return;
+ if (env.NODE_ENV !== "development") return;
try {
const results = await db
.insert(events)
@@ -42,14 +43,16 @@ export class EventService {
internalCustomerId,
env,
limit = 10,
+ workerEnv,
}: {
db: DrizzleCli;
internalCustomerId: string;
orgId: string;
env: string;
limit?: number;
+ workerEnv: Env;
}) {
- if (runtimeEnv.NODE_ENV === "production") return [];
+ if (workerEnv.NODE_ENV === "production") return [];
const results = await db
.select({
id: events.id,
diff --git a/server/src/internal/auth/handleListAuthOrganizations.ts b/server/src/internal/auth/handleListAuthOrganizations.ts
index 9a8d5d227..c5269891a 100644
--- a/server/src/internal/auth/handleListAuthOrganizations.ts
+++ b/server/src/internal/auth/handleListAuthOrganizations.ts
@@ -2,11 +2,13 @@ import { ErrCode, member, organizations, RecaseError } from "@autumn/shared";
import { asc, eq } from "drizzle-orm";
import type { Context } from "hono";
import { db } from "@/db/initDrizzle.js";
-import { auth } from "@/utils/auth.js";
+import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
+import { createAuth } from "@/utils/auth.js";
const AUTH_ORGANIZATION_LIST_LIMIT = 1000;
-export const handleListAuthOrganizations = async (c: Context) => {
+export const handleListAuthOrganizations = async (c: Context) => {
+ const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
diff --git a/server/src/internal/auth/oauth/atmnOAuthClients.ts b/server/src/internal/auth/oauth/atmnOAuthClients.ts
index 56c0c3f44..0766219ef 100644
--- a/server/src/internal/auth/oauth/atmnOAuthClients.ts
+++ b/server/src/internal/auth/oauth/atmnOAuthClients.ts
@@ -1,12 +1,11 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { oauthClientRepo } from "../repos/index.js";
const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]);
-const configuredAtmnClientIds = () =>
+const configuredAtmnClientIds = (env: Env) =>
new Set(
- (runtimeEnv.ATMN_OAUTH_CLIENT_IDS ?? "")
+ (env.ATMN_OAUTH_CLIENT_IDS ?? "")
.split(",")
.map((id) => id.trim())
.filter(Boolean),
@@ -36,15 +35,17 @@ const metadataMarksAtmn = (metadata: unknown) => {
};
export const isAtmnOAuthClientRecord = ({
+ env,
clientId,
name,
metadata,
}: {
+ env: Env;
clientId: string | null | undefined;
name: string | null | undefined;
metadata?: unknown;
}) => {
- if (clientId && configuredAtmnClientIds().has(clientId)) return true;
+ if (clientId && configuredAtmnClientIds(env).has(clientId)) return true;
if (metadataMarksAtmn(metadata)) return true;
const normalizedName = name?.trim().toLowerCase();
@@ -53,12 +54,17 @@ export const isAtmnOAuthClientRecord = ({
export const isAtmnOAuthClientId = async ({
db,
+ env,
clientId,
}: {
db: DrizzleCli;
+ env: Env;
clientId: string;
}) => {
const client = await oauthClientRepo.getByClientId({ db, clientId });
- return isAtmnOAuthClientRecord(client ?? { clientId, name: null });
+ return isAtmnOAuthClientRecord({
+ env,
+ ...(client ?? { clientId, name: null }),
+ });
};
diff --git a/server/src/internal/auth/oauth/handleGetOAuthClient.ts b/server/src/internal/auth/oauth/handleGetOAuthClient.ts
index 57d21503a..0d5c5aa5f 100644
--- a/server/src/internal/auth/oauth/handleGetOAuthClient.ts
+++ b/server/src/internal/auth/oauth/handleGetOAuthClient.ts
@@ -1,5 +1,6 @@
import type { Context } from "hono";
import { db } from "@/db/initDrizzle.js";
+import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { oauthClientRepo } from "../repos/index.js";
import { isAtmnOAuthClientRecord } from "./atmnOAuthClients.js";
import {
@@ -7,7 +8,7 @@ import {
isInternalMcpOAuthClientRecord,
} from "./internalMcpOAuthClients.js";
-export const handleGetOAuthClient = async (c: Context) => {
+export const handleGetOAuthClient = async (c: Context) => {
const clientId = c.req.param("client_id");
const redirectUri = c.req.query("redirect_uri");
if (!clientId) {
@@ -20,7 +21,10 @@ export const handleGetOAuthClient = async (c: Context) => {
return c.json({ error: "Client not found" }, 404);
}
- const isInternalMcp = isInternalMcpOAuthClientRecord(client);
+ const isInternalMcp = isInternalMcpOAuthClientRecord({
+ env: c.env,
+ ...client,
+ });
const internalMcpName = isInternalMcp
? getInternalMcpDisplayName({
metadata: client.metadata,
@@ -31,7 +35,7 @@ export const handleGetOAuthClient = async (c: Context) => {
return c.json({
client_id: client.clientId,
name: internalMcpName || client.name || "Unknown Application",
- is_atmn: isAtmnOAuthClientRecord(client),
+ is_atmn: isAtmnOAuthClientRecord({ env: c.env, ...client }),
is_internal_mcp: isInternalMcp,
});
};
diff --git a/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts b/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts
index 5bff44cbb..80cde1ab4 100644
--- a/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts
+++ b/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts
@@ -1,7 +1,8 @@
import { AppEnv, RecaseError } from "@autumn/shared";
import type { Context } from "hono";
import { db } from "@/db/initDrizzle.js";
-import { auth } from "@/utils/auth.js";
+import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
+import { createAuth } from "@/utils/auth.js";
import { oauthConsentRepo } from "../repos/index.js";
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
import { getOAuthConsentScopeGrant } from "./oauthConsentScopes.js";
@@ -120,7 +121,8 @@ const jsonOAuthError = ({ error }: { error: RecaseError }) =>
},
);
-export const handleOAuthConsentWithEnv = async (c: Context) => {
+export const handleOAuthConsentWithEnv = async (c: Context) => {
+ const auth = createAuth(c.env);
const { contentType, fields } = await parseRequestFields(c.req.raw.clone());
const clientId = getClientIdFromFields(fields);
const redirectUri = getRedirectUriFromFields(fields);
@@ -165,7 +167,11 @@ export const handleOAuthConsentWithEnv = async (c: Context) => {
return response;
}
- if (!clientId || !env || (await isAtmnOAuthClientId({ db, clientId }))) {
+ if (
+ !clientId ||
+ !env ||
+ (await isAtmnOAuthClientId({ db, env: c.env, clientId }))
+ ) {
return response;
}
diff --git a/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts
index 2e4ea2068..d0cbb7130 100644
--- a/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts
+++ b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts
@@ -7,7 +7,8 @@ import {
import { ErrCode, RecaseError } from "@autumn/shared";
import type { Context } from "hono";
import { db } from "@/db/initDrizzle.js";
-import { auth } from "@/utils/auth.js";
+import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
+import { createAuth } from "@/utils/auth.js";
import { oauthAccessTokenRepo, oauthRefreshTokenRepo } from "../repos/index.js";
import { isMcpOAuthClient } from "./mcpOAuthScopes.js";
import {
@@ -108,7 +109,8 @@ const jsonTokenResponse = ({
headers: tokenResponseHeaders(response),
});
-export const handleOAuthTokenWithApiKey = async (c: Context) => {
+export const handleOAuthTokenWithApiKey = async (c: Context) => {
+ const auth = createAuth(c.env);
const resource = await getResourceFromOAuthTokenRequest(c.req.raw.clone());
const response = await auth.handler(c.req.raw);
if (!response.ok) return response;
@@ -132,6 +134,7 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => {
try {
const tokenRecord = await getOAuthAccessTokenRecord({
db,
+ env: c.env,
accessToken,
resource,
requestedScopes,
@@ -185,6 +188,7 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => {
}
apiKeyResult = await getExternalOAuthApiKeyForToken({
db,
+ env: c.env,
tokenRecord,
requestedScopes,
});
diff --git a/server/src/internal/auth/oauth/internalMcpOAuthClients.ts b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts
index d3ddfb083..d9a5a6462 100644
--- a/server/src/internal/auth/oauth/internalMcpOAuthClients.ts
+++ b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts
@@ -1,11 +1,10 @@
-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 type { HonoEnv } from "@/honoUtils/HonoEnv.js";
+import { createAuth } from "@/utils/auth.js";
import { oauthClientRepo } from "../repos/index.js";
-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();
@@ -43,15 +42,20 @@ const inferClientNameFromRedirectUri = (redirectUri: string) => {
};
export const isInternalMcpOAuthClientRecord = ({
+ env,
clientId,
name,
metadata,
}: {
+ env: Env;
clientId: string | null | undefined;
name: string | null | undefined;
metadata?: unknown;
}) => {
- if (INTERNAL_MCP_CLIENT_ID && clientId === INTERNAL_MCP_CLIENT_ID)
+ if (
+ env.INTERNAL_MCP_OAUTH_CLIENT_ID &&
+ clientId === env.INTERNAL_MCP_OAUTH_CLIENT_ID
+ )
return true;
if (name?.trim().toLowerCase() === INTERNAL_MCP_CLIENT_NAME_NORMALIZED) {
return true;
@@ -79,20 +83,29 @@ export const getInternalMcpDisplayName = ({
export const isInternalMcpOAuthClientId = async ({
db,
+ env,
clientId,
}: {
db: DrizzleCli;
+ env: Env;
clientId: string;
}) => {
const client = await oauthClientRepo.getByClientId({ db, clientId });
- return isInternalMcpOAuthClientRecord(client ?? { clientId, name: null });
+ return isInternalMcpOAuthClientRecord({
+ env,
+ ...(client ?? { clientId, name: null }),
+ });
};
-export const handleInternalMcpOAuthAuthorize = async (c: Context) => {
+export const handleInternalMcpOAuthAuthorize = async (c: Context) => {
+ const auth = createAuth(c.env);
const url = new URL(c.req.raw.url);
const clientId = url.searchParams.get("client_id");
- if (!clientId || !(await isInternalMcpOAuthClientId({ db, clientId }))) {
+ if (
+ !clientId ||
+ !(await isInternalMcpOAuthClientId({ db, env: c.env, clientId }))
+ ) {
return auth.handler(c.req.raw);
}
diff --git a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts
index bfe1fbee5..35bd8641a 100644
--- a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts
+++ b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { stripOAuthTokenPrefix } from "@autumn/auth";
import {
AppEnv,
@@ -19,21 +18,23 @@ import { oauthAccessTokenRepo, oauthConsentRepo } from "../repos/index.js";
import { isAtmnOAuthClientId } from "./atmnOAuthClients.js";
import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js";
-const getOAuthIssuer = () =>
- `${runtimeEnv.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
+const getOAuthIssuer = (env: Env) =>
+ `${env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`;
const verifyResourceAccessToken = async ({
+ env,
accessToken,
resource,
requestedScopes,
}: {
+ env: Env;
accessToken: string;
resource: string | null;
requestedScopes: ScopeString[] | null;
}) => {
if (!resource) return null;
- const issuer = getOAuthIssuer();
+ const issuer = getOAuthIssuer(env);
try {
const payload = await verifyAccessToken(accessToken, {
jwksUrl: `${issuer}/jwks`,
@@ -52,11 +53,13 @@ const verifyResourceAccessToken = async ({
export const getOAuthAccessTokenRecord = async ({
db,
+ env,
accessToken,
resource,
requestedScopes,
}: {
db: DrizzleCli;
+ env: Env;
accessToken: string;
resource: string | null;
requestedScopes: ScopeString[] | null;
@@ -67,6 +70,7 @@ export const getOAuthAccessTokenRecord = async ({
const tokenRecord =
(await oauthAccessTokenRepo.getValidByTokenValues({ db, tokenValues })) ??
(await verifyResourceAccessToken({
+ env,
accessToken: rawAccessToken,
resource,
requestedScopes,
@@ -120,10 +124,12 @@ export const getOAuthAccessTokenRecord = async ({
export const getExternalOAuthApiKeyForToken = async ({
db,
+ env,
tokenRecord,
requestedScopes,
}: {
db: DrizzleCli;
+ env: Env;
tokenRecord: ResourceAccessTokenRecord & {
userId: string;
referenceId: string;
@@ -132,6 +138,7 @@ export const getExternalOAuthApiKeyForToken = async ({
}) => {
const isAtmnClient = await isAtmnOAuthClientId({
db,
+ env,
clientId: tokenRecord.clientId,
});
if (isAtmnClient) return null;
@@ -151,19 +158,19 @@ export const getExternalOAuthApiKeyForToken = async ({
});
}
- const env = consent.env ?? AppEnv.Sandbox;
+ const appEnv = consent.env ?? AppEnv.Sandbox;
const scopes = requestedScopes ?? tokenRecord.scopes;
const apiKey = await rotateOAuthConsentApiKey({
db,
consent,
tokenRecord,
- env,
+ env: appEnv,
scopes,
});
return {
apiKey,
- env,
+ env: appEnv,
orgId: tokenRecord.referenceId,
userId: tokenRecord.userId,
clientId: tokenRecord.clientId,
diff --git a/server/src/internal/auth/oauth/oauthRouter.ts b/server/src/internal/auth/oauth/oauthRouter.ts
index 5f8e956bc..018a2383c 100644
--- a/server/src/internal/auth/oauth/oauthRouter.ts
+++ b/server/src/internal/auth/oauth/oauthRouter.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
oauthProviderAuthServerMetadata,
oauthProviderOpenIdConfigMetadata,
@@ -6,7 +5,7 @@ import {
import { type Context, Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
-import { auth } from "@/utils/auth.js";
+import { createAuth } from "@/utils/auth.js";
import { handleGetOAuthClient } from "./handleGetOAuthClient.js";
import { handleMcpOAuthRegistration } from "./handleMcpOAuthRegistration.js";
import { handleOAuthConsentWithEnv } from "./handleOAuthConsentWithEnv.js";
@@ -23,24 +22,28 @@ const getClientLookupRateLimitKey = (c: Context) =>
const oauthClientLookupLimiter = rateLimiter({
windowMs: 60 * 1000,
- limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 60,
+ limit: (c) => (c.env.NODE_ENV === "development" ? 1000 : 60),
standardHeaders: "draft-6",
keyGenerator: getClientLookupRateLimitKey,
});
oauthRouter.get("/api/auth/.well-known/openid-configuration", (c) => {
+ const auth = createAuth(c.env);
return oauthProviderOpenIdConfigMetadata(auth)(c.req.raw);
});
oauthRouter.get("/.well-known/oauth-authorization-server", (c) => {
+ const auth = createAuth(c.env);
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
});
oauthRouter.get("/api/auth/.well-known/oauth-authorization-server", (c) => {
+ const auth = createAuth(c.env);
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
});
oauthRouter.get("/.well-known/oauth-authorization-server/api/auth", (c) => {
+ const auth = createAuth(c.env);
return oauthProviderAuthServerMetadata(auth)(c.req.raw);
});
diff --git a/server/src/internal/balances/events/EventBatchingManager.ts b/server/src/internal/balances/events/EventBatchingManager.ts
index 7c5f79408..08f385c2b 100644
--- a/server/src/internal/balances/events/EventBatchingManager.ts
+++ b/server/src/internal/balances/events/EventBatchingManager.ts
@@ -1,5 +1,5 @@
import type { EventInsert } from "@autumn/shared";
-import { logger } from "@server/external/logtail/logtailUtils.js";
+import { createLogger } from "@server/external/logtail/logtailUtils.js";
import { sendEventsToTinybird } from "@server/external/tinybird/sendEvents/sendEvents.js";
import { JobName } from "@server/queue/JobName.js";
import { addTaskToQueue } from "@server/queue/queueUtils.js";
@@ -11,13 +11,13 @@ class BatchingManager {
private readonly maxBatchSize = 200; // Max events per batch (~200kb per event, keep batches under 10MB for Tinybird)
/** Add an event to the batch */
- addEvent(event: EventInsert): void {
+ addEvent(event: EventInsert, env: Env): void {
const key = event.id;
this.events.set(key, event);
// Auto-execute if batch size is reached
if (this.events.size >= this.maxBatchSize) {
- this.executeBatch();
+ this.executeBatch(env);
return;
}
@@ -27,12 +27,12 @@ class BatchingManager {
}
this.timer = setTimeout(() => {
- this.executeBatch();
+ this.executeBatch(env);
}, this.batchWindow);
}
/** Execute the current batch - queue to SQS for Postgres and send to Tinybird */
- private async executeBatch(): Promise {
+ private async executeBatch(env: Env): Promise {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
@@ -56,7 +56,7 @@ class BatchingManager {
await sendEventsToTinybird({
events: eventItems,
- logger,
+ logger: createLogger(env),
});
}
}
diff --git a/server/src/internal/balances/finalizeLock/expireLock.ts b/server/src/internal/balances/finalizeLock/expireLock.ts
index 431c20c42..1d3e6de7d 100644
--- a/server/src/internal/balances/finalizeLock/expireLock.ts
+++ b/server/src/internal/balances/finalizeLock/expireLock.ts
@@ -4,15 +4,18 @@ import type { ExpireLockReceiptPayload } from "@/queue/workflows.js";
import { runFinalizeLock } from "./runFinalizeLock.js";
export const expireLock = async ({
+ workerEnv,
ctx,
payload,
}: {
+ workerEnv: Env;
ctx: AutumnContext;
payload: ExpireLockReceiptPayload;
}) => {
try {
ctx.skipCache = false;
await runFinalizeLock({
+ workerEnv,
ctx,
params: {
lock_id: payload.lockId,
diff --git a/server/src/internal/balances/finalizeLock/runFinalizeLock.ts b/server/src/internal/balances/finalizeLock/runFinalizeLock.ts
index 067072f1b..29b74c1d1 100644
--- a/server/src/internal/balances/finalizeLock/runFinalizeLock.ts
+++ b/server/src/internal/balances/finalizeLock/runFinalizeLock.ts
@@ -13,6 +13,7 @@ import { runFinalizeLockV2 } from "./runFinalizeLockV2.js";
import { runRedisFinalizeLock } from "./runRedisFinalizeLock.js";
type RunFinalizeLockArgs = {
+ workerEnv: Env;
ctx: AutumnContext;
params: FinalizeLockParamsV0;
};
@@ -35,7 +36,11 @@ export const runFinalizeLock = async (args: RunFinalizeLockArgs) => {
});
};
-const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
+const runFinalizeLockInner = async ({
+ workerEnv,
+ ctx,
+ params,
+}: RunFinalizeLockArgs) => {
const fetchedReceipt = await fetchLockReceipt({
ctx,
lockId: params.lock_id,
@@ -43,6 +48,7 @@ const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
if (fetchedReceipt.source === "redis_v2") {
return runFinalizeLockV2({
+ workerEnv,
ctx,
params,
receipt: fetchedReceipt.receipt,
@@ -64,8 +70,9 @@ const runFinalizeLockInner = async ({ ctx, params }: RunFinalizeLockArgs) => {
try {
if (notNullish(receipt.expires_at)) {
await cancelLockExpiry({
+ workerEnv,
orgId: ctx.org.id,
- env: ctx.env,
+ appEnv: ctx.env,
hashedKey: Bun.hash(params.lock_id).toString(),
});
}
diff --git a/server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts b/server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts
index bb9028d21..97009df06 100644
--- a/server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts
+++ b/server/src/internal/balances/finalizeLock/runFinalizeLockV2.ts
@@ -20,6 +20,7 @@ import { runRedisFinalizeLockV2 } from "./runRedisFinalizeLockV2.js";
* marker key; `claimed === false` means another finalizer holds it.
*/
export const runFinalizeLockV2 = async ({
+ workerEnv,
ctx,
params,
receipt,
@@ -27,6 +28,7 @@ export const runFinalizeLockV2 = async ({
claimed,
lockRedisInstance,
}: {
+ workerEnv: Env;
ctx: AutumnContext;
params: FinalizeLockParamsV0;
receipt: LockReceipt;
@@ -55,8 +57,9 @@ export const runFinalizeLockV2 = async ({
try {
if (notNullish(receipt.expires_at)) {
await cancelLockExpiry({
+ workerEnv,
orgId: ctx.org.id,
- env: ctx.env,
+ appEnv: ctx.env,
hashedKey: Bun.hash(params.lock_id).toString(),
});
}
diff --git a/server/src/internal/balances/handlers/handleFinalizeLock.ts b/server/src/internal/balances/handlers/handleFinalizeLock.ts
index e59e64d17..f6046bcd8 100644
--- a/server/src/internal/balances/handlers/handleFinalizeLock.ts
+++ b/server/src/internal/balances/handlers/handleFinalizeLock.ts
@@ -9,7 +9,7 @@ export const handleFinalizeLock = createRoute({
const ctx = c.get("ctx");
const params = c.req.valid("json");
- const response = await runFinalizeLock({ ctx, params });
+ const response = await runFinalizeLock({ workerEnv: c.env, ctx, params });
const status = ctx.extraLogs.finalizeLockFailedOpen ? 202 : 200;
return c.json(response, status);
diff --git a/server/src/internal/balances/track/runAsyncTrack.ts b/server/src/internal/balances/track/runAsyncTrack.ts
index 888170a70..c711d0759 100644
--- a/server/src/internal/balances/track/runAsyncTrack.ts
+++ b/server/src/internal/balances/track/runAsyncTrack.ts
@@ -1,4 +1,3 @@
-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";
@@ -13,7 +12,7 @@ export const runAsyncTrack = async ({
ctx: AutumnContext;
body: TrackParams;
}): Promise => {
- const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
+ const queueUrl = ctx.env.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 81663a470..c59b77e6d 100644
--- a/server/src/internal/balances/track/runBatchTrack.ts
+++ b/server/src/internal/balances/track/runBatchTrack.ts
@@ -1,4 +1,3 @@
-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";
@@ -16,7 +15,7 @@ export const runBatchTrack = async ({
ctx: AutumnContext;
body: BatchTrackParams;
}): Promise => {
- const queueUrl = runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL;
+ const queueUrl = ctx.env.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 f397304d8..521996882 100644
--- a/server/src/internal/balances/track/utils/queueTrack.ts
+++ b/server/src/internal/balances/track/utils/queueTrack.ts
@@ -1,4 +1,3 @@
-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";
@@ -18,7 +17,7 @@ export const queueTrack = async ({
messageDeduplicationId?: string;
}) => {
try {
- const resolvedQueueUrl = queueUrl ?? runtimeEnv.TRACK_SQS_QUEUE_URL;
+ const resolvedQueueUrl = queueUrl ?? ctx.env.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/deduction/computeCreditCosts.ts b/server/src/internal/balances/utils/deduction/computeCreditCosts.ts
index 998f12a97..b72023cf2 100644
--- a/server/src/internal/balances/utils/deduction/computeCreditCosts.ts
+++ b/server/src/internal/balances/utils/deduction/computeCreditCosts.ts
@@ -1,5 +1,5 @@
import type { FullCusEntWithFullCusProduct } from "@autumn/shared";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { getCreditCost } from "@/internal/features/creditSystemUtils.js";
import type { FeatureDeduction } from "../types/featureDeduction.js";
@@ -11,12 +11,14 @@ export type CreditCostLookup = (entitlementId: string) => number;
export const computeCreditCosts = ({
cusEnts,
deduction,
+ env
}: {
cusEnts: FullCusEntWithFullCusProduct[];
deduction: FeatureDeduction;
+ env: Env
}): CreditCostLookup => {
const costMap = new Map();
-
+ const logger = createLogger(env)
for (const ce of cusEnts) {
// Token cost is USD: 1:1 on its own ent; parents apply their ratio to it.
if (
diff --git a/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts b/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts
index 88064edbe..41daf4887 100644
--- a/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts
+++ b/server/src/internal/balances/utils/deductionV2/executeRedisDeductionV2.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
type FullCusEntWithFullCusProduct,
type FullSubject,
@@ -52,6 +51,7 @@ export const executeRedisDeductionV2 = async ({
idempotencyKey,
deductionOptions = {},
redisInstance,
+ workerEnv
}: {
ctx: AutumnContext;
fullSubject: FullSubject;
@@ -60,6 +60,7 @@ export const executeRedisDeductionV2 = async ({
idempotencyKey?: string | null;
deductionOptions?: DeductionOptions;
redisInstance?: Redis;
+ workerEnv: Env
}): Promise<{
oldFullSubject: FullSubject;
fullSubject: FullSubject;
@@ -176,10 +177,10 @@ export const executeRedisDeductionV2 = async ({
const idempotencyRedisKey = idempotencyKey
? getRedisTrackFeatureIdempotencyKey({
- ctx,
- customerId,
- featureId: feature.id,
- }).redisKey
+ ctx,
+ customerId,
+ featureId: feature.id,
+ }).redisKey
: null;
const { keys, balanceKeyIndexByFeatureId } =
@@ -228,12 +229,12 @@ export const executeRedisDeductionV2 = async ({
idempotencyRedisKey !== null ? TRACK_V3_IDEMPOTENCY_TTL_MS : null,
lock: preparedLock
? {
- ...preparedLock,
- region: currentRegion,
- }
+ ...preparedLock,
+ region: currentRegion,
+ }
: null,
unwind_value: unwindValue ?? null,
- debug: runtimeEnv.NODE_ENV !== "production",
+ debug: workerEnv.NODE_ENV !== "production",
};
const targetRedis = redisInstance ?? ctx.redisV2;
diff --git a/server/src/internal/balances/utils/lock/cancelLockExpiry.ts b/server/src/internal/balances/utils/lock/cancelLockExpiry.ts
index 468f2d91d..eb60014ce 100644
--- a/server/src/internal/balances/utils/lock/cancelLockExpiry.ts
+++ b/server/src/internal/balances/utils/lock/cancelLockExpiry.ts
@@ -3,14 +3,20 @@ import { buildLockScheduleName } from "./buildLockScheduleName.js";
/** Cancels the EventBridge expiry schedule for a lock receipt. Safe to call even if no schedule exists. */
export const cancelLockExpiry = async ({
+ workerEnv,
orgId,
- env,
+ appEnv,
hashedKey,
}: {
+ workerEnv: Env;
orgId: string;
- env: string;
+ appEnv: string;
hashedKey: string;
}) => {
- const scheduleName = buildLockScheduleName({ orgId, env, hashedKey });
- await deleteSchedule({ scheduleName });
+ const scheduleName = buildLockScheduleName({
+ orgId,
+ env: appEnv,
+ hashedKey,
+ });
+ await deleteSchedule({ env: workerEnv, scheduleName });
};
diff --git a/server/src/internal/balances/utils/refreshEntityAggregate/RefreshEntityAggregateBatchingManager.ts b/server/src/internal/balances/utils/refreshEntityAggregate/RefreshEntityAggregateBatchingManager.ts
index 6fa4f597a..38383b137 100644
--- a/server/src/internal/balances/utils/refreshEntityAggregate/RefreshEntityAggregateBatchingManager.ts
+++ b/server/src/internal/balances/utils/refreshEntityAggregate/RefreshEntityAggregateBatchingManager.ts
@@ -1,5 +1,5 @@
import type { AppEnv } from "@autumn/shared";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { currentRegion } from "@/external/redis/initRedis.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
diff --git a/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts b/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts
index fc887d6e8..5f106da08 100644
--- a/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts
+++ b/server/src/internal/balances/utils/refreshEntityAggregate/queueRefreshEntityAggregate.ts
@@ -1,9 +1,8 @@
-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 =
- runtimeEnv.NODE_ENV === "development" ? 1000 : 5000;
+export const REFRESH_ENTITY_AGGREGATE_DEDUP_BUCKET_MS = (env: Env) =>
+ env.NODE_ENV === "development" ? 1000 : 5000;
/**
* Buffer added after the bucket boundary so the trailing enqueue fires *after*
@@ -21,15 +20,18 @@ export const buildRefreshEntityAggregateDedupId = ({
env,
customerId,
nowMs,
- bucketMs = REFRESH_ENTITY_AGGREGATE_DEDUP_BUCKET_MS,
+ workerEnv,
+ bucketMs,
}: {
orgId: string;
env: AppEnv;
customerId: string;
nowMs: number;
+ workerEnv: Env,
bucketMs?: number;
}): string => {
- const bucket = Math.floor(nowMs / bucketMs);
+ const _bucketMs = bucketMs ?? REFRESH_ENTITY_AGGREGATE_DEDUP_BUCKET_MS(workerEnv)
+ const bucket = Math.floor(nowMs / _bucketMs);
const key = JSON.stringify({
jobName: JobName.RefreshEntityAggregate,
orgId,
diff --git a/server/src/internal/balances/utils/sync/SyncBatchingManagerV2.ts b/server/src/internal/balances/utils/sync/SyncBatchingManagerV2.ts
index fd1385514..07362fb46 100644
--- a/server/src/internal/balances/utils/sync/SyncBatchingManagerV2.ts
+++ b/server/src/internal/balances/utils/sync/SyncBatchingManagerV2.ts
@@ -1,5 +1,5 @@
import type { AppEnv } from "@autumn/shared";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { currentRegion } from "@/external/redis/initRedis.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
diff --git a/server/src/internal/balances/utils/sync/SyncBatchingManagerV3.ts b/server/src/internal/balances/utils/sync/SyncBatchingManagerV3.ts
index 120bbcb12..b8ff4ae06 100644
--- a/server/src/internal/balances/utils/sync/SyncBatchingManagerV3.ts
+++ b/server/src/internal/balances/utils/sync/SyncBatchingManagerV3.ts
@@ -1,5 +1,5 @@
import type { AppEnv } from "@autumn/shared";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { currentRegion } from "@/external/redis/initRedis.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
diff --git a/server/src/internal/billing/attach/handleAttach.ts b/server/src/internal/billing/attach/handleAttach.ts
index eab671050..cde89cb33 100644
--- a/server/src/internal/billing/attach/handleAttach.ts
+++ b/server/src/internal/billing/attach/handleAttach.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
type AttachResponseV1,
AttachResponseV1Schema,
@@ -25,24 +24,22 @@ export const handleAttach = createRoute({
body: AttachBodyV0Schema,
resource: AffectedResource.Attach,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 60000,
- errorMessage:
- "Attach already in progress for this customer, try again in a few seconds",
+ lock: {
+ ttlMs: 60000,
+ errorMessage:
+ "Attach already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const attachBody = c.req.valid("json");
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: attachBody.customer_id,
- });
- },
- }
- : undefined,
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const attachBody = c.req.valid("json");
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: attachBody.customer_id,
+ });
+ },
+ },
handler: async (c) => {
// await handleAttachRaceCondition({ req, res });
diff --git a/server/src/internal/billing/v2/handlers/handleAttachV2.ts b/server/src/internal/billing/v2/handlers/handleAttachV2.ts
index 464c98e07..6425ab84e 100644
--- a/server/src/internal/billing/v2/handlers/handleAttachV2.ts
+++ b/server/src/internal/billing/v2/handlers/handleAttachV2.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
ApiVersion,
@@ -19,23 +18,21 @@ export const handleAttachV2 = createRoute({
[ApiVersion.V1_Beta]: AttachParamsV0Schema,
},
resource: AffectedResource.Attach,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Attach already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const body = c.req.valid("json");
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: body.customer_id,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Attach already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const body = c.req.valid("json");
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: body.customer_id,
+ });
+ },
+ },
handler: async (c) => {
const ctx = c.get("ctx");
const body = c.req.valid("json");
diff --git a/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts b/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts
index 517a929a3..c64bfd058 100644
--- a/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts
+++ b/server/src/internal/billing/v2/handlers/handleCreateSchedule.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
CreateScheduleParamsV0Schema,
type CreateScheduleResponse,
@@ -13,23 +12,21 @@ export const handleCreateSchedule = createRoute({
scopes: [Scopes.Billing.Write],
body: CreateScheduleParamsV0Schema,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Create schedule already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const body = c.req.valid("json");
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: body.customer_id,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Create schedule already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const body = c.req.valid("json");
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: body.customer_id,
+ });
+ },
+ },
handler: async (c) => {
const response = (await billingActions.createSchedule({
ctx: c.get("ctx"),
diff --git a/server/src/internal/billing/v2/handlers/handleMultiAttach.ts b/server/src/internal/billing/v2/handlers/handleMultiAttach.ts
index 9d0dde249..6693d9dcd 100644
--- a/server/src/internal/billing/v2/handlers/handleMultiAttach.ts
+++ b/server/src/internal/billing/v2/handlers/handleMultiAttach.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
InternalError,
@@ -14,23 +13,21 @@ export const handleMultiAttach = createRoute({
scopes: [Scopes.Billing.Write],
body: MultiAttachParamsV0Schema,
resource: AffectedResource.MultiAttach,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Multi-attach already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const body = c.req.valid("json");
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: body.customer_id,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Multi-attach already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const body = c.req.valid("json");
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: body.customer_id,
+ });
+ },
+ },
handler: async (c) => {
const ctx = c.get("ctx");
const body = c.req.valid("json");
diff --git a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts
index d4ce36394..605b0b0c2 100644
--- a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts
+++ b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
ApiVersion,
@@ -19,23 +18,21 @@ export const handleUpdateSubscription = createRoute({
[ApiVersion.V1_Beta]: UpdateSubscriptionV0ParamsSchema,
},
resource: AffectedResource.ApiSubscriptionUpdate,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Update subscription already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const attachBody = c.req.valid("json");
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: attachBody.customer_id,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Update subscription already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const attachBody = c.req.valid("json");
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: attachBody.customer_id,
+ });
+ },
+ },
handler: async (c) => {
const ctx = c.get("ctx");
const body = c.req.valid("json");
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 cc35448e6..a4a67ea66 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,14 +1,19 @@
-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";
import type { AutumnContext } from "@/honoUtils/HonoEnv";
import { isStripeConnected } from "@/internal/orgs/orgUtils";
-const expectedStripeApplicationId = ({ ctx }: { ctx: AutumnContext }) =>
+const expectedStripeApplicationId = ({
+ ctx,
+ env,
+}: {
+ ctx: AutumnContext;
+ env: Env;
+}) =>
ctx.env === AppEnv.Live
- ? runtimeEnv.STRIPE_LIVE_CLIENT_ID
- : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID;
+ ? env.STRIPE_LIVE_CLIENT_ID
+ : env.STRIPE_SANDBOX_CLIENT_ID;
const shouldValidateStripeApplicationOwnership = ({
ctx,
@@ -30,10 +35,12 @@ export const validateStripeSubscriptionActionOwnership = ({
ctx,
billingContext,
stripeSubscriptionAction,
+ env,
}: {
ctx: AutumnContext;
billingContext: BillingContext;
stripeSubscriptionAction?: StripeSubscriptionAction;
+ env: Env;
}) => {
if (!shouldValidateStripeApplicationOwnership({ ctx })) return;
@@ -49,7 +56,7 @@ export const validateStripeSubscriptionActionOwnership = ({
});
if (!applicationId) return;
- const expectedApplicationId = expectedStripeApplicationId({ ctx });
+ const expectedApplicationId = expectedStripeApplicationId({ ctx, env });
if (!expectedApplicationId) return;
if (applicationId === expectedApplicationId) {
diff --git a/server/src/internal/chat/ChatService.ts b/server/src/internal/chat/ChatService.ts
index e4c466253..7079f60ad 100644
--- a/server/src/internal/chat/ChatService.ts
+++ b/server/src/internal/chat/ChatService.ts
@@ -42,9 +42,9 @@ export class ChatService {
});
}
- static createInstallUrl(ctx: AutumnContext, env = AppEnv.Live) {
+ static createInstallUrl(ctx: AutumnContext, workerEnv: Env, env = AppEnv.Live) {
const state = createChatInstallState({
- secret: getChatStateSecret(),
+ secret: getChatStateSecret(workerEnv),
provider: slackProvider,
orgId: ctx.org.id,
userId: ctx.userId ?? "",
@@ -52,7 +52,7 @@ export class ChatService {
expiresAt: addMinutes(Date.now(), 10).getTime(),
nonce: randomUUID(),
});
- const url = createSlackInstallUrl(state);
+ const url = createSlackInstallUrl(workerEnv, state);
console.info("[chat] Created install URL", {
provider: slackProvider,
diff --git a/server/src/internal/chat/chatUtils.ts b/server/src/internal/chat/chatUtils.ts
index 6eda7693f..54023df67 100644
--- a/server/src/internal/chat/chatUtils.ts
+++ b/server/src/internal/chat/chatUtils.ts
@@ -1,14 +1,15 @@
import { ErrCode, RecaseError } from "@autumn/shared";
-import { runtimeEnv } from "@/utils/envUtils.js";
export const slackProvider = "slack" as const;
export const slackAdminProviderPrefix = "slack_admin" as const;
export const getSlackAdminProvider = ({
- clientId = getRequiredChatEnv("SLACK_CLIENT_ID"),
+ env,
+ clientId = getRequiredChatEnv(env, "SLACK_CLIENT_ID"),
}: {
+ env: Env;
clientId?: string;
-} = {}) => `${slackAdminProviderPrefix}:${clientId}` as const;
+}) => `${slackAdminProviderPrefix}:${clientId}` as const;
export const defaultSlackScopes = [
"app_mentions:read",
@@ -32,9 +33,10 @@ export const getMissingSlackScopes = (scopes: string[]) => {
};
export const getRequiredChatEnv = (
+ env: Env,
key: "SLACK_CLIENT_ID" | "ENCRYPTION_PASSWORD",
) => {
- const value = runtimeEnv[key];
+ const value = env[key];
if (value) return value;
throw new RecaseError({
@@ -44,21 +46,21 @@ export const getRequiredChatEnv = (
});
};
-export const getChatStateSecret = () =>
- runtimeEnv.CHAT_STATE_SECRET ??
- runtimeEnv.SLACK_STATE_SECRET ??
- runtimeEnv.BETTER_AUTH_SECRET ??
- getRequiredChatEnv("ENCRYPTION_PASSWORD");
+export const getChatStateSecret = (env: Env) =>
+ env.CHAT_STATE_SECRET ??
+ env.SLACK_STATE_SECRET ??
+ env.BETTER_AUTH_SECRET ??
+ getRequiredChatEnv(env, "ENCRYPTION_PASSWORD");
-export const createSlackInstallUrl = (state: string) => {
- const scope = runtimeEnv.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(",");
+export const createSlackInstallUrl = (env: Env, state: string) => {
+ const scope = env.SLACK_BOT_SCOPES ?? defaultSlackScopes.join(",");
const params = new URLSearchParams({
- client_id: getRequiredChatEnv("SLACK_CLIENT_ID"),
+ client_id: getRequiredChatEnv(env, "SLACK_CLIENT_ID"),
scope,
state,
});
- if (runtimeEnv.SLACK_REDIRECT_URI) {
- params.set("redirect_uri", runtimeEnv.SLACK_REDIRECT_URI);
+ if (env.SLACK_REDIRECT_URI) {
+ params.set("redirect_uri", env.SLACK_REDIRECT_URI);
}
return `https://slack.com/oauth/v2/authorize?${params}`;
};
diff --git a/server/src/internal/chat/handlers/handleCreateChatInstall.ts b/server/src/internal/chat/handlers/handleCreateChatInstall.ts
index 91ad3f862..5163c71d1 100644
--- a/server/src/internal/chat/handlers/handleCreateChatInstall.ts
+++ b/server/src/internal/chat/handlers/handleCreateChatInstall.ts
@@ -14,7 +14,7 @@ export const handleCreateChatInstall = createRoute({
body: installBody,
handler: async (c) => {
const { env } = c.req.valid("json");
- const url = ChatService.createInstallUrl(c.get("ctx"), env);
+ const url = ChatService.createInstallUrl(c.get("ctx"), c.env, env);
return c.json({ url });
},
diff --git a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts
index 8df4bff73..eaaba5397 100644
--- a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts
+++ b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AffectedResource,
type Checkout,
@@ -24,23 +23,21 @@ export const handleConfirmCheckout = createRoute({
scopes: [Scopes.Public],
resource: AffectedResource.Attach,
body: ConfirmCheckoutParamsSchema,
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Checkout confirmation already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- const checkout = c.get("checkout") as Checkout;
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: checkout.customer_id,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Checkout confirmation already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ const checkout = c.get("checkout") as Checkout;
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: checkout.customer_id,
+ });
+ },
+ },
handler: async (c) => {
const ctx = c.get("ctx");
const checkout = c.get("checkout") as Checkout;
diff --git a/server/src/internal/customers/cancel/handleCancelV2.ts b/server/src/internal/customers/cancel/handleCancelV2.ts
index 4a377a9b8..7213f6000 100644
--- a/server/src/internal/customers/cancel/handleCancelV2.ts
+++ b/server/src/internal/customers/cancel/handleCancelV2.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { UpdateSubscriptionV1Params } from "@autumn/shared";
import { Scopes } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler";
@@ -16,23 +15,21 @@ import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumn
export const handleCancelV2 = createRoute({
scopes: [Scopes.Billing.Write],
- lock:
- runtimeEnv.NODE_ENV !== "development"
- ? {
- ttlMs: 120000,
- errorMessage:
- "Cancel already in progress for this customer, try again in a few seconds",
- getKey: (c) => {
- const ctx = c.get("ctx");
- if (!ctx.customerId) return null;
- return buildBillingLockKey({
- orgId: ctx.org.id,
- env: ctx.env,
- customerId: ctx.customerId,
- });
- },
- }
- : undefined,
+ lock: {
+ ttlMs: 120000,
+ errorMessage:
+ "Cancel already in progress for this customer, try again in a few seconds",
+ getKey: (c) => {
+ if (c.env.NODE_ENV === "development") return null;
+ const ctx = c.get("ctx");
+ if (!ctx.customerId) return null;
+ return buildBillingLockKey({
+ orgId: ctx.org.id,
+ env: ctx.env,
+ customerId: ctx.customerId,
+ });
+ },
+ },
handler: async (c) => {
const ctx = c.get("ctx");
diff --git a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts
index 8532200cd..7aa422b42 100644
--- a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts
+++ b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/batchDeleteCachedFullCustomers.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { AppEnv } from "@autumn/shared";
import type { Redis } from "ioredis";
import {
@@ -19,7 +18,13 @@ type CustomerToDelete = {
customerId: string;
};
-const isProductionNode = runtimeEnv.NODE_ENV === "production";
+let _isProductionNode: boolean | undefined;
+const getIsProductionNode = (): boolean => {
+ if (_isProductionNode === undefined) {
+ _isProductionNode = process.env.NODE_ENV === "production";
+ }
+ return _isProductionNode;
+};
/**
* Per org: all keys share `{orgId}` so Redis Cluster stays in one slot per pipeline.
@@ -36,7 +41,7 @@ const deleteFullCustomerCacheRowsForOrg = async ({
let skipped = 0;
let customersToProcess = orgCustomers;
- if (!isProductionNode) {
+ if (!getIsProductionNode()) {
const existsPipeline = regionalRedis.pipeline();
for (const customer of orgCustomers) {
existsPipeline.exists(
diff --git a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts
index c846430a2..50771a2b6 100644
--- a/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts
+++ b/server/src/internal/customers/cusUtils/fullCustomerCacheUtils/testFullCustomerCacheGuard.ts
@@ -1,4 +1,3 @@
-import { logger } from "@/external/logtail/logtailUtils.js";
import { redis } from "@/external/redis/initRedis.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
@@ -37,7 +36,7 @@ export const setTestFullCustomerCacheGuard = async ({
await redis.set(key, "1", "PX", ttlMs);
return true;
} catch (error) {
- logger.error(`Failed to set test fullCustomer cache guard: ${error}`);
+ ctx.logger.error(`Failed to set test fullCustomer cache guard: ${error}`);
return false;
}
};
@@ -61,7 +60,7 @@ export const removeTestFullCustomerCacheGuard = async ({
await redis.del(key);
return true;
} catch (error) {
- logger.error(`Failed to remove test fullCustomer cache guard: ${error}`);
+ ctx.logger.error(`Failed to remove test fullCustomer cache guard: ${error}`);
return false;
}
};
diff --git a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts
index b6836a175..e5be11a54 100644
--- a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts
+++ b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts
@@ -76,6 +76,7 @@ export const handleCreateOAuthApiKeys = createRoute({
const tokenRecord = await getOAuthAccessTokenRecord({
db,
+ env: c.env,
accessToken,
resource,
requestedScopes,
@@ -109,6 +110,7 @@ export const handleCreateOAuthApiKeys = createRoute({
const externalApiKey = await getExternalOAuthApiKeyForToken({
db,
+ env: c.env,
tokenRecord,
requestedScopes: apiKeyScopes,
});
diff --git a/server/src/internal/dev/handlers/handleCreateSecretKey.ts b/server/src/internal/dev/handlers/handleCreateSecretKey.ts
index 7ee0e3af9..276f9e9b1 100644
--- a/server/src/internal/dev/handlers/handleCreateSecretKey.ts
+++ b/server/src/internal/dev/handlers/handleCreateSecretKey.ts
@@ -6,7 +6,7 @@ import {
Scopes,
} from "@autumn/shared";
import { z } from "zod/v4";
-import { auth } from "@/utils/auth";
+import { createAuth } from "@/utils/auth";
import { captureOrgEvent } from "@/utils/posthog.js";
import { createRoute } from "../../../honoMiddlewares/routeHandler";
import { ApiKeyPrefix, createKey } from "../api-keys/apiKeyUtils";
@@ -40,6 +40,7 @@ export const handleCreateSecretKey = createRoute({
}
// Get session to check for impersonation and author
+ const auth = createAuth(c.env);
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
diff --git a/server/src/internal/emails/constants.ts b/server/src/internal/emails/constants.ts
index fa6c25ffc..449d49081 100644
--- a/server/src/internal/emails/constants.ts
+++ b/server/src/internal/emails/constants.ts
@@ -1,3 +1,4 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-export const FROM_AUTUMN = `Autumn `;
-export const FROM_AYUSH = `Ayush `;
+export const getFromAddresses = (env: Env) => ({
+ FROM_AUTUMN: `Autumn `,
+ FROM_AYUSH: `Ayush `,
+});
diff --git a/server/src/internal/emails/sendInvitationEmail.ts b/server/src/internal/emails/sendInvitationEmail.ts
index 839bb3692..cc753b406 100644
--- a/server/src/internal/emails/sendInvitationEmail.ts
+++ b/server/src/internal/emails/sendInvitationEmail.ts
@@ -1,35 +1,49 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { safeResend } from "@/external/resend/safeResend.js";
-import { FROM_AUTUMN } from "./constants.js";
+import { getFromAddresses } from "./constants.js";
-const getInvitationEmailBody = ({ orgName }: { orgName: string }) => {
- return `Hey there! You've been invited to join ${orgName} on Autumn.
+const getInvitationEmailBody = ({
+ orgName,
+ env,
+}: {
+ orgName: string;
+ env: Env;
+}) => {
+ return `Hey there! You've been invited to join ${orgName} on Autumn.
Click the link below to create an account / sign in to Autumn and accept the invitation.
-${runtimeEnv.CLIENT_URL}/sign-in
+${env.CLIENT_URL}/sign-in
`;
};
-export const sendInvitationEmail = safeResend({
- fn: async ({
- email,
- orgName,
- inviteLink,
- }: {
- email: string;
- orgName: string;
- inviteLink?: string;
- }) => {
- logger.info(`Sending invitation email to ${email}`);
- await sendTextEmail({
- from: FROM_AUTUMN,
- to: email,
- subject: `Join ${orgName} on Autumn`,
- body: getInvitationEmailBody({ orgName }),
- });
- },
- action: "send org invitation email",
-});
+export const sendInvitationEmail = ({
+ email,
+ orgName,
+ inviteLink,
+ env,
+}: {
+ email: string;
+ orgName: string;
+ inviteLink?: string;
+ env: Env;
+}) => {
+ const logger = createLogger(env);
+ const { FROM_AUTUMN } = getFromAddresses(env);
+
+ logger.info(`Sending invitation email to ${email}`);
+ return safeResend({
+ env,
+ fn: async () => {
+ await sendTextEmail({
+ env,
+ from: FROM_AUTUMN,
+ to: email,
+ subject: `Join ${orgName} on Autumn`,
+ body: getInvitationEmailBody({ orgName, env }),
+ });
+ },
+ action: "send org invitation email",
+ })();
+};
diff --git a/server/src/internal/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts
index 4f698278f..910567d86 100644
--- a/server/src/internal/emails/sendOTPEmail.ts
+++ b/server/src/internal/emails/sendOTPEmail.ts
@@ -1,20 +1,20 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { createResendCli } from "@/external/resend/resendUtils.js";
-import { FROM_AUTUMN } from "./constants.js";
+import { getFromAddresses } from "./constants.js";
import OTPEmail from "./OTPEmail.js";
-const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
- if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
+const sendOTPEmail = async ({ email, otp, env }: { email: string; otp: string; env: Env }) => {
+ const logger = createLogger(env);
+ if (!env.RESEND_API_KEY || !env.RESEND_DOMAIN) {
logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
return;
}
try {
logger.info(`Sending OTP email to ${email}`);
- const resend = createResendCli();
+ const resend = createResendCli(env);
const { error } = await resend.emails.send({
- from: FROM_AUTUMN,
+ from: getFromAddresses(env).FROM_AUTUMN,
to: email,
subject: "Your verification code for Autumn",
react: OTPEmail({ otpCode: otp }),
diff --git a/server/src/internal/emails/sendOnboardingEmail.ts b/server/src/internal/emails/sendOnboardingEmail.ts
index 5a4c55892..538ecbae4 100644
--- a/server/src/internal/emails/sendOnboardingEmail.ts
+++ b/server/src/internal/emails/sendOnboardingEmail.ts
@@ -1,6 +1,6 @@
import { sendHtmlEmail } from "@/external/resend/resendUtils.js";
import { safeResend } from "@/external/resend/safeResend.js";
-import { FROM_AYUSH } from "./constants.js";
+import { getFromAddresses } from "./constants.js";
const getWelcomeEmailBody = (userFirstName: string) => {
return `
@@ -19,17 +19,28 @@ Co-founder, Autumn
`;
};
-export const sendOnboardingEmail = safeResend({
- fn: async ({ name, email }: { name: string; email: string }) => {
+export const sendOnboardingEmail = ({
+ env,
+ name,
+ email,
+}: {
+ env: Env;
+ name: string;
+ email: string;
+}) =>
+ safeResend({
+ env,
+ fn: async () => {
const firstName = name.split(" ")[0];
await sendHtmlEmail({
- from: FROM_AYUSH,
+ env,
+ from: getFromAddresses(env).FROM_AYUSH,
to: email,
subject: "Anything I can help with?",
body: getWelcomeEmailBody(firstName),
replyTo: "ayush@useautumn.com",
});
- },
- action: "send onboarding email",
-});
+ },
+ action: "send onboarding email",
+ })();
diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts
index a94473a50..9c3d9457f 100644
--- a/server/src/internal/invoices/InvoiceService.ts
+++ b/server/src/internal/invoices/InvoiceService.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
type ApiInvoiceV1,
type Customer,
@@ -26,10 +25,12 @@ export const processInvoice = ({
invoice,
withItems = false,
features,
+ env,
}: {
invoice: Invoice;
withItems?: boolean;
features?: Feature[];
+ env: Env;
}): ApiInvoiceV1 => {
const processorType = invoice.processor_type ?? ProcessorType.Stripe;
const isStripe = processorType === ProcessorType.Stripe;
@@ -44,7 +45,7 @@ export const processInvoice = ({
currency: invoice.currency,
created_at: invoice.created_at,
hosted_invoice_url: isStripe
- ? `${runtimeEnv.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`
+ ? `${env.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/v2/handlers/handleRunMigration.ts b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts
index 5a78e52cd..5fe15a781 100644
--- a/server/src/internal/migrations/v2/handlers/handleRunMigration.ts
+++ b/server/src/internal/migrations/v2/handlers/handleRunMigration.ts
@@ -1,4 +1,3 @@
-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";
@@ -73,7 +72,7 @@ export const handleRunMigration = createRoute({
});
}
- const isDev = runtimeEnv.NODE_ENV === "development";
+ const isDev = c.env.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 d7d0e3076..99fa86d70 100644
--- a/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts
+++ b/server/src/internal/migrations/v2/run/migrateCustomer/logs/logMigrateCustomerResult.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import chalk from "chalk";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { addExtrasToLogs } from "@/utils/logging/addContextToLogs.js";
@@ -23,9 +22,11 @@ const getMigrationCustomerExtras = ({
export const logMigrateCustomerResult = ({
ctx,
+ env,
result,
}: {
ctx: AutumnContext;
+ env: Env;
result: MigrateCustomerLogResult;
}) => {
const durationMs = Date.now() - ctx.timestamp;
@@ -63,7 +64,7 @@ export const logMigrateCustomerResult = ({
if (
Object.keys(ctx.extraLogs).length > 0 &&
- runtimeEnv.NODE_ENV === "development"
+ env.NODE_ENV === "development"
) {
const maskedLogs = maskExtraLogs(ctx.extraLogs);
ctx.logger.debug(`EXTRA LOGS: ${JSON.stringify(maskedLogs, null, 2)}`);
diff --git a/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts b/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts
index 7f9267cd2..f1f677243 100644
--- a/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts
+++ b/server/src/internal/migrations/v2/run/migrateCustomer/migrateCustomer.ts
@@ -26,12 +26,14 @@ export type MigrateCustomerResult = {
/** Top-level per-customer migration runner. Preview evaluates without writes. */
export const migrateCustomer = async ({
ctx,
+ env,
customerId,
migration,
preview = false,
hooks,
}: {
ctx: AutumnContext;
+ env: Env;
customerId: string;
migration: MigrationRuntime;
preview?: boolean;
diff --git a/server/src/internal/misc/cacheV2Ramp/cacheV2RampClient.ts b/server/src/internal/misc/cacheV2Ramp/cacheV2RampClient.ts
index 0bdc1dab7..54fcea592 100644
--- a/server/src/internal/misc/cacheV2Ramp/cacheV2RampClient.ts
+++ b/server/src/internal/misc/cacheV2Ramp/cacheV2RampClient.ts
@@ -1,5 +1,5 @@
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { getReachableDragonflyUrl } from "@/external/redis/getReachableDragonflyUrl.js";
import {
createRedisConnection,
@@ -24,10 +24,11 @@ let lastDecryptFailureKey: string | null = null;
* connection string (e.g. credential rotation on the same host), the next
* call disconnects the old client and creates a new one. Returns null when
* no destination is configured (ramp is dormant). */
-export const getRampDestinationRedis = (): Redis | null => {
+export const getRampDestinationRedis = (env: Env): Redis | null => {
+ const logger = createLogger(env);
const config = getCacheV2RampConfig();
if (!config) {
- closeRampDestinationClient();
+ closeRampDestinationClient(env);
return null;
}
@@ -95,7 +96,8 @@ export const getRampDestinationRedis = (): Redis | null => {
};
/** Tear down the cached destination client. Safe to call multiple times. */
-export const closeRampDestinationClient = () => {
+export const closeRampDestinationClient = (env: Env) => {
+ const logger = createLogger(env);
if (!cached) return;
try {
cached.instance.disconnect();
diff --git a/server/src/internal/misc/edgeConfig/edgeConfigStore.ts b/server/src/internal/misc/edgeConfig/edgeConfigStore.ts
index 4146f5889..7f4a3e83d 100644
--- a/server/src/internal/misc/edgeConfig/edgeConfigStore.ts
+++ b/server/src/internal/misc/edgeConfig/edgeConfigStore.ts
@@ -1,4 +1,3 @@
-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";
@@ -24,14 +23,16 @@ const nowIso = () => new Date().toISOString();
* Fail-open: any S3 error resets the in-memory config to `defaultValue()`.
*/
export const createEdgeConfigStore = ({
+ env,
s3Key,
schema,
defaultValue,
- pollIntervalMs = runtimeEnv.NODE_ENV === "development"
+ pollIntervalMs = env.NODE_ENV === "development"
? ms.seconds(1)
: ms.seconds(10),
s3Client: injectedS3Client,
}: {
+ env: Env;
s3Key: string;
schema: z.ZodType;
defaultValue: () => T;
@@ -47,7 +48,7 @@ export const createEdgeConfigStore = ({
let pollTimer: ReturnType | null = null;
const getConfigLocation = () => {
- const { bucket, region } = getAdminS3Config();
+ const { bucket, region } = getAdminS3Config(env);
return {
bucket,
region,
diff --git a/server/src/internal/misc/feedback/handleSubmitFeedback.ts b/server/src/internal/misc/feedback/handleSubmitFeedback.ts
index 36d5ace3c..709d3fcac 100644
--- a/server/src/internal/misc/feedback/handleSubmitFeedback.ts
+++ b/server/src/internal/misc/feedback/handleSubmitFeedback.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { z } from "zod/v4";
import { Scopes } from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
@@ -17,7 +16,7 @@ export const handleSubmitFeedback = createRoute({
const userEmail = ctx.user?.email ?? "Unknown user";
const orgSlug = ctx.org?.slug ?? "Unknown org";
- const webhookUrl = runtimeEnv.DISCORD_FEEDBACK_WEBHOOK;
+ const webhookUrl = c.env.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 f2cb593dd..a9221590b 100644
--- a/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts
+++ b/server/src/internal/misc/pricingAgent/pricingAgentRouter.ts
@@ -1,4 +1,3 @@
-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,13 +11,13 @@ import { OrganisationConfigurationSchema } from "./pricingAgentSchemas.js";
// PostHog client singleton
let phClient: PostHog | null = null;
-const getPostHogClient = (): PostHog | null => {
- if (!runtimeEnv.POSTHOG_API_KEY) {
+const getPostHogClient = (env: Env): PostHog | null => {
+ if (!env.POSTHOG_API_KEY) {
return null;
}
if (!phClient) {
- phClient = new PostHog(runtimeEnv.POSTHOG_API_KEY, {
- host: runtimeEnv.POSTHOG_HOST || "https://us.i.posthog.com",
+ phClient = new PostHog(env.POSTHOG_API_KEY, {
+ host: env.POSTHOG_HOST || "https://us.i.posthog.com",
});
}
return phClient;
@@ -123,7 +122,7 @@ pricingAgentRouter.post("/chat", async (c) => {
} = await c.req.json();
const ctx = c.var.ctx;
- if (!runtimeEnv.ANTHROPIC_API_KEY) {
+ if (!c.env.ANTHROPIC_API_KEY) {
throw new InternalError({
message: "ANTHROPIC_API_KEY not configured",
code: "anthropic_not_configured",
@@ -151,11 +150,11 @@ 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: runtimeEnv.ANTHROPIC_API_KEY,
+ apiKey: c.env.ANTHROPIC_API_KEY,
});
const baseModel = anthropicClient("claude-opus-4-5");
- const posthog = getPostHogClient();
+ const posthog = getPostHogClient(c.env);
const distinctId = ctx.userId || ctx.org?.id || "anonymous";
const model = posthog
diff --git a/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts b/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts
index d4e3865d1..ee684d415 100644
--- a/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts
+++ b/server/src/internal/misc/rateLimiter/rateLimitConfigs.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { ApiVersion, ApiVersionClass } from "@autumn/shared";
import type { Context } from "hono";
import { matchRoute } from "../../../honoMiddlewares/middlewareUtils";
@@ -225,7 +224,7 @@ export const resolveRateLimit = ({
export const RATE_LIMIT_CONFIGS: Record = {
[RateLimitType.General]: {
name: "general",
- limit: runtimeEnv.NODE_ENV === "development" ? 1000 : 25,
+ limit: 25,
windowMs: 1000,
notInRedis: false,
scope: RateLimitScope.Org,
diff --git a/server/src/internal/misc/rateLimiter/rateLimitFactory.ts b/server/src/internal/misc/rateLimiter/rateLimitFactory.ts
index 34feef182..7f6376158 100644
--- a/server/src/internal/misc/rateLimiter/rateLimitFactory.ts
+++ b/server/src/internal/misc/rateLimiter/rateLimitFactory.ts
@@ -1,7 +1,7 @@
import type { ApiVersion } from "@autumn/shared";
import type { Context, Next } from "hono";
import { rateLimiter } from "hono-rate-limiter";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { shouldUseRedis } from "@/external/redis/initRedis";
import type { HonoEnv } from "@/honoUtils/HonoEnv";
import {
@@ -29,7 +29,8 @@ export const setRateLimitKeyInContext = (c: Context, key: string): void => {
const RATE_LIMIT_WARNING_INTERVAL_MS = 30_000;
let lastRateLimitBypassWarningAt = 0;
-const warnRateLimitBypass = () => {
+const warnRateLimitBypass = (env: Env) => {
+ const logger = createLogger(env);
const now = Date.now();
if (now - lastRateLimitBypassWarningAt < RATE_LIMIT_WARNING_INTERVAL_MS)
return;
@@ -44,12 +45,15 @@ const CAP_EXCEEDED_WARNING_INTERVAL_MS = 10_000;
const lastCapWarnAtByType = new Map();
const warnOrgCapExceeded = ({
+ env,
limitType,
orgSlug,
}: {
+ env: Env;
limitType: string;
orgSlug?: string;
}) => {
+ const logger = createLogger(env);
const now = Date.now();
const lastWarnAt = lastCapWarnAtByType.get(limitType) ?? 0;
if (now - lastWarnAt < CAP_EXCEEDED_WARNING_INTERVAL_MS) return;
@@ -90,7 +94,7 @@ export const rateLimitFactory = ({
): Promise => {
const honoContext = c as Context;
const ctx = honoContext.get("ctx");
- warnOrgCapExceeded({ limitType: type, orgSlug: ctx?.org?.slug });
+ warnOrgCapExceeded({ env: honoContext.env, limitType: type, orgSlug: ctx?.org?.slug });
if (type === RateLimitType.CheckOrg && !isCheckFailOpenRoute(honoContext)) {
return c.json(
@@ -146,7 +150,7 @@ export const rateLimitFactory = ({
}
if (!shouldUseRedis()) {
- warnRateLimitBypass();
+ warnRateLimitBypass(c.env as Env);
return notInRedis ? getInMemoryLimiter()(c, next) : next();
}
diff --git a/server/src/internal/misc/trmnl/trmnlRouter.ts b/server/src/internal/misc/trmnl/trmnlRouter.ts
index 97360ad33..4e5e6fa93 100644
--- a/server/src/internal/misc/trmnl/trmnlRouter.ts
+++ b/server/src/internal/misc/trmnl/trmnlRouter.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { Hono } from "hono";
import { rateLimiter } from "hono-rate-limiter";
import { traceEnrichMiddleware } from "@/honoMiddlewares/traceMiddleware.js";
@@ -11,7 +10,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: runtimeEnv.NODE_ENV === "development" ? 1000 : 10,
+ limit: (c) => (c.env.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 a71efc9e3..5bfe8d351 100644
--- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
+++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
@@ -1,4 +1,3 @@
-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";
@@ -12,7 +11,7 @@ export const handleGetUploadUrl = createRoute({
const path = `logo/${org.id}`;
- if (!runtimeEnv.SUPABASE_URL || !runtimeEnv.SUPABASE_SERVICE_KEY) {
+ if (!c.env.SUPABASE_URL || !c.env.SUPABASE_SERVICE_KEY) {
throw new RecaseError({
message: "Supabase storage not set up",
code: ErrCode.SupabaseNotFound,
@@ -20,7 +19,7 @@ export const handleGetUploadUrl = createRoute({
});
}
- const data = await getUploadUrl({ path });
+ const data = await getUploadUrl({ env: c.env, path });
return c.json(data);
},
diff --git a/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts b/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts
index f994d3865..15c28db00 100644
--- a/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts
+++ b/server/src/internal/orgs/handlers/handleResetDefaultAccount.ts
@@ -1,4 +1,3 @@
-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";
@@ -13,7 +12,7 @@ export const handleResetDefaultAccount = createRoute({
const { db, org, logger, env } = ctx;
// Validation: Only allow for test org
- if (org.id !== runtimeEnv.TESTS_ORG_ID) {
+ if (org.id !== c.env.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 9b0ca1a62..6d4b45da9 100644
--- a/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts
+++ b/server/src/internal/orgs/handlers/revenueCatHandlers/handleGetRevenueCatOAuthUrl.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared";
import { z } from "zod/v4";
import {
@@ -21,8 +20,8 @@ export const handleGetRevenueCatOAuthUrl = createRoute({
const { org, env } = ctx;
if (
- !runtimeEnv.REVENUECAT_OAUTH_CLIENT_ID ||
- !runtimeEnv.REVENUECAT_OAUTH_CLIENT_SECRET
+ !c.env.REVENUECAT_OAUTH_CLIENT_ID ||
+ !c.env.REVENUECAT_OAUTH_CLIENT_SECRET
) {
throw new RecaseError({
message: "RevenueCat OAuth client credentials not configured",
@@ -31,7 +30,7 @@ export const handleGetRevenueCatOAuthUrl = createRoute({
});
}
- const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:5173";
+ const frontendUrl = c.env.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 0922548ae..57b989133 100644
--- a/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts
+++ b/server/src/internal/orgs/handlers/revenueCatHandlers/handleRevenueCatOAuthCallback.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AppEnv,
type Organization,
@@ -76,7 +75,7 @@ export const handleRevenueCatOAuthCallback = async (c: Context) => {
const { db } = initDrizzle();
- const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:3000";
+ const frontendUrl = c.env.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 a85a5f689..e33e5260d 100644
--- a/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts
+++ b/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import {
AppEnv,
type Organization,
@@ -19,10 +18,12 @@ const disconnectStripe = async ({
org,
env,
logger,
+ workerEnv,
}: {
org: Organization;
env: AppEnv;
logger: Logger;
+ workerEnv: Env;
}) => {
if (isStripeConnected({ org, env, throughSecretKey: true })) {
const stripeCli = createStripeCli({ org, env, throughSecretKey: true });
@@ -45,8 +46,8 @@ const disconnectStripe = async ({
await masterStripe.oauth.deauthorize({
client_id:
env === AppEnv.Live
- ? runtimeEnv.STRIPE_LIVE_CLIENT_ID || ""
- : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID || "",
+ ? workerEnv.STRIPE_LIVE_CLIENT_ID || ""
+ : workerEnv.STRIPE_SANDBOX_CLIENT_ID || "",
stripe_user_id: accountId,
});
} catch (error) {
@@ -99,7 +100,7 @@ export const handleDeleteStripe = createRoute({
});
try {
- await disconnectStripe({ org, env, logger });
+ await disconnectStripe({ org, env, logger, workerEnv: c.env });
} catch (error) {
logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, {
error,
diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts
index fc437f35f..3d6b5f2c2 100644
--- a/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts
+++ b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts
@@ -1,4 +1,3 @@
-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";
@@ -16,8 +15,8 @@ export const handleGetOAuthUrl = createRoute({
const clientId =
env === AppEnv.Live
- ? runtimeEnv.STRIPE_LIVE_CLIENT_ID
- : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID;
+ ? c.env.STRIPE_LIVE_CLIENT_ID
+ : c.env.STRIPE_SANDBOX_CLIENT_ID;
if (!clientId) {
throw new RecaseError({
@@ -28,7 +27,7 @@ export const handleGetOAuthUrl = createRoute({
}
// Generate OAuth state and store in Redis
- const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:5173";
+ const frontendUrl = c.env.CLIENT_URL || "http://localhost:5173";
const redirectUri = redirect_url || `${frontendUrl}/dev?tab=stripe`;
@@ -43,13 +42,13 @@ export const handleGetOAuthUrl = createRoute({
`https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write`,
);
- let serverUrl = runtimeEnv.BETTER_AUTH_URL;
+ let serverUrl = c.env.BETTER_AUTH_URL;
if (env === AppEnv.Live && serverUrl?.includes("localhost")) {
serverUrl = `https://express.dev.useautumn.com`;
}
- if (runtimeEnv.NGROK_URL) {
- serverUrl = runtimeEnv.NGROK_URL;
+ if (c.env.NGROK_URL) {
+ serverUrl = c.env.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 48700681c..2163dd2f1 100644
--- a/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts
+++ b/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv } from "@autumn/shared";
import type { Context } from "hono";
import { initDrizzle } from "@/db/initDrizzle.js";
@@ -20,7 +19,7 @@ export const handleOAuthCallback = async (c: Context) => {
const { db } = initDrizzle();
// Build frontend redirect URL (default)
- const frontendUrl = runtimeEnv.CLIENT_URL || "http://localhost:3000";
+ const frontendUrl = c.env.CLIENT_URL || "http://localhost:3000";
let redirectUrl = new URL(`${frontendUrl}`);
redirectUrl.searchParams.set("tab", "stripe");
diff --git a/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts
index 522b8e3db..4c4c9fef8 100644
--- a/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts
+++ b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts
@@ -1,6 +1,6 @@
import { AppEnv } from "@autumn/shared";
import Stripe from "stripe";
-import { logger } from "@/external/logtail/logtailUtils";
+import { createLogger } from "@/external/logtail/logtailUtils";
import {
checkKeyValid,
createWebhookEndpoint,
@@ -16,6 +16,8 @@ export const handleStripeSecretKey = async ({
secretKey: string;
env: AppEnv;
}) => {
+ const logger = createLogger(env as unknown as Env);
+
// 1. Check if key is valid
await checkKeyValid(secretKey);
const stripe = new Stripe(secretKey);
diff --git a/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts
index 3f1607465..eac08e116 100644
--- a/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts
+++ b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts
@@ -1,4 +1,3 @@
-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";
@@ -45,8 +44,8 @@ export const handleGetPlatformOAuth = createRoute({
// Get appropriate Stripe client ID based on environment
const clientId =
env === "live"
- ? runtimeEnv.STRIPE_LIVE_CLIENT_ID
- : runtimeEnv.STRIPE_SANDBOX_CLIENT_ID;
+ ? c.env.STRIPE_LIVE_CLIENT_ID
+ : c.env.STRIPE_SANDBOX_CLIENT_ID;
if (!clientId) {
throw new RecaseError({
@@ -64,7 +63,7 @@ export const handleGetPlatformOAuth = createRoute({
oauthUrl.searchParams.set("state", stateKey);
oauthUrl.searchParams.set(
"redirect_uri",
- `${runtimeEnv.BETTER_AUTH_URL || "https://express.dev.useautumn.com"}/stripe/oauth_callback`,
+ `${c.env.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/handlers/handleUpdateOrganizationStripe.ts b/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts
index ecc089172..e5aac2424 100644
--- a/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts
+++ b/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts
@@ -3,7 +3,7 @@ import { eq } from "drizzle-orm";
import { z } from "zod/v4";
import { initPlatformStripe } from "@/external/connect/initStripeCli.js";
import { registerConnectWebhook } from "@/external/connect/registerConnectWebhook.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
import { validatePlatformOrg } from "../utils/validatePlatformOrg.js";
@@ -24,18 +24,23 @@ const UpdateOrganizationStripeSchema = z
*/
const validateAndUpdateStripeAccount = async ({
accountId,
+ appEnv,
env,
masterOrg,
org,
}: {
accountId: string;
- env: AppEnv;
+ appEnv: AppEnv;
+ env: Env;
masterOrg: Organization;
org: Organization;
}) => {
+ const logger = createLogger(env);
+
const stripeCli = initPlatformStripe({
masterOrg,
env,
+ appEnv,
accountId,
});
@@ -44,7 +49,7 @@ const validateAndUpdateStripeAccount = async ({
// Update the organization's Stripe Connect configuration
const currentConnect =
- env === AppEnv.Sandbox ? org.test_stripe_connect : org.live_stripe_connect;
+ appEnv === AppEnv.Sandbox ? org.test_stripe_connect : org.live_stripe_connect;
return {
...currentConnect,
@@ -86,7 +91,8 @@ export const handleUpdateOrganizationStripe = createRoute({
if (test_account_id) {
updates.test_stripe_connect = await validateAndUpdateStripeAccount({
accountId: test_account_id,
- env: AppEnv.Sandbox,
+ appEnv: AppEnv.Sandbox,
+ env: c.env,
masterOrg,
org,
});
@@ -95,7 +101,8 @@ export const handleUpdateOrganizationStripe = createRoute({
if (live_account_id) {
updates.live_stripe_connect = await validateAndUpdateStripeAccount({
accountId: live_account_id,
- env: AppEnv.Live,
+ appEnv: AppEnv.Live,
+ env: c.env,
masterOrg,
org,
});
@@ -113,7 +120,7 @@ export const handleUpdateOrganizationStripe = createRoute({
`Updated Stripe Connect for platform org ${org.slug}: test=${test_account_id}, live=${live_account_id}`,
);
- await registerConnectWebhook({ ctx });
+ await registerConnectWebhook({ ctx, env: c.env });
return c.json({
message: "Stripe Connect configuration updated successfully",
diff --git a/server/src/internal/platform/platformBeta/platformBetaRouter.ts b/server/src/internal/platform/platformBeta/platformBetaRouter.ts
index 4df5eb2c1..f4474d41d 100644
--- a/server/src/internal/platform/platformBeta/platformBetaRouter.ts
+++ b/server/src/internal/platform/platformBeta/platformBetaRouter.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js";
@@ -19,7 +18,7 @@ platformBetaRouter.use("*", async (c, next) => {
const ctx = c.get("ctx");
const { org, logger } = ctx;
- if (!runtimeEnv.AUTUMN_SECRET_KEY) {
+ if (!c.env.AUTUMN_SECRET_KEY) {
return next();
}
diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts
index 1f639a072..8d4d5563a 100644
--- a/server/src/internal/products/productUtils/detectProductVariant.ts
+++ b/server/src/internal/products/productUtils/detectProductVariant.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { anthropic } from "@ai-sdk/anthropic";
import { BillingInterval, type FullProduct } from "@autumn/shared";
import { generateObject } from "ai";
@@ -28,14 +27,16 @@ To determine if a product is an interval variant, please follow these guidelines
export const detectBaseVariant = async ({
db,
curProduct,
+ env,
logger,
}: {
db: DrizzleCli;
curProduct: FullProduct;
+ env: Env;
logger: Logger;
}) => {
logger.info(`Detecting base variant for ${curProduct.id}`);
- if (!runtimeEnv.ANTHROPIC_API_KEY) return;
+ if (!env.ANTHROPIC_API_KEY) return;
const existingProducts = (await ProductService.listFull({
db,
diff --git a/server/src/node.ts b/server/src/node.ts
index bd7afd1d1..16d8f5819 100644
--- a/server/src/node.ts
+++ b/server/src/node.ts
@@ -1,17 +1,3 @@
-// Entry point: Load Infisical secrets, then start the Node/Bun app.
-import { setRuntimeEnvFromProcess } from "@/utils/envUtils.js";
-import cluster from "node:cluster";
+import { startNodeServer } from "./init.js";
-import { initInfisical } from "./external/infisical/initInfisical.js";
-
-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();
-}
-
-// Now dynamically import and run the main app.
-await import("./instrumentation.js");
-await import("./init.js");
+export { startNodeServer };
diff --git a/server/src/queue/blueGreen/blueGreenReadinessChecks.ts b/server/src/queue/blueGreen/blueGreenReadinessChecks.ts
index d7cfe8d7c..938e86251 100644
--- a/server/src/queue/blueGreen/blueGreenReadinessChecks.ts
+++ b/server/src/queue/blueGreen/blueGreenReadinessChecks.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { GetQueueAttributesCommand, SQSClient } from "@aws-sdk/client-sqs";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import {
@@ -41,19 +40,21 @@ const probe = async ({
}
};
-const getConfiguredQueueUrls = () =>
+const getConfiguredQueueUrls = (env: Env) =>
[
QUEUE_URL,
- runtimeEnv.TRACK_SQS_QUEUE_URL,
- runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL,
+ env.TRACK_SQS_QUEUE_URL,
+ env.TRACK_ASYNC_SQS_QUEUE_URL,
].filter((url): url is string => Boolean(url));
export const getBlueGreenQueueUrls = ({
+ env,
knownQueueUrls = [],
}: {
+ env: Env;
knownQueueUrls?: string[];
-} = {}) =>
- Array.from(new Set([...getConfiguredQueueUrls(), ...knownQueueUrls]));
+}) =>
+ Array.from(new Set([...getConfiguredQueueUrls(env), ...knownQueueUrls]));
// Build a per-queue-URL SQSClient using the region extracted from the URL,
// so SigV4 signs against the queue's region. Reusing one singleton across
diff --git a/server/src/queue/blueGreen/initBlueGreen.ts b/server/src/queue/blueGreen/initBlueGreen.ts
index 2d535edd8..31926c66b 100644
--- a/server/src/queue/blueGreen/initBlueGreen.ts
+++ b/server/src/queue/blueGreen/initBlueGreen.ts
@@ -27,12 +27,14 @@ import {
*/
export const initBlueGreen = async ({
db,
+ env,
logger,
}: {
db: DrizzleCli;
+ env: Env;
logger?: Logger;
}) => {
- const identity = await resolveAwsTaskIdentity();
+ const identity = await resolveAwsTaskIdentity(env);
await startBlueGreenSlotStorePolling({ serviceName: "workers", logger });
startBlueGreenHeartbeat({ db, logger, serviceName: "workers" });
diff --git a/server/src/queue/hatchetWorkflows/createWorkflowTask.ts b/server/src/queue/hatchetWorkflows/createWorkflowTask.ts
index 8a97789ec..be6c4b225 100644
--- a/server/src/queue/hatchetWorkflows/createWorkflowTask.ts
+++ b/server/src/queue/hatchetWorkflows/createWorkflowTask.ts
@@ -5,7 +5,6 @@ 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";
@@ -25,15 +24,17 @@ type BaseWorkflowInput = {
*/
export const createWorkflowTask = ({
handler,
+ bindings,
}: {
handler: (params: {
input: TInput;
autumnContext: AutumnContext;
}) => Promise;
+ bindings: Env;
}): ((input: TInput, hatchetCtx: Context) => Promise) => {
return async (input: TInput, hatchetCtx: Context) => {
const { orgId, env, customerId } = input;
- const logger = createLogger(runtimeEnv);
+ const logger = createLogger(bindings);
// 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 7168b54eb..380848b70 100644
--- a/server/src/queue/initSqs.ts
+++ b/server/src/queue/initSqs.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { SQSClient } from "@aws-sdk/client-sqs";
import {
DEFAULT_AWS_REGION,
@@ -23,8 +22,14 @@ export const extractLocalEndpoint = ({
}
};
-const getSqsClientConfig = ({ queueUrl }: { queueUrl?: string } = {}) => {
- const resolvedQueueUrl = queueUrl ?? runtimeEnv.SQS_QUEUE_URL_V2;
+const getSqsClientConfig = ({
+ env,
+ queueUrl,
+}: {
+ env: Env;
+ queueUrl?: string;
+}) => {
+ const resolvedQueueUrl = queueUrl ?? env.SQS_QUEUE_URL_V2;
const endpoint = extractLocalEndpoint({ queueUrl: resolvedQueueUrl });
const region =
extractRegionFromQueueUrl({ queueUrl: resolvedQueueUrl }) ||
@@ -34,14 +39,20 @@ const getSqsClientConfig = ({ queueUrl }: { queueUrl?: string } = {}) => {
region,
...(endpoint ? { endpoint } : {}),
credentials: {
- accessKeyId: runtimeEnv.AWS_ACCESS_KEY_ID || "",
- secretAccessKey: runtimeEnv.AWS_SECRET_ACCESS_KEY || "",
+ accessKeyId: env.AWS_ACCESS_KEY_ID || "",
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "",
},
};
};
-const getSqsClientCacheKey = ({ queueUrl }: { queueUrl?: string } = {}) => {
- const resolvedQueueUrl = queueUrl ?? runtimeEnv.SQS_QUEUE_URL_V2;
+const getSqsClientCacheKey = ({
+ env,
+ queueUrl,
+}: {
+ env: Env;
+ queueUrl?: string;
+}) => {
+ const resolvedQueueUrl = queueUrl ?? env.SQS_QUEUE_URL_V2;
const endpoint = extractLocalEndpoint({ queueUrl: resolvedQueueUrl });
const region =
extractRegionFromQueueUrl({ queueUrl: resolvedQueueUrl }) ||
@@ -52,45 +63,54 @@ const getSqsClientCacheKey = ({ queueUrl }: { queueUrl?: string } = {}) => {
const sqsClientsByCacheKey = new Map();
-let sqsClient = new SQSClient(getSqsClientConfig());
-sqsClientsByCacheKey.set(getSqsClientCacheKey(), sqsClient);
+/**
+ * Create and initialize the SQS subsystem. Returns a handle with the
+ * primary SQS client and queue URL. Callers must thread this through
+ * to any code that needs SQS access.
+ */
+export const initSqs = (env: Env) => {
+ const sqsClient = new SQSClient(getSqsClientConfig({ env }));
+ sqsClientsByCacheKey.set(getSqsClientCacheKey({ env }), sqsClient);
-export const sqs = sqsClient;
+ return { sqs: sqsClient, queueUrl: env.SQS_QUEUE_URL_V2 || "" };
+};
+
+export type SqsHandle = ReturnType;
/** Recreates the SQS client with fresh connections */
export const recreateSqsClient = ({
+ env,
queueUrl,
}: {
+ env: Env;
queueUrl?: string;
-} = {}): SQSClient => {
+}): SQSClient => {
console.log(`[SQS] Recreating SQS client (stale connection suspected)`);
- const cacheKey = getSqsClientCacheKey({ queueUrl });
+ const cacheKey = getSqsClientCacheKey({ env, queueUrl });
const existingClient = sqsClientsByCacheKey.get(cacheKey);
existingClient?.destroy();
- const nextClient = new SQSClient(getSqsClientConfig({ queueUrl }));
+ const nextClient = new SQSClient(getSqsClientConfig({ env, queueUrl }));
sqsClientsByCacheKey.set(cacheKey, nextClient);
- if (!queueUrl || queueUrl === runtimeEnv.SQS_QUEUE_URL_V2) {
- sqsClient = nextClient;
- }
-
return nextClient;
};
/** Get the current SQS client (use this instead of direct sqs export for refreshable access) */
export const getSqsClient = ({
+ env,
queueUrl,
}: {
+ env: Env;
queueUrl?: string;
-} = {}): SQSClient => {
- const cacheKey = getSqsClientCacheKey({ queueUrl });
+}): SQSClient => {
+ const cacheKey = getSqsClientCacheKey({ env, queueUrl });
const existingClient = sqsClientsByCacheKey.get(cacheKey);
if (existingClient) return existingClient;
- const nextClient = new SQSClient(getSqsClientConfig({ queueUrl }));
+ const nextClient = new SQSClient(getSqsClientConfig({ env, queueUrl }));
sqsClientsByCacheKey.set(cacheKey, nextClient);
return nextClient;
};
-export const QUEUE_URL = runtimeEnv.SQS_QUEUE_URL_V2 || "";
+export const getQueueUrl = (env: Env): string => env.SQS_QUEUE_URL_V2 || "";
diff --git a/server/src/queue/initWorkers.ts b/server/src/queue/initWorkers.ts
index 280a4d82d..79f256068 100644
--- a/server/src/queue/initWorkers.ts
+++ b/server/src/queue/initWorkers.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
await import("../sentry.js");
import { ms } from "@autumn/shared";
@@ -12,7 +11,7 @@ import {
import * as Sentry from "@sentry/bun";
import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js";
import { startPgPoolMonitor, stopPgPoolMonitor } from "@/db/pgPoolMonitor.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { verifyCacheConsistency } from "@/internal/billing/v2/workflows/verifyCacheConsistency/verifyCacheConsistency.js";
import {
isJobQueueEnabled,
@@ -27,7 +26,7 @@ import {
recordPollAttempt,
} from "./blueGreen/blueGreenHeartbeat.js";
import { initBlueGreen, shutdownBlueGreen } from "./blueGreen/initBlueGreen.js";
-import { getSqsClient, QUEUE_URL, recreateSqsClient } from "./initSqs.js";
+import { getQueueUrl, getSqsClient, recreateSqsClient } from "./initSqs.js";
import { JobName } from "./JobName.js";
import { processMessage, type SqsJob } from "./processMessage.js";
@@ -41,7 +40,6 @@ 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 = runtimeEnv.NODE_ENV !== "development";
// Per-message processing timeout — must be under VisibilityTimeout (30s)
const MESSAGE_TIMEOUT_MS = 25_000;
@@ -91,6 +89,7 @@ export const startPollingLoop = async ({
getSqsClientFn,
recreateSqsClientFn,
shouldPoll = () => true,
+ env,
}: {
db: DrizzleCli;
queueUrl: string;
@@ -98,7 +97,11 @@ export const startPollingLoop = async ({
getSqsClientFn: () => SQSClient;
recreateSqsClientFn: () => SQSClient;
shouldPoll?: () => boolean;
+ env: Env;
}) => {
+ const logger = createLogger(env);
+ const shouldIdleSelfKill = env.NODE_ENV !== "development";
+
// Per-loop state
let messagesProcessed = 0;
let totalMessagesProcessed = 0;
@@ -172,7 +175,7 @@ export const startPollingLoop = async ({
consecutiveZeroMessageIntervals >= IDLE_SELF_KILL_THRESHOLD &&
totalMessagesProcessed > 0 &&
activeMigrationJobs === 0 &&
- runtimeEnv.NODE_ENV !== "development"
+ env.NODE_ENV !== "development"
) {
console.log(
`${prefix} Idle self-kill: 0 messages for ${consecutiveZeroMessageIntervals} intervals after processing ${totalMessagesProcessed} total. Exiting for cluster respawn.`,
@@ -238,12 +241,12 @@ export const startPollingLoop = async ({
}
if (override) {
- await processMessage({ message, db });
+ await processMessage({ message, db, env });
} else {
await withTimeout({
timeoutMs: MESSAGE_TIMEOUT_MS,
timeoutMessage: `Processing timed out after ${MESSAGE_TIMEOUT_MS}ms`,
- fn: () => processMessage({ message, db }),
+ fn: () => processMessage({ message, db, env }),
});
}
@@ -419,16 +422,19 @@ export const startPollingLoop = async ({
export const initWorkers = async ({
startupStartedAt,
queueImplementation,
+ env,
}: {
startupStartedAt: number;
queueImplementation: string;
+ env: Env;
}) => {
- const { db } = initDrizzle({ name: "worker", maxConnections: 40 });
- startPgPoolMonitor();
+ const logger = createLogger(env);
+ const { db } = initDrizzle({ name: "worker", maxConnections: 40, databaseUrl: env.DATABASE_URL });
+ startPgPoolMonitor(env);
const { warmupRegionalRedis } = await import("@/external/redis/initRedis.js");
await warmupRegionalRedis();
- await initBlueGreen({ db, logger });
+ await initBlueGreen({ db, env, logger });
const shutdown = async () => {
console.log(`[SQS Worker ${process.pid}] Shutting down...`);
@@ -439,7 +445,7 @@ export const initWorkers = async ({
controller.abort();
}
- const isProd = runtimeEnv.NODE_ENV === "production";
+ const isProd = env.NODE_ENV === "production";
if (isProd) {
const shutdownTimeout = setTimeout(() => process.exit(0), 5000);
if (shutdownTimeout.unref) {
@@ -462,15 +468,15 @@ export const initWorkers = async ({
for (const { queueId, queueUrl } of [
{
queueId: JOB_QUEUE_IDS.primary,
- queueUrl: QUEUE_URL,
+ queueUrl: getQueueUrl(env),
},
{
queueId: JOB_QUEUE_IDS.track,
- queueUrl: runtimeEnv.TRACK_SQS_QUEUE_URL,
+ queueUrl: env.TRACK_SQS_QUEUE_URL,
},
{
queueId: JOB_QUEUE_IDS.trackAsync,
- queueUrl: runtimeEnv.TRACK_ASYNC_SQS_QUEUE_URL,
+ queueUrl: env.TRACK_ASYNC_SQS_QUEUE_URL,
},
]) {
if (!queueUrl) continue;
@@ -479,9 +485,10 @@ export const initWorkers = async ({
startPollingLoop({
db,
queueUrl,
+ env,
isFifo: queueUrl.endsWith(".fifo"),
- getSqsClientFn: () => getSqsClient({ queueUrl }),
- recreateSqsClientFn: () => recreateSqsClient({ queueUrl }),
+ getSqsClientFn: () => getSqsClient({ env, queueUrl }),
+ recreateSqsClientFn: () => recreateSqsClient({ env, queueUrl }),
shouldPoll: () =>
isJobQueueEnabled({ queue: queueId }) &&
isActiveSlot({ serviceName: "workers" }),
diff --git a/server/src/queue/processMessage.ts b/server/src/queue/processMessage.ts
index 001358a6f..a142ba72b 100644
--- a/server/src/queue/processMessage.ts
+++ b/server/src/queue/processMessage.ts
@@ -1,11 +1,10 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { Message } from "@aws-sdk/client-sqs";
import * as Sentry from "@sentry/bun";
import chalk from "chalk";
import type { Logger } from "pino";
import { isTransientDbError } from "@/db/dbUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { isTransientRedisError } from "@/external/redis/utils/isTransientRedisError.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask.js";
@@ -67,9 +66,11 @@ export const shouldRetrySqsJobError = ({
export const processMessage = async ({
message,
db,
+ env,
}: {
message: Message;
db: DrizzleCli;
+ env: Env;
}) => {
if (!message.Body) {
console.warn("Received message without body");
@@ -81,7 +82,7 @@ export const processMessage = async ({
const workflowId = message.MessageId ?? generateId("job");
const workerLogger = addWorkflowToLogs({
- logger: logger,
+ logger: createLogger(env),
workflowContext: {
id: workflowId,
name: job.name,
@@ -100,6 +101,7 @@ export const processMessage = async ({
await detectBaseVariant({
db,
curProduct: job.data.curProduct,
+ env,
logger: workerLogger as Logger,
});
return;
@@ -318,6 +320,7 @@ export const processMessage = async ({
return;
}
await expireLock({
+ workerEnv: env,
ctx,
payload: job.data,
});
@@ -372,7 +375,7 @@ export const processMessage = async ({
done: true,
});
- if (runtimeEnv.NODE_ENV === "development") {
+ if (env.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 438cef788..7c6a39756 100644
--- a/server/src/queue/queueUtils.ts
+++ b/server/src/queue/queueUtils.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type {
ApiVersion,
AppEnv,
@@ -119,6 +118,7 @@ export const addTaskToQueue = async ({
generateDeduplicationId,
delayMs,
queueUrl,
+ env,
}: {
jobName: T;
payload: Payloads[T];
@@ -127,11 +127,12 @@ export const addTaskToQueue = async ({
generateDeduplicationId?: boolean;
delayMs?: number;
queueUrl?: string;
+ env: Env;
}) => {
- const resolvedQueueUrl = queueUrl || runtimeEnv.SQS_QUEUE_URL_V2;
+ const resolvedQueueUrl = queueUrl || env.SQS_QUEUE_URL_V2;
if (resolvedQueueUrl) {
- const sqsClient = getSqsClient({ queueUrl: resolvedQueueUrl });
+ const sqsClient = getSqsClient({ env, queueUrl: resolvedQueueUrl });
// SQS implementation
const isFifoQueue = resolvedQueueUrl.endsWith(".fifo");
diff --git a/server/src/sentry.ts b/server/src/sentry.ts
index d8d7788c0..320cc2336 100644
--- a/server/src/sentry.ts
+++ b/server/src/sentry.ts
@@ -1,10 +1,11 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import * as Sentry from "@sentry/bun";
-if (runtimeEnv.SENTRY_DSN) {
+export const initSentry = (env: Env) => {
+ if (!env.SENTRY_DSN) return;
+
Sentry.init({
- dsn: runtimeEnv.SENTRY_DSN,
+ dsn: env.SENTRY_DSN,
sendDefaultPii: true,
skipOpenTelemetrySetup: true,
});
-}
+};
diff --git a/server/src/trigger/configureTrigger.ts b/server/src/trigger/configureTrigger.ts
index 1e6751fc5..ec18edaeb 100644
--- a/server/src/trigger/configureTrigger.ts
+++ b/server/src/trigger/configureTrigger.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { configure } from "@trigger.dev/sdk/v3";
/**
@@ -9,11 +8,10 @@ import { configure } from "@trigger.dev/sdk/v3";
* never collide in a shared shell — the SDK's default `TRIGGER_SECRET_KEY`
* is left to autumn-cloud.
*
- * Module side-effect: this `configure` call runs once on first import.
- * Anything that triggers tasks server-side imports from
- * `@/trigger/migrations/...`, which re-exports from this file's siblings,
- * so the configure happens before any `.trigger()` call.
+ * Call this before server-side code triggers tasks.
*/
-if (runtimeEnv.TRIGGER_SERVER_SECRET_KEY) {
- configure({ secretKey: runtimeEnv.TRIGGER_SERVER_SECRET_KEY });
-}
+export const configureTrigger = (env: Env) => {
+ if (env.TRIGGER_SERVER_SECRET_KEY) {
+ configure({ secretKey: env.TRIGGER_SERVER_SECRET_KEY });
+ }
+};
diff --git a/server/src/trigger/utils/createTriggerContext.ts b/server/src/trigger/utils/createTriggerContext.ts
index b33b6704c..dd5e15b91 100644
--- a/server/src/trigger/utils/createTriggerContext.ts
+++ b/server/src/trigger/utils/createTriggerContext.ts
@@ -7,7 +7,6 @@ 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";
/**
@@ -17,16 +16,18 @@ import { addTriggerToLogs } from "@/utils/logging/addContextToLogs.js";
export const createTriggerContext = async ({
orgId,
env,
+ bindings,
triggerCtx,
customerId,
}: {
orgId: string;
env: AppEnv;
+ bindings: Env;
triggerCtx: TriggerRunContext;
customerId?: string;
}): Promise<{ ctx: AutumnContext; logger: Logger }> => {
const logger = addTriggerToLogs({
- logger: createDualLogger(runtimeEnv),
+ logger: createDualLogger(bindings),
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 ee2c05a3c..c404a66bf 100644
--- a/server/src/utils/auth.ts
+++ b/server/src/utils/auth.ts
@@ -1,4 +1,3 @@
-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";
@@ -15,393 +14,405 @@ import {
import type { AccessControl } from "better-auth/plugins/access";
import { eq } from "drizzle-orm";
import { db } from "@/db/initDrizzle.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { createLoopsContact } from "@/external/resend/loopsUtils.js";
import { sendInvitationEmail } from "@/internal/emails/sendInvitationEmail.js";
import { sendOnboardingEmail } from "@/internal/emails/sendOnboardingEmail.js";
import sendOTPEmail from "@/internal/emails/sendOTPEmail.js";
import { afterOrgCreated } from "./authUtils/afterOrgCreated.js";
-import { afterSessionCreated } from "./authUtils/afterSessionCreated.js";
-import { afterSessionDeleted } from "./authUtils/afterSessionDeleted.js";
-import { beforeSessionCreated } from "./authUtils/beforeSessionCreated.js";
+import { createAfterSessionCreated } from "./authUtils/afterSessionCreated.js";
+import { createAfterSessionDeleted } from "./authUtils/afterSessionDeleted.js";
+import { createBeforeSessionCreated } from "./authUtils/beforeSessionCreated.js";
import { getScopesForUserInOrg } from "./authUtils/customSessionScopes.js";
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 (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 =
- typeof input === "string"
- ? input
- : input instanceof URL
- ? input.href
- : (input as Request).url;
- if (url.startsWith("https://oauth2.googleapis.com")) {
- return originalFetch(
- url.replace("https://oauth2.googleapis.com", `${emulate}/oauth2`),
- init,
- );
- }
- if (url.startsWith("https://www.googleapis.com/oauth2")) {
- return originalFetch(
- url.replace("https://www.googleapis.com", emulate),
- init,
- );
- }
- return originalFetch(input, init);
- }) as typeof fetch;
-}
+let hasConfiguredGoogleEmulator = false;
-const emulateGoogleUrl =
- 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 = runtimeEnv.BETTER_AUTH_URL?.startsWith("https://");
-const isProductionAuth = runtimeEnv.NODE_ENV === "production";
-
-const parseMcpResourceUrl = (rawUrl: string) => {
- const resourceUrl = rawUrl.trim();
- if (!resourceUrl) return null;
-
- try {
- return new URL(resourceUrl).href;
- } catch {
- console.warn(`Ignoring invalid MCP_RESOURCE_URLS entry: ${resourceUrl}`);
- return null;
+export const createAuth = (env: Env) => {
+ const logger = createLogger(env);
+ // 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 (
+ env.EMULATE_GOOGLE_URL &&
+ env.NODE_ENV !== "production" &&
+ !hasConfiguredGoogleEmulator
+ ) {
+ hasConfiguredGoogleEmulator = true;
+ const emulate = env.EMULATE_GOOGLE_URL.replace(/\/$/, "");
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = ((input: any, init?: any) => {
+ const url =
+ typeof input === "string"
+ ? input
+ : input instanceof URL
+ ? input.href
+ : (input as Request).url;
+ if (url.startsWith("https://oauth2.googleapis.com")) {
+ return originalFetch(
+ url.replace("https://oauth2.googleapis.com", `${emulate}/oauth2`),
+ init,
+ );
+ }
+ if (url.startsWith("https://www.googleapis.com/oauth2")) {
+ return originalFetch(
+ url.replace("https://www.googleapis.com", emulate),
+ init,
+ );
+ }
+ return originalFetch(input, init);
+ }) as typeof fetch;
}
-};
-// Public hosts that serve OAuth-protected MCP endpoints. leaf serves both the
-// MCP server (MCP_SERVER_URL) and the chat/slackbot (CHAT_SERVER_URL); the
-// autumn server can also proxy /mcp under its own origin (BETTER_AUTH_URL).
-// 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 =
- runtimeEnv.MCP_SERVER_URL ??
- (isProductionAuth ? "https://mcp.useautumn.com" : "http://localhost:3099");
-const chatServerUrl =
- runtimeEnv.CHAT_SERVER_URL ??
- (isProductionAuth ? "https://chat.useautumn.com" : "http://localhost:3099");
+ const emulateGoogleUrl =
+ env.NODE_ENV !== "production"
+ ? env.EMULATE_GOOGLE_URL?.replace(/\/$/, "")
+ : undefined;
-const mcpResourcePaths = ["/mcp"];
-const mcpResourceBases = [
- runtimeEnv.BETTER_AUTH_URL,
- mcpServerUrl,
- chatServerUrl,
-].filter((base): base is string => Boolean(base));
+ // 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 = env.BETTER_AUTH_URL?.startsWith("https://");
+ const isProductionAuth = env.NODE_ENV === "production";
-const mcpResourceUrls = [
- ...new Set([
- ...mcpResourceBases.flatMap((base) =>
- mcpResourcePaths.map((path) => new URL(path, base).href),
- ),
- ...(runtimeEnv.MCP_RESOURCE_URLS?.split(",")
- .map(parseMcpResourceUrl)
- .filter((url): url is string => Boolean(url)) ?? []),
- ]),
-];
+ const parseMcpResourceUrl = (rawUrl: string) => {
+ const resourceUrl = rawUrl.trim();
+ if (!resourceUrl) return null;
-/**
- * Passkey (WebAuthn) is bound to the FRONTEND origin where the browser calls
- * `navigator.credentials.{create,get}`. Derive rpID/origin from CLIENT_URL so
- * Portless worktrees (e.g. https://wt44.localhost) and production both work
- * without explicit env vars.
- *
- * - rpID: the hostname only (no scheme, no port). Browsers treat `*.localhost`
- * as a secure context, so passkeys work in dev over Portless TLS.
- * - origin: full URL with scheme. Multiple origins may be supplied for envs
- * that need to accept both Portless and direct localhost.
- */
-const passkeyFrontendUrl = runtimeEnv.CLIENT_URL ?? "http://localhost:3000";
-const passkeyOrigins: string[] = [passkeyFrontendUrl];
-const passkeyRpID = (() => {
- try {
- return new URL(passkeyFrontendUrl).hostname;
- } catch {
- return "localhost";
- }
-})();
-
-if (
- runtimeEnv.VITE_FRONTEND_URL &&
- runtimeEnv.VITE_FRONTEND_URL !== passkeyFrontendUrl
-) {
- try {
- const viteOrigin = new URL(runtimeEnv.VITE_FRONTEND_URL);
- if (viteOrigin.hostname === passkeyRpID) {
- passkeyOrigins.push(runtimeEnv.VITE_FRONTEND_URL);
+ try {
+ return new URL(resourceUrl).href;
+ } catch {
+ console.warn(`Ignoring invalid MCP_RESOURCE_URLS entry: ${resourceUrl}`);
+ return null;
}
- } catch {
- // Invalid URL, ignore
- }
-}
+ };
-const options = {
- baseURL: runtimeEnv.BETTER_AUTH_URL,
- telemetry: {
- enabled: false,
- },
- ...(isHttpsBaseUrl && {
- advanced: {
- useSecureCookies: true,
- defaultCookieAttributes: {
- sameSite: "none" as const,
- secure: true,
- },
+ // Public hosts that serve OAuth-protected MCP endpoints. leaf serves both the
+ // MCP server (MCP_SERVER_URL) and the chat/slackbot (CHAT_SERVER_URL); the
+ // autumn server can also proxy /mcp under its own origin (BETTER_AUTH_URL).
+ // 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 =
+ env.MCP_SERVER_URL ??
+ (isProductionAuth ? "https://mcp.useautumn.com" : "http://localhost:3099");
+ const chatServerUrl =
+ env.CHAT_SERVER_URL ??
+ (isProductionAuth ? "https://chat.useautumn.com" : "http://localhost:3099");
+
+ const mcpResourcePaths = ["/mcp"];
+ const mcpResourceBases = [
+ env.BETTER_AUTH_URL,
+ mcpServerUrl,
+ chatServerUrl,
+ ].filter((base): base is string => Boolean(base));
+
+ const mcpResourceUrls = [
+ ...new Set([
+ ...mcpResourceBases.flatMap((base) =>
+ mcpResourcePaths.map((path) => new URL(path, base).href),
+ ),
+ ...(env.MCP_RESOURCE_URLS?.split(",")
+ .map(parseMcpResourceUrl)
+ .filter((url): url is string => Boolean(url)) ?? []),
+ ]),
+ ];
+
+ /**
+ * Passkey (WebAuthn) is bound to the FRONTEND origin where the browser calls
+ * `navigator.credentials.{create,get}`. Derive rpID/origin from CLIENT_URL so
+ * Portless worktrees (e.g. https://wt44.localhost) and production both work
+ * without explicit env vars.
+ *
+ * - rpID: the hostname only (no scheme, no port). Browsers treat `*.localhost`
+ * as a secure context, so passkeys work in dev over Portless TLS.
+ * - origin: full URL with scheme. Multiple origins may be supplied for envs
+ * that need to accept both Portless and direct localhost.
+ */
+ const passkeyFrontendUrl = env.CLIENT_URL ?? "http://localhost:3000";
+ const passkeyOrigins: string[] = [passkeyFrontendUrl];
+ const passkeyRpID = (() => {
+ try {
+ return new URL(passkeyFrontendUrl).hostname;
+ } catch {
+ return "localhost";
+ }
+ })();
+
+ if (
+ env.VITE_FRONTEND_URL &&
+ env.VITE_FRONTEND_URL !== passkeyFrontendUrl
+ ) {
+ try {
+ const viteOrigin = new URL(env.VITE_FRONTEND_URL);
+ if (viteOrigin.hostname === passkeyRpID) {
+ passkeyOrigins.push(env.VITE_FRONTEND_URL);
+ }
+ } catch {
+ // Invalid URL, ignore
+ }
+ }
+
+ const options = {
+ baseURL: env.BETTER_AUTH_URL,
+ telemetry: {
+ enabled: false,
},
- }),
-
- database: drizzleAdapter(db, {
- provider: "pg",
- schema: schemas,
- }),
-
- user: {
- deleteUser: {
- enabled: true,
- sendDeleteAccountVerification: async ({
- user,
- url,
- token,
- }: {
- user: User;
- url: string;
- token: string;
- }) => {
- console.log("Delete account verification", { user, url, token });
+ ...(isHttpsBaseUrl && {
+ advanced: {
+ useSecureCookies: true,
+ defaultCookieAttributes: {
+ sameSite: "none" as const,
+ secure: true,
+ },
},
- },
- },
- databaseHooks: {
+ }),
+
+ database: drizzleAdapter(db, {
+ provider: "pg",
+ schema: schemas,
+ }),
+
user: {
- create: {
- after: async (user) => {
- await createLoopsContact(user);
- await sendOnboardingEmail({
- name: user.name,
- email: user.email,
- });
+ deleteUser: {
+ enabled: true,
+ sendDeleteAccountVerification: async ({
+ user,
+ url,
+ token,
+ }: {
+ user: User;
+ url: string;
+ token: string;
+ }) => {
+ console.log("Delete account verification", { user, url, token });
},
},
},
- session: {
- create: {
- before: beforeSessionCreated,
- after: afterSessionCreated,
+ databaseHooks: {
+ user: {
+ create: {
+ after: async (user) => {
+ await createLoopsContact(user);
+ await sendOnboardingEmail({
+ name: user.name,
+ email: user.email,
+ });
+ },
+ },
},
- delete: {
- after: afterSessionDeleted,
+ session: {
+ create: {
+ before: createBeforeSessionCreated(env),
+ after: createAfterSessionCreated(env),
+ },
+ delete: {
+ after: createAfterSessionDeleted(env),
+ },
},
},
- },
- trustedOrigins: (request?: Request): string[] => {
- const origins: string[] = [
- "http://localhost:3000",
- "https://app.useautumn.com",
- "https://staging.useautumn.com",
- "https://*.useautumn.com",
- ];
- if (runtimeEnv.NODE_ENV === "production") return origins;
+ trustedOrigins: (request?: Request): string[] => {
+ const origins: string[] = [
+ "http://localhost:3000",
+ "https://app.useautumn.com",
+ "https://staging.useautumn.com",
+ "https://*.useautumn.com",
+ ];
+ if (env.NODE_ENV === "production") return origins;
- // Worktree ports follow worktreeOffset = (N-1)*100; accept any localhost
- // port the running stack might use as origin.
- const origin = request?.headers.get("origin") ?? null;
- if (
- origin &&
- /^https?:\/\/(?:[a-zA-Z0-9-]+\.)*localhost(?::\d+)?$/.test(origin)
- ) {
- origins.push(origin);
- }
- if (runtimeEnv.CLIENT_URL) origins.push(runtimeEnv.CLIENT_URL);
- if (runtimeEnv.BETTER_AUTH_URL) origins.push(runtimeEnv.BETTER_AUTH_URL);
- return origins;
- },
- emailAndPassword: {
- enabled: true,
- disableSignUp: false,
- requireEmailVerification: true,
- minPasswordLength: 8,
- maxPasswordLength: 128,
- autoSignIn: true,
- resetPasswordTokenExpiresIn: 3600, // 1 hour
- },
+ // Worktree ports follow worktreeOffset = (N-1)*100; accept any localhost
+ // port the running stack might use as origin.
+ const origin = request?.headers.get("origin") ?? null;
+ if (
+ origin &&
+ /^https?:\/\/(?:[a-zA-Z0-9-]+\.)*localhost(?::\d+)?$/.test(origin)
+ ) {
+ origins.push(origin);
+ }
+ if (env.CLIENT_URL) origins.push(env.CLIENT_URL);
+ if (env.BETTER_AUTH_URL) origins.push(env.BETTER_AUTH_URL);
+ return origins;
+ },
+ emailAndPassword: {
+ enabled: true,
+ disableSignUp: false,
+ requireEmailVerification: true,
+ minPasswordLength: 8,
+ maxPasswordLength: 128,
+ autoSignIn: true,
+ resetPasswordTokenExpiresIn: 3600, // 1 hour
+ },
- socialProviders: {
- google: {
- clientId: runtimeEnv.GOOGLE_CLIENT_ID!,
- clientSecret: runtimeEnv.GOOGLE_CLIENT_SECRET,
- redirectURI: `${runtimeEnv.BETTER_AUTH_URL}/api/auth/callback/google`,
- ...(emulateGoogleUrl
- ? {
+ socialProviders: {
+ google: {
+ clientId: env.GOOGLE_CLIENT_ID!,
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
+ redirectURI: `${env.BETTER_AUTH_URL}/api/auth/callback/google`,
+ ...(emulateGoogleUrl
+ ? {
// HS256-signed id_tokens from emulate fail real Google's RS256 JWKS check.
authorizationEndpoint: `${emulateGoogleUrl}/o/oauth2/v2/auth`,
verifyIdToken: async () => true,
}
- : {}),
+ : {}),
+ },
},
- },
- plugins: [
- emailOTP({
- async sendVerificationOTP({ email, otp, type }) {
- // Implement the sendVerificationOTP method to send the OTP to the user's email address
+ plugins: [
+ emailOTP({
+ async sendVerificationOTP({ email, otp, type }) {
+ // Implement the sendVerificationOTP method to send the OTP to the user's email address
- await sendOTPEmail({
- email,
- otp,
- });
- },
- }),
- admin({
- adminUserIds: ADMIN_USER_IDs,
- impersonationSessionDuration: 1000 * 60 * 60 * 24, // 1 days
- }),
-
- jwt(),
- oauthProvider({
- 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: [runtimeEnv.BETTER_AUTH_URL, ...mcpResourceUrls].filter(
- Boolean,
- ) as string[],
- allowDynamicClientRegistration: true,
- allowUnauthenticatedClientRegistration: true,
- customAccessTokenClaims: ({ referenceId }) => ({
- reference_id: referenceId,
+ await sendOTPEmail({
+ email,
+ otp,
+ });
+ },
}),
- clientReference: ({ session }) => {
- return (
- (session?.activeOrganizationId as string | undefined) ?? undefined
- );
- },
- // Use the active organization as the consent reference
- // This makes consent org-scoped, not just user-scoped
- postLogin: {
- // Required: page to redirect to if shouldRedirect returns true
- 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)
- consentReferenceId: ({ session }) => {
+ admin({
+ adminUserIds: ADMIN_USER_IDs,
+ impersonationSessionDuration: 1000 * 60 * 60 * 24, // 1 days
+ }),
+
+ jwt(),
+ oauthProvider({
+ loginPage: `${env.CLIENT_URL}/sign-in`,
+ consentPage: `${env.CLIENT_URL}/consent`,
+ // Resource-based scopes with R/W actions (plus legacy CRUDL +
+ // meta scopes — see shared/utils/scopeDefinitions.ts).
+ scopes: [...ALL_SCOPES],
+ validAudiences: [env.BETTER_AUTH_URL, ...mcpResourceUrls].filter(
+ Boolean,
+ ) as string[],
+ allowDynamicClientRegistration: true,
+ allowUnauthenticatedClientRegistration: true,
+ customAccessTokenClaims: ({ referenceId }) => ({
+ reference_id: referenceId,
+ }),
+ clientReference: ({ session }) => {
return (
(session?.activeOrganizationId as string | undefined) ?? undefined
);
},
- },
- }),
-
- passkey({
- rpID: passkeyRpID,
- rpName: "Autumn",
- origin: passkeyOrigins.length === 1 ? passkeyOrigins[0]! : passkeyOrigins,
- }),
-
- organization({
- ac: ac as AccessControl,
- roles,
- creatorRole: "owner",
- async sendInvitationEmail(data: {
- id: string;
- email: string;
- organization: Organization;
- }) {
- const inviteLink = `${runtimeEnv.CLIENT_URL}/accept?id=${data.id}`;
- await sendInvitationEmail({
- email: data.email,
- orgName: (data.organization.name as string) ?? "an organization",
- inviteLink: inviteLink,
- });
-
- try {
- // Update invite to expire in 7 days
- await db
- .update(invitation)
- .set({
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
- })
- .where(eq(invitation.id, data.id));
- } catch (error) {
- logger.error("Error updating invite expiration date:", { error });
- }
- },
- schema: {
- organization: {
- modelName: "organizations",
- fields: {
- createdAt: "createdAt",
+ // Use the active organization as the consent reference
+ // This makes consent org-scoped, not just user-scoped
+ postLogin: {
+ // Required: page to redirect to if shouldRedirect returns true
+ page: `${env.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)
+ consentReferenceId: ({ session }) => {
+ return (
+ (session?.activeOrganizationId as string | undefined) ?? undefined
+ );
},
},
- },
- organizationHooks: {
- afterCreateOrganization: async ({
- organization,
- user,
- }: {
+ }),
+
+ passkey({
+ rpID: passkeyRpID,
+ rpName: "Autumn",
+ origin: passkeyOrigins.length === 1 ? passkeyOrigins[0]! : passkeyOrigins,
+ }),
+
+ organization({
+ ac: ac as AccessControl,
+ roles,
+ creatorRole: "owner",
+ async sendInvitationEmail(data: {
+ id: string;
+ email: string;
organization: Organization;
- user: User;
- }) => {
- await afterOrgCreated({ org: organization, user });
+ }) {
+ const inviteLink = `${env.CLIENT_URL}/accept?id=${data.id}`;
+ await sendInvitationEmail({
+ email: data.email,
+ orgName: (data.organization.name as string) ?? "an organization",
+ inviteLink: inviteLink,
+ });
+
+ try {
+ // Update invite to expire in 7 days
+ await db
+ .update(invitation)
+ .set({
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ })
+ .where(eq(invitation.id, data.id));
+ } catch (error) {
+ logger.error("Error updating invite expiration date:", { error });
+ }
},
- },
- }),
- ],
-} satisfies BetterAuthOptions;
-
-export const auth = betterAuth({
- ...options,
- plugins: [
- ...options.plugins,
- /**
- * Attach `role` and `scopes` to every session response.
- *
- * Must be last in the plugin list so it can observe the session
- * produced by the other plugins (notably `organization`, which
- * populates `session.activeOrganizationId`).
- *
- * better-auth docs note that custom session fields are NOT cached
- * (neither in secondary storage nor the cookie cache), so every
- * `getSession` call pays a DB round-trip. Accepted.
- */
- customSession(async ({ user, session }) => {
- let role: string | null = null;
- let scopes: string[] = [];
- const orgId = session.activeOrganizationId;
- if (orgId && user?.id) {
- const resolved = await getScopesForUserInOrg({
- db,
- userId: user.id,
- organizationId: orgId,
- });
- role = resolved.role;
- scopes = [...resolved.scopes];
- }
+ schema: {
+ organization: {
+ modelName: "organizations",
+ fields: {
+ createdAt: "createdAt",
+ },
+ },
+ },
+ organizationHooks: {
+ afterCreateOrganization: async ({
+ organization,
+ user,
+ }: {
+ organization: Organization;
+ user: User;
+ }) => {
+ await afterOrgCreated({ org: organization, user });
+ },
+ },
+ }),
+ ],
+ } satisfies BetterAuthOptions;
+ const auth = betterAuth({
+ ...options,
+ plugins: [
+ ...options.plugins,
/**
- * Inject `superuser` for Autumn staff. Triggered when the
- * better-auth GLOBAL user role is "admin" (NOT the org role),
- * or when the session is an impersonation. Mirrors the
- * client-side check in `useAdmin` and `adminAuthMiddleware`.
+ * Attach `role` and `scopes` to every session response.
+ *
+ * Must be last in the plugin list so it can observe the session
+ * produced by the other plugins (notably `organization`, which
+ * populates `session.activeOrganizationId`).
+ *
+ * better-auth docs note that custom session fields are NOT cached
+ * (neither in secondary storage nor the cookie cache), so every
+ * `getSession` call pays a DB round-trip. Accepted.
*/
- const globalUserRole = (user as { role?: string } | null | undefined)
- ?.role;
- const impersonatedBy = (
- session as { impersonatedBy?: string | null } | null | undefined
- )?.impersonatedBy;
- if (globalUserRole === "admin" || impersonatedBy) {
- if (!scopes.includes("superuser")) scopes.push("superuser");
- }
+ customSession(async ({ user, session }) => {
+ let role: string | null = null;
+ let scopes: string[] = [];
+ const orgId = session.activeOrganizationId;
+ if (orgId && user?.id) {
+ const resolved = await getScopesForUserInOrg({
+ db,
+ userId: user.id,
+ organizationId: orgId,
+ });
+ role = resolved.role;
+ scopes = [...resolved.scopes];
+ }
- return { user, session, role, scopes };
- }, options),
- ],
-});
+ /**
+ * Inject `superuser` for Autumn staff. Triggered when the
+ * better-auth GLOBAL user role is "admin" (NOT the org role),
+ * or when the session is an impersonation. Mirrors the
+ * client-side check in `useAdmin` and `adminAuthMiddleware`.
+ */
+ const globalUserRole = (user as { role?: string } | null | undefined)
+ ?.role;
+ const impersonatedBy = (
+ session as { impersonatedBy?: string | null } | null | undefined
+ )?.impersonatedBy;
+ if (globalUserRole === "admin" || impersonatedBy) {
+ if (!scopes.includes("superuser")) scopes.push("superuser");
+ }
+
+ return { user, session, role, scopes };
+ }, options),
+ ],
+ });
+
+ return auth;
+};
diff --git a/server/src/utils/authUtils/afterOrgCreated.ts b/server/src/utils/authUtils/afterOrgCreated.ts
index 7d478132a..8bc40b62a 100644
--- a/server/src/utils/authUtils/afterOrgCreated.ts
+++ b/server/src/utils/authUtils/afterOrgCreated.ts
@@ -4,7 +4,7 @@ import type { User } from "better-auth";
import type { Organization as BetterAuthOrganization } from "better-auth/plugins/organization";
import { isUniqueConstraintError } from "@/db/dbUtils.js";
import { db } from "@/db/initDrizzle.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { createSvixApp } from "@/external/svix/svixHelpers.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { createConnectAccount } from "@/internal/orgs/orgUtils/createConnectAccount.js";
@@ -39,13 +39,16 @@ export const afterOrgCreated = async ({
createStripeAccount = true,
pkey,
livePkey,
+ env,
}: {
org: Organization | BetterAuthOrganization;
user: User;
createStripeAccount?: boolean;
pkey?: string;
livePkey?: string;
+ env: Env;
}) => {
+ const logger = createLogger(env);
logger.info(`Org created: ${org.id} (${org.slug})`);
const { id, slug, createdAt } = org;
diff --git a/server/src/utils/authUtils/afterSessionCreated.ts b/server/src/utils/authUtils/afterSessionCreated.ts
index 49779c70c..1e7b46605 100644
--- a/server/src/utils/authUtils/afterSessionCreated.ts
+++ b/server/src/utils/authUtils/afterSessionCreated.ts
@@ -1,8 +1,7 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { GenericEndpointContext } from "@better-auth/core";
import type { BetterAuthOptions, Session } from "better-auth";
-export const afterSessionCreated = async (
+export const createAfterSessionCreated = (env: Env) => async (
session: Session,
context: GenericEndpointContext | null,
) => {
@@ -12,7 +11,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 (runtimeEnv.NODE_ENV !== "production") return;
+ if (env.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 20e966fa3..6b9e669ab 100644
--- a/server/src/utils/authUtils/afterSessionDeleted.ts
+++ b/server/src/utils/authUtils/afterSessionDeleted.ts
@@ -1,8 +1,7 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import type { GenericEndpointContext } from "@better-auth/core";
import type { BetterAuthOptions, Session } from "better-auth";
-export const afterSessionDeleted = async (
+export const createAfterSessionDeleted = (env: Env) => async (
_session: Session,
context: GenericEndpointContext | null,
) => {
@@ -11,7 +10,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 (runtimeEnv.NODE_ENV !== "production") return;
+ if (env.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/authUtils/beforeSessionCreated.ts b/server/src/utils/authUtils/beforeSessionCreated.ts
index 8b63ae22a..cd87c6155 100644
--- a/server/src/utils/authUtils/beforeSessionCreated.ts
+++ b/server/src/utils/authUtils/beforeSessionCreated.ts
@@ -4,7 +4,8 @@ import { eq } from "drizzle-orm";
import { db } from "@/db/initDrizzle.js";
import { createDefaultOrg } from "@/utils/authUtils/createDefaultOrg.js";
-export const beforeSessionCreated = async (session: Session) => {
+export const createBeforeSessionCreated =
+ (env: Env) => async (session: Session) => {
try {
console.log(`Running beforeSessionCreated for user ${session.userId}`);
@@ -24,7 +25,7 @@ export const beforeSessionCreated = async (session: Session) => {
};
}
- const orgId = await createDefaultOrg({ session });
+ const orgId = await createDefaultOrg({ env, session });
return {
data: {
@@ -33,4 +34,4 @@ export const beforeSessionCreated = async (session: Session) => {
},
};
} catch (error) {}
-};
+ };
diff --git a/server/src/utils/authUtils/createDefaultOrg.ts b/server/src/utils/authUtils/createDefaultOrg.ts
index 97ec7e393..e88369344 100644
--- a/server/src/utils/authUtils/createDefaultOrg.ts
+++ b/server/src/utils/authUtils/createDefaultOrg.ts
@@ -3,14 +3,17 @@ import type { Session } from "better-auth";
import type { Organization } from "better-auth/plugins/organization";
import { and, eq, gt } from "drizzle-orm";
import { db } from "@/db/initDrizzle.js";
-import { auth } from "@/utils/auth.js";
+import { createAuth } from "@/utils/auth.js";
import { slugify } from "@/utils/genUtils.js";
export const createDefaultOrg = async ({
+ env,
session,
}: {
+ env: Env;
session: Session;
}): Promise => {
+ const auth = createAuth(env);
try {
const user = await db.query.user.findFirst({
where: eq(userTable.id, session.userId),
diff --git a/server/src/utils/cacheUtils/cacheUtils.ts b/server/src/utils/cacheUtils/cacheUtils.ts
index 86c8d45b9..851581828 100644
--- a/server/src/utils/cacheUtils/cacheUtils.ts
+++ b/server/src/utils/cacheUtils/cacheUtils.ts
@@ -1,5 +1,5 @@
import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
+import { createLogger } from "@/external/logtail/logtailUtils.js";
import { redis } from "@/external/redis/initRedis.js";
import { RedisUnavailableError } from "@/external/redis/utils/errors.js";
import type { UnavailableReason } from "@/external/redis/utils/runRedisOp.js";
@@ -10,10 +10,14 @@ const lastRedisWarningAtBySource = new Map();
const warnRedisUnavailable = ({
source,
error,
+ env,
}: {
source: string;
error?: unknown;
+ env: Env;
}) => {
+ const logger = createLogger(env);
+
const now = Date.now();
const lastWarningAt = lastRedisWarningAtBySource.get(source) ?? 0;
if (now - lastWarningAt < REDIS_WARNING_INTERVAL_MS) return;
@@ -43,12 +47,14 @@ const throwIfRedisUnavailable = ({
targetRedis,
source,
error,
+ env,
}: {
targetRedis: Redis;
source: string;
error?: unknown;
+ env: Env;
}) => {
- warnRedisUnavailable({ source, error });
+ warnRedisUnavailable({ source, error, env });
const reason = classifyRedisUnavailable(targetRedis, error);
if (reason) {
@@ -65,12 +71,14 @@ const throwIfRedisUnavailable = ({
export const tryRedisNx = async ({
operation,
redisInstance,
+ env,
onRedisUnavailable,
onSuccess,
onKeyAlreadyExists,
}: {
operation: () => Promise<"OK" | null>;
redisInstance?: Redis;
+ env: Env;
onRedisUnavailable: () => TUnavailable | Promise;
onSuccess: () => TSuccess | Promise;
onKeyAlreadyExists: () => TExists | Promise;
@@ -82,6 +90,7 @@ export const tryRedisNx = async ({
throwIfRedisUnavailable({
targetRedis,
source: "tryRedisNx:not-ready",
+ env,
});
return await onRedisUnavailable();
}
@@ -95,6 +104,7 @@ export const tryRedisNx = async ({
targetRedis,
source: "tryRedisNx:error",
error,
+ env,
});
return await onRedisUnavailable();
}
@@ -110,6 +120,7 @@ export const tryRedisNx = async ({
* @returns Promise - The result if successful, null otherwise. Returns true if operation returns void/undefined.
*/
export const tryRedisWrite = async (
+ env: Env,
operation: () => Promise,
redisInstance?: Redis,
): Promise => {
@@ -120,6 +131,7 @@ export const tryRedisWrite = async (
throwIfRedisUnavailable({
targetRedis,
source: "tryRedisWrite:not-ready",
+ env,
});
return null as T extends void ? true : T | null;
}
@@ -135,6 +147,7 @@ export const tryRedisWrite = async (
targetRedis,
source: "tryRedisWrite:error",
error,
+ env,
});
return null as T extends void ? true : T | null;
}
@@ -149,6 +162,7 @@ export const tryRedisWrite = async (
* @returns Promise - The data if successful, null otherwise
*/
export const tryRedisRead = async (
+ env: Env,
operation: () => Promise,
redisInstance?: Redis,
): Promise => {
@@ -159,6 +173,7 @@ export const tryRedisRead = async (
throwIfRedisUnavailable({
targetRedis,
source: "tryRedisRead:not-ready",
+ env,
});
return null;
}
@@ -171,6 +186,7 @@ export const tryRedisRead = async (
targetRedis,
source: "tryRedisRead:error",
error,
+ env,
});
return null;
}
diff --git a/server/src/utils/constants.ts b/server/src/utils/constants.ts
index 24326c380..a074dedb4 100644
--- a/server/src/utils/constants.ts
+++ b/server/src/utils/constants.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { CusProductStatus } from "@autumn/shared";
const BREAK_API_VERSION = 0.2;
@@ -22,12 +21,12 @@ export const ADMIN_USER_IDs = [
"K7NDwSwohMCV9BXJ3Yb5MxgeXhWcwj0L", // charlie sandbox
];
-export const dashboardOrigins = [
+export const getDashboardOrigins = (env: Env) => [
"http://localhost:3000",
"https://app.useautumn.com",
"https://staging.useautumn.com",
"https://dev.useautumn.com",
- runtimeEnv.CLIENT_URL!,
+ env.CLIENT_URL!,
];
export const WEBHOOK_EVENTS = [
diff --git a/server/src/utils/encryptUtils.ts b/server/src/utils/encryptUtils.ts
index 491890075..f546f47e3 100644
--- a/server/src/utils/encryptUtils.ts
+++ b/server/src/utils/encryptUtils.ts
@@ -1,18 +1,17 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { AppEnv } from "@autumn/shared";
import crypto from "crypto";
import KSUID from "ksuid";
-const getKey = () => {
+const getKey = (env: Env) => {
return crypto
.createHash("sha512")
- .update(runtimeEnv.ENCRYPTION_PASSWORD!)
+ .update(env.ENCRYPTION_PASSWORD!)
.digest("hex")
.substring(0, 32);
};
-export function encryptData(data: string) {
- const key = getKey();
+export function encryptData(data: string, env: Env) {
+ const key = getKey(env);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
@@ -26,14 +25,14 @@ export function encryptData(data: string) {
return result.toString("base64");
}
-export function decryptData(encryptedData: string) {
+export function decryptData(encryptedData: string, env: Env) {
const buffer = Buffer.from(encryptedData, "base64");
// Extract IV and encrypted data
const iv = buffer.slice(0, 16);
const encrypted = buffer.slice(16);
- const key = getKey();
+ const key = getKey(env);
const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const decrypted = Buffer.concat([
diff --git a/server/src/utils/envUtils.ts b/server/src/utils/envUtils.ts
index 551eca0b7..5057f96d2 100644
--- a/server/src/utils/envUtils.ts
+++ b/server/src/utils/envUtils.ts
@@ -1,140 +1,22 @@
-import { existsSync, readFileSync } from "node:fs";
-import { join } from "node:path";
-
-let hasLoadedLocalEnv = false;
-const shouldLogLocalEnvLoading = false;
-
-type ProcessLike = {
- env?: Record;
-};
-
type MutableEnv = Partial & Record;
-const getProcessEnv = () =>
- (globalThis as typeof globalThis & { process?: ProcessLike }).process?.env;
-
-let injectedEnv: MutableEnv = getProcessEnv() ?? {};
-
-export const setRuntimeEnv = (env: Partial) => {
- const nextEnv: MutableEnv = {};
- Object.assign(nextEnv, env);
- injectedEnv = nextEnv;
-};
-
-export const setRuntimeEnvFromProcess = () => {
- const processEnv = getProcessEnv();
- if (!processEnv) return;
- setRuntimeEnv(processEnv);
-};
-
-export const getRuntimeEnvValue = (key: string) => injectedEnv[key];
-
-export const setRuntimeEnvValue = (key: string, value: string | undefined) => {
- injectedEnv[key] = value;
- const processEnv = getProcessEnv();
- if (processEnv) processEnv[key] = value;
-};
-
-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") {
- setRuntimeEnvValue(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
- * - cwd is `autumn/` (typical workspace root)
- * - cwd is the monorepo root (one level above `autumn/`) — happens with
- * `bun test autumn/...` invocations from VSCode tasks
- */
-const resolveServerDir = (): string => {
- const cwd = process.cwd();
- const candidates = [cwd, join(cwd, "server"), join(cwd, "autumn", "server")];
- for (const dir of candidates) {
- if (existsSync(join(dir, "package.json"))) return dir;
+export const requireEnv = (
+ env: Partial,
+ key: K,
+ context: string,
+): NonNullable => {
+ const value = env[key];
+ if (value === undefined || value === null || value === "") {
+ throw new Error(`Missing required env ${key} for ${context}`);
}
- // Fall back to first guess so local env loading silently no-ops if missing.
- return cwd.includes("server") ? cwd : join(cwd, "server");
+ return value as NonNullable;
};
-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;
-
- const serverDir = resolveServerDir();
-
- // Determine which env file to load based on ENV_FILE environment variable
- // Defaults to .env if not specified
- const envFileName = runtimeEnv.ENV_FILE || ".env";
- const envPath = join(serverDir, envFileName);
-
- // Load local .env file FIRST - these will take precedence over Infisical
- 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(parsed).length} variables from ${envFileName}`,
- );
- }
- for (const [key, value] of Object.entries(parsed)) {
- setRuntimeEnvValue(key, value);
- }
- } else {
- if (shouldLogLocalEnvLoading) {
- console.error(
- `No ${envFileName} file found (using only Infisical secrets)`,
- );
- }
+export const mergeEnv = (...sources: Array | undefined>) => {
+ const merged: MutableEnv = {};
+ for (const source of sources) {
+ if (!source) continue;
+ Object.assign(merged, source);
}
+ return merged;
};
diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts
index ff95aaeea..66e35682d 100644
--- a/server/src/utils/initUtils.ts
+++ b/server/src/utils/initUtils.ts
@@ -1,12 +1,14 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
-import { logger } from "@/external/logtail/logtailUtils.js";
-export const checkEnvVars = () => {
- if (!runtimeEnv.DATABASE_URL) {
+import { createLogger } from "@/external/logtail/logtailUtils.js";
+
+export const checkEnvVars = (env: Env) => {
+ const logger = createLogger(env);
+
+ if (!env.DATABASE_URL) {
console.error(`DATABASE_URL is not set`);
process.exit(1);
}
- if (!runtimeEnv.ENCRYPTION_IV || !runtimeEnv.ENCRYPTION_PASSWORD) {
+ if (!env.ENCRYPTION_IV || !env.ENCRYPTION_PASSWORD) {
console.error(
`ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`,
);
@@ -14,33 +16,33 @@ export const checkEnvVars = () => {
}
if (
- !runtimeEnv.CACHE_URL &&
- !runtimeEnv.CACHE_URL_US_EAST &&
- !runtimeEnv.CACHE_BACKUP_URL?.trim()
+ !env.CACHE_URL &&
+ !env.CACHE_URL_US_EAST &&
+ !env.CACHE_BACKUP_URL?.trim()
) {
logger.warn(
"No Redis URL set (CACHE_URL, CACHE_URL_US_EAST, or CACHE_BACKUP_URL), running without Redis",
);
}
- if (!runtimeEnv.BETTER_AUTH_SECRET || !runtimeEnv.BETTER_AUTH_URL) {
+ if (!env.BETTER_AUTH_SECRET || !env.BETTER_AUTH_URL) {
console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
process.exit(1);
}
- if (!runtimeEnv.RESEND_API_KEY || !runtimeEnv.RESEND_DOMAIN) {
+ if (!env.RESEND_API_KEY || !env.RESEND_DOMAIN) {
logger.warn(
"RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)",
);
}
- if (!runtimeEnv.SUPABASE_URL || !runtimeEnv.SUPABASE_SERVICE_KEY) {
+ if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_KEY) {
logger.warn(
`SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, some actions will be skipped`,
);
}
- if (!runtimeEnv.SVIX_API_KEY) {
+ if (!env.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 48e9e23b6..c16c189e8 100644
--- a/server/src/utils/logging/initLogger.ts
+++ b/server/src/utils/logging/initLogger.ts
@@ -1,6 +1,9 @@
import { Writable } from "node:stream";
import pino from "pino";
-import { getAwsTaskIdentity } from "@/external/aws/ecs/awsTaskIdentity.js";
+import {
+ getAwsTaskIdentity,
+ resolveAwsTaskIdentity,
+} from "@/external/aws/ecs/awsTaskIdentity.js";
/**
* Fields that don't render in the formatted dev/local console output
@@ -167,6 +170,7 @@ export type InitLoggerOptions = {
export const initLogger = (options: InitLoggerOptions, env: Env) => {
const { mode = "default" } = options;
+ void resolveAwsTaskIdentity(env);
const streams: pino.StreamEntry[] = [];
const nodeEnv = env.NODE_ENV as string | undefined;
diff --git a/server/src/utils/memoryMonitor.ts b/server/src/utils/memoryMonitor.ts
index 32f08a181..d63dca2a4 100644
--- a/server/src/utils/memoryMonitor.ts
+++ b/server/src/utils/memoryMonitor.ts
@@ -5,9 +5,9 @@
* 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";
+import { createLogger } from "../external/logtail/logtailUtils.js";
+import type { Logger } from "../external/logtail/logtailUtils.js";
// Event loop lag histogram — samples at 100ms resolution at the C++ level.
// No JS callbacks involved, negligible overhead.
@@ -22,16 +22,26 @@ export function getEventLoopLagMs(): number {
const DEFAULT_INTERVAL_MS = 60_000; // 1 minute
let intervalHandle: ReturnType | null = null;
+let _logger: Logger | null = null;
+let _isDev = false;
function toMB(bytes: number): number {
return Math.round((bytes / 1024 / 1024) * 10) / 10;
}
+function getLogger(): Logger {
+ if (!_logger) {
+ throw new Error("Memory monitor not initialized — call startMemoryMonitor first");
+ }
+ return _logger;
+}
+
function logMemoryUsage(label: string) {
- if (runtimeEnv.NODE_ENV === "development") {
+ if (_isDev) {
return;
}
+ const logger = getLogger();
const mem = process.memoryUsage();
const lagMeanMs = Math.round((lagHistogram.mean / 1e6) * 10) / 10;
@@ -67,8 +77,12 @@ function logMemoryUsage(label: string) {
*/
export function startMemoryMonitor(
label: string,
+ env: Env,
intervalMs = DEFAULT_INTERVAL_MS,
) {
+ _logger = createLogger(env);
+ _isDev = env.NODE_ENV === "development";
+
// Log immediately on start
logMemoryUsage(label);
diff --git a/server/src/utils/otel/FilteringSpanProcessor.ts b/server/src/utils/otel/FilteringSpanProcessor.ts
index 47a53b1aa..d79624013 100644
--- a/server/src/utils/otel/FilteringSpanProcessor.ts
+++ b/server/src/utils/otel/FilteringSpanProcessor.ts
@@ -1,4 +1,3 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { type Context, SpanStatusCode } from "@opentelemetry/api";
import type {
ReadableSpan,
@@ -7,15 +6,12 @@ import type {
} from "@opentelemetry/sdk-trace-base";
import { recordSpanDurationMetric } from "./spanMetrics.js";
-const REDIS_SUCCESS_SAMPLE_RATE = Number.parseFloat(
- runtimeEnv.OTEL_REDIS_SUCCESS_SAMPLE_RATE ?? "0.01",
-);
-
-const normalizedRedisSuccessSampleRate = Number.isFinite(
- REDIS_SUCCESS_SAMPLE_RATE,
-)
- ? Math.min(Math.max(REDIS_SUCCESS_SAMPLE_RATE, 0), 1)
- : 0.01;
+const resolveSampleRate = (env: Env): number => {
+ const raw = Number.parseFloat(env.OTEL_REDIS_SUCCESS_SAMPLE_RATE ?? "0.01");
+ return Number.isFinite(raw)
+ ? Math.min(Math.max(raw, 0), 1)
+ : 0.01;
+};
const hashStringToUnitInterval = (value: string): number => {
let hash = 2166136261;
@@ -32,20 +28,24 @@ const isSuccessfulNonSlowRedisSpan = (span: ReadableSpan) =>
span.status.code === SpanStatusCode.OK &&
span.attributes["db.redis.slow"] !== true;
-const shouldDropSuccessfulRedisSpan = (span: ReadableSpan): boolean => {
- if (!isSuccessfulNonSlowRedisSpan(span)) return false;
- if (normalizedRedisSuccessSampleRate >= 1) return false;
- if (normalizedRedisSuccessSampleRate <= 0) return true;
-
- const spanContext = span.spanContext();
- const sampleKey = `${spanContext.traceId}:${spanContext.spanId}:${span.name}`;
- return (
- hashStringToUnitInterval(sampleKey) >= normalizedRedisSuccessSampleRate
- );
-};
-
export class FilteringSpanProcessor implements SpanProcessor {
- constructor(private readonly delegate: SpanProcessor) {}
+ private readonly sampleRate: number;
+
+ constructor(private readonly delegate: SpanProcessor, env: Env) {
+ this.sampleRate = resolveSampleRate(env);
+ }
+
+ private shouldDropSuccessfulRedisSpan(span: ReadableSpan): boolean {
+ if (!isSuccessfulNonSlowRedisSpan(span)) return false;
+ if (this.sampleRate >= 1) return false;
+ if (this.sampleRate <= 0) return true;
+
+ const spanContext = span.spanContext();
+ const sampleKey = `${spanContext.traceId}:${spanContext.spanId}:${span.name}`;
+ return (
+ hashStringToUnitInterval(sampleKey) >= this.sampleRate
+ );
+ }
onStart(span: Span, parentContext: Context): void {
this.delegate.onStart(span, parentContext);
@@ -54,7 +54,7 @@ export class FilteringSpanProcessor implements SpanProcessor {
onEnd(span: ReadableSpan): void {
recordSpanDurationMetric(span);
- if (shouldDropSuccessfulRedisSpan(span)) return;
+ if (this.shouldDropSuccessfulRedisSpan(span)) return;
this.delegate.onEnd(span);
}
diff --git a/server/src/utils/posthog.ts b/server/src/utils/posthog.ts
index e938a4fca..8ff95499f 100644
--- a/server/src/utils/posthog.ts
+++ b/server/src/utils/posthog.ts
@@ -1,26 +1,35 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import { PostHog } from "posthog-node";
-const posthogClient = runtimeEnv.POSTHOG_API_KEY
- ? new PostHog(runtimeEnv.POSTHOG_API_KEY, {
- host: runtimeEnv.POSTHOG_HOST || "https://us.i.posthog.com",
- })
- : null;
+let _posthogClient: PostHog | null = null;
+
+const getPostHogClient = (env: Env): PostHog | null => {
+ if (!env.POSTHOG_API_KEY) {
+ return null;
+ }
+ if (!_posthogClient) {
+ _posthogClient = new PostHog(env.POSTHOG_API_KEY, {
+ host: env.POSTHOG_HOST || "https://us.i.posthog.com",
+ });
+ }
+ return _posthogClient;
+};
-// Helper for capturing events with org group
export const captureOrgEvent = async ({
+ env,
orgId,
event,
properties = {},
}: {
+ env: Env;
orgId: string;
event: string;
properties?: Record;
}) => {
- if (!posthogClient) return;
+ const client = getPostHogClient(env);
+ if (!client) return;
try {
- await posthogClient.capture({
+ await client.capture({
distinctId: orgId,
event,
properties: {
diff --git a/server/src/utils/scriptUtils/scriptUtils.ts b/server/src/utils/scriptUtils/scriptUtils.ts
index fc35a0710..d4200999a 100644
--- a/server/src/utils/scriptUtils/scriptUtils.ts
+++ b/server/src/utils/scriptUtils/scriptUtils.ts
@@ -9,7 +9,6 @@ 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";
@@ -226,9 +225,11 @@ export const saveCusSubsAndProducts = async ({
export const initScript = async ({
orgId,
env,
+ bindings,
}: {
orgId: string;
env: AppEnv;
+ bindings: Env;
}) => {
const [org, autumnProducts, features] = await Promise.all([
OrgService.get({ db, orgId }),
@@ -246,7 +247,7 @@ export const initScript = async ({
const stripeCli: Stripe = createStripeCli({ org, env });
- const logger = createLogger(runtimeEnv);
+ const logger = createLogger(bindings);
const req: ExtendedRequest = {
orgId,
@@ -302,7 +303,7 @@ export const initReadScript = async ({
const stripeCliRaw: Stripe = createStripeCli({ org, env });
const stripeCli = createReadOnlyStripeCli(stripeCliRaw);
- const logger = createLogger(runtimeEnv);
+ const logger = createLogger(bindings);
const req: ExtendedRequest = {
orgId,
diff --git a/server/src/workers.ts b/server/src/workers.ts
index 69278ae5c..e7730cebe 100644
--- a/server/src/workers.ts
+++ b/server/src/workers.ts
@@ -1,8 +1,8 @@
-import { runtimeEnv } from "@/utils/envUtils.js";
import cluster from "node:cluster";
+import { initHatchet } from "./external/hatchet/initHatchet.js";
import { initInfisical } from "./external/infisical/initInfisical.js";
-import { logger } from "./external/logtail/logtailUtils.js";
+import { createLogger } from "./external/logtail/logtailUtils.js";
import {
startAllEdgeConfigPolling,
stopAllEdgeConfigPolling,
@@ -13,122 +13,124 @@ import "./internal/misc/redisV2Cache/redisV2CacheStore.js";
import "./internal/misc/cacheV2Ramp/cacheV2RampStore.js";
import "./internal/misc/jobQueues/jobQueueStore.js";
-// Number of worker processes (defaults to CPU cores)
-const NUM_PROCESSES = runtimeEnv.NODE_ENV === "development" ? 3 : 4;
+export const startWorkers = async (initialEnv: Env) => {
+ const env = await initInfisical(initialEnv);
+ const logger = createLogger(env);
-// Track if we're shutting down
-let isShuttingDown = false;
+ // Number of worker processes (capped at 4)
+ const NUM_PROCESSES = env.NODE_ENV === "development" ? 3 : 4;
-import { startMemoryMonitor } from "./utils/memoryMonitor.js";
+ // Track if we're shutting down
+ let isShuttingDown = false;
-if (cluster.isPrimary) {
- await initInfisical();
+ const { startMemoryMonitor } = await import("./utils/memoryMonitor.js");
- // const { initHatchetWorker } = await import("./queue/initWorkers.js");
- // await initHatchetWorker();
+ if (cluster.isPrimary) {
+ // const { initHatchetWorker } = await import("./queue/initWorkers.js");
+ // await initHatchetWorker();
- console.log(`Starting ${NUM_PROCESSES} worker processes`);
- console.log(`SQS URL: ${runtimeEnv.SQS_QUEUE_URL_V2}`);
+ console.log(`Starting ${NUM_PROCESSES} worker processes`);
+ console.log(`SQS URL: ${env.SQS_QUEUE_URL_V2}`);
- // Fork workers
- for (let i = 0; i < NUM_PROCESSES; i++) {
- cluster.fork();
- }
-
- // Graceful shutdown handler for primary process
- const shutdown = async () => {
- if (isShuttingDown) return;
- isShuttingDown = true;
-
- console.log(
- "\n🛑 Received shutdown signal, gracefully shutting down workers...",
- );
-
- // Send SIGTERM to all workers
- for (const id in cluster.workers) {
- cluster.workers[id]?.kill("SIGTERM");
- }
-
- // Give workers 10 seconds to finish, then force exit
- const shutdownTimeout = setTimeout(() => {
- console.log("⚠️ Shutdown timeout reached, forcing exit");
- process.exit(0);
- }, 10000);
-
- if (shutdownTimeout.unref) {
- shutdownTimeout.unref();
- }
-
- // Wait for all workers to exit gracefully
- const checkWorkers = setInterval(() => {
- const aliveWorkers = Object.keys(cluster.workers || {}).length;
- if (aliveWorkers === 0) {
- clearInterval(checkWorkers);
- clearTimeout(shutdownTimeout);
- console.log("✅ All workers shut down gracefully");
- process.exit(0);
- }
- }, 100);
- };
-
- process.on("SIGTERM", shutdown);
- process.on("SIGINT", shutdown);
-
- // Handle worker exits
- cluster.on("exit", (worker, code, signal) => {
- if (isShuttingDown) {
- console.log(`[Worker ${worker.process.pid}] Exited gracefully`);
- return;
- }
-
- console.log(
- `⚠️ Worker ${worker.process.pid} died unexpectedly (${signal || code}). Restarting...`,
- );
-
- if (runtimeEnv.NODE_ENV === "development") {
- process.exit(1);
- } else {
+ // Fork workers
+ for (let i = 0; i < NUM_PROCESSES; i++) {
cluster.fork();
}
- });
-} else {
- // Worker process — start OTel SDK so child spans (Stripe/Redis/Drizzle/
- // withSpan/withWorkerSpan) export to Axiom.
- await import("./instrumentation.js");
- const startupStartedAt = Date.now();
- const queueImplementation = "SQS";
- startMemoryMonitor("worker", 60_000);
- await startAllEdgeConfigPolling({ logger });
+ // Graceful shutdown handler for primary process
+ const shutdown = async () => {
+ if (isShuttingDown) return;
+ isShuttingDown = true;
- const { db } = await import("./db/initDrizzle.js");
- const { primeRedisMonitor } = await import(
- "./external/redis/initUtils/redisAvailability.js"
- );
- const { primeRedisV2Monitor, startRedisV2Monitor, stopRedisV2Monitor } =
- await import("./external/redis/initUtils/redisV2Availability.js");
- const { startRedisMonitor, stopRedisMonitor } = await import(
- "./external/redis/initRedis.js"
- );
- const { preWarmOrgRedisConnections } = await import(
- "./external/redis/orgRedisPool.js"
- );
+ console.log(
+ "\n🛑 Received shutdown signal, gracefully shutting down workers...",
+ );
- await Promise.all([primeRedisMonitor(), primeRedisV2Monitor()]);
- startRedisMonitor();
- startRedisV2Monitor();
+ // Send SIGTERM to all workers
+ for (const id in cluster.workers) {
+ cluster.workers[id]?.kill("SIGTERM");
+ }
- void preWarmOrgRedisConnections({ db }).catch((error) => {
- logger.warn("[OrgRedis] Warmup failed", { error });
- });
+ // Give workers 10 seconds to finish, then force exit
+ const shutdownTimeout = setTimeout(() => {
+ console.log("⚠️ Shutdown timeout reached, forcing exit");
+ process.exit(0);
+ }, 10000);
- process.once("exit", () => {
- stopAllEdgeConfigPolling();
- stopRedisMonitor();
- stopRedisV2Monitor();
- });
+ if (shutdownTimeout.unref) {
+ shutdownTimeout.unref();
+ }
- const { initWorkers } = await import("./queue/initWorkers.js");
- await initWorkers({ startupStartedAt, queueImplementation });
- // SQS implementation handles its own SIGTERM/SIGINT
-}
+ // Wait for all workers to exit gracefully
+ const checkWorkers = setInterval(() => {
+ const aliveWorkers = Object.keys(cluster.workers || {}).length;
+ if (aliveWorkers === 0) {
+ clearInterval(checkWorkers);
+ clearTimeout(shutdownTimeout);
+ console.log("✅ All workers shut down gracefully");
+ process.exit(0);
+ }
+ }, 100);
+ };
+
+ process.on("SIGTERM", shutdown);
+ process.on("SIGINT", shutdown);
+
+ // Handle worker exits
+ cluster.on("exit", (worker, code, signal) => {
+ if (isShuttingDown) {
+ console.log(`[Worker ${worker.process.pid}] Exited gracefully`);
+ return;
+ }
+
+ console.log(
+ `⚠️ Worker ${worker.process.pid} died unexpectedly (${signal || code}). Restarting...`,
+ );
+
+ if (env.NODE_ENV === "development") {
+ process.exit(1);
+ } else {
+ cluster.fork();
+ }
+ });
+ } else {
+ // Worker process — start OTel SDK so child spans (Stripe/Redis/Drizzle/
+ // withSpan/withWorkerSpan) export to Axiom.
+ await import("./instrumentation.js");
+
+ startMemoryMonitor("worker", env, 60_000);
+ await startAllEdgeConfigPolling({ logger });
+
+ const { db } = await import("./db/initDrizzle.js");
+ const { primeRedisMonitor } = await import(
+ "./external/redis/initUtils/redisAvailability.js"
+ );
+ const { primeRedisV2Monitor, startRedisV2Monitor, stopRedisV2Monitor } =
+ await import("./external/redis/initUtils/redisV2Availability.js");
+ const { startRedisMonitor, stopRedisMonitor } = await import(
+ "./external/redis/initRedis.js"
+ );
+ const { preWarmOrgRedisConnections } = await import(
+ "./external/redis/orgRedisPool.js"
+ );
+
+ await Promise.all([primeRedisMonitor(), primeRedisV2Monitor()]);
+ startRedisMonitor();
+ startRedisV2Monitor();
+
+ void preWarmOrgRedisConnections({ db }).catch((error) => {
+ logger.warn("[OrgRedis] Warmup failed", { error });
+ });
+
+ process.once("exit", () => {
+ stopAllEdgeConfigPolling();
+ stopRedisMonitor();
+ stopRedisV2Monitor();
+ });
+
+ initHatchet(env);
+ const { initWorkers } = await import("./queue/initWorkers.js");
+ await initWorkers();
+ // SQS implementation handles its own SIGTERM/SIGINT
+ }
+};
diff --git a/server/tests/setup-integration-tests.ts b/server/tests/setup-integration-tests.ts
index b0d7e8a58..d11843611 100644
--- a/server/tests/setup-integration-tests.ts
+++ b/server/tests/setup-integration-tests.ts
@@ -1,5 +1,4 @@
import { execSync } from "node:child_process";
-import { loadLocalEnv } from "@/utils/envUtils";
import type { TestContext } from "./utils/testInitUtils/createTestContext";
const loadInfisicalSecrets = async () => {
@@ -50,7 +49,6 @@ declare global {
console.log("--- Setup integration tests ---");
await loadInfisicalSecrets();
-loadLocalEnv({ force: true });
// Unit-only lanes don't set TESTS_ORG; silently skip there. Anything else
// must succeed — a swallowed init failure here resurfaces as the opaque
diff --git a/server/tests/setupMain.ts b/server/tests/setupMain.ts
index ff0460e0d..8fa5e8ac0 100644
--- a/server/tests/setupMain.ts
+++ b/server/tests/setupMain.ts
@@ -1,6 +1,3 @@
-import { loadLocalEnv } from "../src/utils/envUtils";
-
-loadLocalEnv();
import { AppEnv } from "@autumn/shared";
import { setupOrg } from "@tests/utils/setup/setupOrg.js";