import { z } from "zod"; import { MachinePresetName } from "@trigger.dev/core/v3"; import { BoolEnv } from "./utils/boolEnv"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; import { isValidDuration } from "./services/realtime/duration.server"; // `z.string()` constrained to a `parseDuration`-parseable string (e.g. // `7d`, `1h`). Validated at boot so a typo'd duration fails fast. function durationString() { return z.string().refine(isValidDuration, "must be a duration like 7d, 30d, 365d, 1h, 1y"); } // Parses a CSV of machine preset names (e.g. "small-1x,small-2x") into a // non-empty array of MachinePresetName. Used by COMPUTE_TEMPLATE_MACHINE_PRESETS // and its _REQUIRED variant. Adds zod issues for empty input or unknown names. const parseMachinePresetCsv = (raw: string, ctx: z.RefinementCtx): MachinePresetName[] => { const names = raw .split(",") .map((s) => s.trim()) .filter(Boolean); if (names.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "must list at least one machine preset", }); return z.NEVER; } const out: MachinePresetName[] = []; for (const name of names) { const parsed = MachinePresetName.safeParse(name); if (!parsed.success) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `unknown machine preset: "${name}"`, }); return z.NEVER; } out.push(parsed.data); } return out; }; const GithubAppEnvSchema = z.preprocess( (val) => { const obj = val as any; if (!obj || !obj.GITHUB_APP_ENABLED) { return { ...obj, GITHUB_APP_ENABLED: "0" }; } return obj; }, z.discriminatedUnion("GITHUB_APP_ENABLED", [ z.object({ GITHUB_APP_ENABLED: z.literal("1"), GITHUB_APP_ID: z.string(), GITHUB_APP_PRIVATE_KEY: z.string(), GITHUB_APP_WEBHOOK_SECRET: z.string(), GITHUB_APP_SLUG: z.string(), }), z.object({ GITHUB_APP_ENABLED: z.literal("0"), }), ]) ); // eventually we can make all S2 env vars required once the S2 OSS version is out const S2EnvSchema = z.preprocess( (val) => { const obj = val as any; if (!obj || !obj.S2_ENABLED) { return { ...obj, S2_ENABLED: "0" }; } return obj; }, z.discriminatedUnion("S2_ENABLED", [ z.object({ S2_ENABLED: z.literal("1"), S2_ACCESS_TOKEN: z.string(), S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(), S2_DEPLOYMENT_STREAMS_LOCAL: z.string().default("0"), }), z.object({ S2_ENABLED: z.literal("0"), }), ]) ); const EnvironmentSchema = z .object({ NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]), DATABASE_URL: z .string() .refine( isValidDatabaseUrl, "DATABASE_URL is invalid, for details please check the additional output above this message." ), DATABASE_CONNECTION_LIMIT: z.coerce.number().int().default(10), DATABASE_POOL_TIMEOUT: z.coerce.number().int().default(60), DATABASE_CONNECTION_TIMEOUT: z.coerce.number().int().default(20), // Dashboard-agent conversation store. Cloud points this at a dedicated // database; when unset it falls back to DATABASE_URL (OSS), where // the tables live in the isolated `trigger_dashboard_agent` schema. DASHBOARD_AGENT_DATABASE_URL: z.string().optional(), // The secret key (tr_*) for the runtime environment the dashboard-agent task // is deployed to. The chat session is created in that environment via the // standard chat.agent SDK flow. When unset, the live agent is disabled — the // conversation store / History still work, no chat can start. DASHBOARD_AGENT_SECRET_KEY: z.string().optional(), // Pins agent sessions to a specific deployed version (paired with // --skip-promotion deploys); unset => the project env's current version. DASHBOARD_AGENT_VERSION: z.string().optional(), // Global default for the `hasDashboardAgentAccess` flag. "0" (off) ships the // agent dark; flip to "1" to enable it for everyone at GA. Per-org overrides // (org featureFlags) win regardless. DASHBOARD_AGENT_ENABLED: z.string().default("0"), // "1" gives admins/impersonators an everywhere-preview (default off), // separate from the per-org rollout flag above. DASHBOARD_AGENT_ADMIN_PREVIEW: z.string().default("0"), // Anthropic key for the dashboard agent's Head Start route only (the warm // first-turn step-1 LLM call runs in this process). The agent run itself // uses its own key on the Trigger side. When unset, Head Start is disabled // and the first turn falls back to the normal cold-start path. ANTHROPIC_API_KEY: z.string().optional(), DIRECT_URL: z .string() .refine( isValidDatabaseUrl, "DIRECT_URL is invalid, for details please check the additional output above this message." ), DATABASE_READ_REPLICA_URL: z.string().optional(), // --- Run-ops DB split — Cloud-only scaling concern; OFF by default. --- // Explicit positive opt-in. Split behavior is unreachable unless this is true // AND the distinct-DB sentinel confirms the two URLs are physically distinct DBs. RUN_OPS_SPLIT_ENABLED: BoolEnv.default(false), // Canonical connection URL for the dedicated NEW run-ops DB — drives the runtime pool, the split // decision, replication, and migrations. Optional so single-DB installs never set it. RUN_OPS_DATABASE_URL: z .string() .refine(isValidDatabaseUrl, "RUN_OPS_DATABASE_URL is invalid") .optional(), // The LEGACY run-ops DB (the control-plane DB during the transition). When unset, legacy // run-ops reuses the existing DATABASE_URL (legacy run-ops == control-plane DB initially). RUN_OPS_LEGACY_DATABASE_URL: z .string() .refine(isValidDatabaseUrl, "RUN_OPS_LEGACY_DATABASE_URL is invalid") .optional(), // The NEW dedicated run-ops DB read replica. Optional; self-host never sets it. // Refined (unlike the unrefined control-plane DATABASE_READ_REPLICA_URL) so a malformed run-ops // replica URL fails boot loudly rather than silently degrading — do not align it down to the CP shape. RUN_OPS_DATABASE_READ_REPLICA_URL: z .string() .refine(isValidDatabaseUrl, "RUN_OPS_DATABASE_READ_REPLICA_URL is invalid") .optional(), // --- Control-plane datasource repoint. Additive-only. --- // Optional control-plane DB. Unset (self-host/single-DB) -> getClient()/getReplicaClient() fall back to // DATABASE_URL/DATABASE_READ_REPLICA_URL, so boot is byte-identical. When set, these point at the // dedicated control-plane DSN; moving off the shared DB is an ops config change, not a code edit. CONTROL_PLANE_DATABASE_URL: z .string() .refine( (v) => v === undefined || isValidDatabaseUrl(v), "CONTROL_PLANE_DATABASE_URL is invalid" ) .optional(), CONTROL_PLANE_DATABASE_READ_REPLICA_URL: z.string().optional(), // Control-plane cache relax knobs. Unset -> defaults (DEFAULT_CP_CACHE_TTL_MS / _MAX_ENTRIES). CONTROL_PLANE_CACHE_TTL_MS: z.coerce.number().int().optional(), CONTROL_PLANE_CACHE_MAX_ENTRIES: z.coerce.number().int().optional(), SESSION_SECRET: z.string(), MAGIC_LINK_SECRET: z.string(), ENCRYPTION_KEY: z .string() .refine( (val) => Buffer.from(val, "utf8").length === 32, "ENCRYPTION_KEY must be exactly 32 bytes" ), WHITELISTED_EMAILS: z .string() .refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.") .optional(), ADMIN_EMAILS: z.string().refine(isValidRegex, "ADMIN_EMAILS must be a valid regex.").optional(), REMIX_APP_PORT: z.string().optional(), // Opt-in, dev-only: stream this process's logs over a local telnet/TCP socket on this port. // Read directly from process.env in server.ts (before this schema loads); declared here for discoverability. WEBAPP_TELNET_LOGS_PORT: z.coerce.number().optional(), LOGIN_ORIGIN: z.string().default("http://localhost:3030"), LOGIN_RATE_LIMITS_ENABLED: BoolEnv.default(true), APP_ORIGIN: z.string().default("http://localhost:3030"), API_ORIGIN: z.string().optional(), STREAM_ORIGIN: z.string().optional(), ELECTRIC_ORIGIN: z.string().default("http://localhost:3060"), // A comma separated list of electric origins to shard into different electric instances by environmentId // example: "http://localhost:3060,http://localhost:3061,http://localhost:3062" ELECTRIC_ORIGIN_SHARDS: z.string().optional(), APP_ENV: z.string().default(process.env.NODE_ENV), SERVICE_NAME: z.string().default("trigger.dev webapp"), SENTRY_DSN: z.string().optional(), POSTHOG_PROJECT_KEY: z.string().default("phc_LFH7kJiGhdIlnO22hTAKgHpaKhpM8gkzWAFvHmf5vfS"), // Upstream hosts the /ph reverse proxy forwards to (defaults: PostHog Cloud // EU). The client points api_host at the same-origin /ph path; the proxy // fans out to the ingest vs assets host by path. POSTHOG_INGEST_HOST: z.string().default("eu.i.posthog.com"), POSTHOG_ASSETS_HOST: z.string().default("eu-assets.i.posthog.com"), // PostHog app host, used for the browser toolbar (ui_host) and the server // client. Set to https://us.posthog.com for a US project (also switch the // ingest/assets hosts to their us.i / us-assets equivalents). POSTHOG_HOST: z.string().default("https://eu.posthog.com"), TRIGGER_TELEMETRY_DISABLED: z.string().optional(), AUTH_GITHUB_CLIENT_ID: z.string().optional(), AUTH_GITHUB_CLIENT_SECRET: z.string().optional(), AUTH_GOOGLE_CLIENT_ID: z.string().optional(), AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(), EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), FROM_EMAIL: z.string().optional(), REPLY_TO_EMAIL: z.string().optional(), RESEND_API_KEY: z.string().optional(), SMTP_HOST: z.string().optional(), SMTP_PORT: z.coerce.number().optional(), SMTP_SECURE: BoolEnv.optional(), SMTP_USER: z.string().optional(), SMTP_PASSWORD: z.string().optional(), PLAIN_API_KEY: z.string().optional(), PLAIN_CUSTOMER_CARDS_SECRET: z.string().optional(), PLAIN_CUSTOMER_CARDS_KEY: z.string().optional(), PLAIN_CUSTOMER_CARDS_HEADERS: z.string().optional(), WORKER_SCHEMA: z.string().default("graphile_worker"), WORKER_CONCURRENCY: z.coerce.number().int().default(10), WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), // How often each replica reloads the global flags snapshot from the DB. // Sets kill/ramp propagation latency. GLOBAL_FLAGS_RELOAD_INTERVAL_MS: z.coerce.number().int().min(1000).default(5000), WORKER_ENABLED: z.string().default("true"), GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000), DISABLE_SSE: z.string().optional(), OPENAI_API_KEY: z.string().optional(), // Redis options REDIS_HOST: z.string().optional(), REDIS_READER_HOST: z.string().optional(), REDIS_READER_PORT: z.coerce.number().optional(), REDIS_PORT: z.coerce.number().optional(), REDIS_USERNAME: z.string().optional(), REDIS_PASSWORD: z.string().optional(), REDIS_TLS_DISABLED: z.string().optional(), RATE_LIMIT_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RATE_LIMIT_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RATE_LIMIT_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RATE_LIMIT_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RATE_LIMIT_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RATE_LIMIT_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RATE_LIMIT_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), CACHE_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), CACHE_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), CACHE_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), CACHE_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), CACHE_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), CACHE_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), CACHE_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), CACHE_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), TASK_META_CACHE_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), TASK_META_CACHE_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), TASK_META_CACHE_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), TASK_META_CACHE_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), TASK_META_CACHE_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), TASK_META_CACHE_CURRENT_ENV_TTL_SECONDS: z.coerce.number().default(86400), // Runs-list empty-state check: how far back the ClickHouse "does this env have any run" // probe looks. Bounds the prove-absence partition scan. 0 = unbounded ("any run ever"). RUN_LIST_HAS_RUNS_LOOKBACK_DAYS: z.coerce.number().default(30), // SWR TTLs for the empty-state has-runs cache (memory + Redis). RUN_LIST_HAS_RUNS_CACHE_FRESH_MS: z.coerce.number().default(86_400_000), RUN_LIST_HAS_RUNS_CACHE_STALE_MS: z.coerce.number().default(604_800_000), TASK_META_CACHE_BY_WORKER_TTL_SECONDS: z.coerce.number().default(2592000), REALTIME_STREAMS_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), REALTIME_STREAMS_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), REALTIME_STREAMS_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), REALTIME_STREAMS_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), REALTIME_STREAMS_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), REALTIME_STREAMS_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), REALTIME_STREAMS_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), REALTIME_STREAMS_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), REALTIME_STREAMS_INACTIVITY_TIMEOUT_MS: z.coerce.number().int().default(60000), // 1 minute REALTIME_MAXIMUM_CREATED_AT_FILTER_AGE_IN_MS: z.coerce .number() .int() .default(24 * 60 * 60 * 1000), // 1 day in milliseconds // Master switch for the native realtime backend; off = Electric serves everything, publishes no-op. REALTIME_BACKEND_NATIVE_ENABLED: z.string().default("0"), // Default backend when an org has no `realtimeBackend` override and no global flag row is set. REALTIME_BACKEND_DEFAULT: z.enum(["electric", "native", "shadow"]).default("electric"), // Live long-poll backstop hold (ms); matches Electric's ~20s cadence. REALTIME_BACKEND_NATIVE_LIVE_POLL_TIMEOUT_MS: z.coerce.number().int().default(20_000), // Jitter ratio on the live-poll hold (0.15 = ±15%) to avoid synchronized refetch herds. REALTIME_BACKEND_NATIVE_LIVE_POLL_JITTER_RATIO: z.coerce.number().default(0.15), // Hard cap on the tag-list snapshot size. REALTIME_BACKEND_NATIVE_MAX_LIST_RESULTS: z.coerce.number().int().default(1_000), // TTL/size of the coalescing cache for the multi-run resolve+hydrate (same-filter feeds share one query). REALTIME_BACKEND_NATIVE_RUNSET_CACHE_TTL_MS: z.coerce.number().int().default(1_000), REALTIME_BACKEND_NATIVE_RUNSET_CACHE_MAX_ENTRIES: z.coerce.number().int().default(5_000), // Size/TTL of the per-handle working-set cache used to diff multi-run live polls. REALTIME_BACKEND_NATIVE_WORKING_SET_MAX_ENTRIES: z.coerce.number().int().default(10_000), REALTIME_BACKEND_NATIVE_WORKING_SET_TTL_MS: z.coerce.number().int().default(300_000), // Bucket (ms) the tag-list createdAt floor is quantized to so same-tag feeds share a cache entry; 0 disables. REALTIME_BACKEND_NATIVE_RUNSET_CREATED_AT_BUCKET_MS: z.coerce.number().int().default(60_000), // Leading-edge throttle (ms) on per-env wake delivery; 0 wakes on every change. REALTIME_BACKEND_NATIVE_ENV_WAKE_COALESCE_WINDOW_MS: z.coerce.number().int().default(250), // "1" shares per-connection replay cursors fleet-wide via Redis, so a load-balancer hop reads the connection's true inter-poll gap instead of cold-resolving. REALTIME_BACKEND_NATIVE_SHARED_REPLAY_CURSORS: z.string().default("1"), // "1" holds a multi-run live poll open on a non-matching wake instead of replying up-to-date. REALTIME_BACKEND_NATIVE_HOLD_ON_EMPTY: z.string().default("1"), // Max concurrent fresh ClickHouse resolves per instance (reconnect-stampede gate); 0 disables. REALTIME_BACKEND_NATIVE_RESOLVE_ADMISSION_LIMIT: z.coerce.number().int().default(16), // Replay window (ms) for buffered change records delivered to newly-armed feeds; 0 disables. REALTIME_BACKEND_NATIVE_REPLAY_WINDOW_MS: z.coerce.number().int().default(2_000), // Cap on buffered recent records per env (latest record per run). REALTIME_BACKEND_NATIVE_REPLAY_MAX_RUNS: z.coerce.number().int().default(512), // Keep an env subscribed + buffering this long (ms) after its last feed closes; 0 disables. REALTIME_BACKEND_NATIVE_UNSUBSCRIBE_LINGER_MS: z.coerce.number().int().default(5_000), // Fallback per-env concurrent-connection limit when the org has none configured. REALTIME_BACKEND_NATIVE_DEFAULT_CONCURRENCY_LIMIT: z.coerce.number().int().default(100_000), // TTL/size of the single-run read-through cache that collapses duplicate refetch bursts. REALTIME_BACKEND_NATIVE_RUN_CACHE_TTL_MS: z.coerce.number().int().default(250), REALTIME_BACKEND_NATIVE_RUN_CACHE_MAX_ENTRIES: z.coerce.number().int().default(5_000), // TTL/size of the per-org realtimeBackend flag cache used to pick the serving backend. REALTIME_BACKEND_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), REALTIME_BACKEND_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(50_000), // "1" enables the read-your-writes gate: wake hydrates wait out the measured replica lag // (anchored to the change record's updatedAtMs) and stale reads are retried. REALTIME_BACKEND_NATIVE_REPLICA_LAG_GATE_ENABLED: z.string().default("1"), // Reader-side lag probe cadence while the router is active; probing pauses when idle. REALTIME_BACKEND_NATIVE_REPLICA_LAG_SAMPLE_INTERVAL_MS: z.coerce.number().int().default(250), REALTIME_BACKEND_NATIVE_REPLICA_LAG_IDLE_AFTER_MS: z.coerce.number().int().default(30_000), // The lag estimate is the max sample inside this window (spikes widen it immediately). REALTIME_BACKEND_NATIVE_REPLICA_LAG_WINDOW_MS: z.coerce.number().int().default(5_000), // Estimate before the first sample lands (and the floor when probing is unavailable). REALTIME_BACKEND_NATIVE_REPLICA_LAG_DEFAULT_MS: z.coerce.number().int().default(30), // Safety margin (clock skew + scheduling) added on top of the lag estimate. REALTIME_BACKEND_NATIVE_REPLICA_LAG_MARGIN_MS: z.coerce.number().int().default(10), // Hard cap on any single gate delay — a sick replica degrades freshness, never liveness. REALTIME_BACKEND_NATIVE_REPLICA_LAG_MAX_DELAY_MS: z.coerce.number().int().default(1_000), // Re-hydrate attempts for rows the tripwire still finds stale after the delay. REALTIME_BACKEND_NATIVE_STALE_HYDRATE_RETRIES: z.coerce.number().int().default(3), // How long a tripwire-observed staleness floors the lag estimate (vanilla-PG replicas // can't measure mid-apply lag, so observations carry the estimate between races). REALTIME_BACKEND_NATIVE_REPLICA_LAG_OBSERVED_FLOOR_TTL_MS: z.coerce .number() .int() .default(60_000), PUBSUB_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), PUBSUB_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), PUBSUB_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), PUBSUB_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), PUBSUB_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), PUBSUB_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), PUBSUB_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), // Dedicated pub/sub Redis for the native realtime backend; falls back to PUBSUB_REDIS_* then REDIS_*. REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.PUBSUB_REDIS_HOST ?? process.env.REDIS_HOST), REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PORT: z.coerce .number() .optional() .transform((v) => { if (v !== undefined) return v; const raw = process.env.PUBSUB_REDIS_PORT ?? process.env.REDIS_PORT; return raw ? parseInt(raw) : undefined; }), REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.PUBSUB_REDIS_USERNAME ?? process.env.REDIS_USERNAME), REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.PUBSUB_REDIS_PASSWORD ?? process.env.REDIS_PASSWORD), REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_TLS_DISABLED: z .string() .default(process.env.PUBSUB_REDIS_TLS_DISABLED ?? process.env.REDIS_TLS_DISABLED ?? "false"), REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_CLUSTER_MODE_ENABLED: z .string() .default(process.env.PUBSUB_REDIS_CLUSTER_MODE_ENABLED ?? "0"), // Use sharded pub/sub (SSUBSCRIBE/SPUBLISH) in cluster mode; "0" forces classic pub/sub. REALTIME_BACKEND_NATIVE_PUBSUB_REDIS_SHARDED_ENABLED: z.string().default("1"), DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), DEFAULT_ENV_EXECUTION_CONCURRENCY_BURST_FACTOR: z.coerce.number().default(1.0), DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: z.coerce.number().int().default(300), DEFAULT_DEV_ENV_EXECUTION_ATTEMPTS: z.coerce.number().int().positive().default(1), //API Rate limiting /** * @example "60s" * @example "1m" * @example "1h" * @example "1d" * @example "1000ms" * @example "1000s" */ API_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds API_RATE_LIMIT_MAX: z.coerce.number().int().default(750), // allow bursts of 750 requests API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds API_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), API_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), API_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), API_RATE_LIMIT_JWT_WINDOW: z.string().default("1m"), API_RATE_LIMIT_JWT_TOKENS: z.coerce.number().int().default(60), //v3 PROVIDER_SECRET: z.string().default("provider-secret"), COORDINATOR_SECRET: z.string().default("coordinator-secret"), DEPOT_TOKEN: z.string().optional(), DEPOT_ORG_ID: z.string().optional(), DEPOT_REGION: z.string().default("us-east-1"), // Deployment registry (v3) DEPLOY_REGISTRY_HOST: z.string().min(1), DEPLOY_REGISTRY_USERNAME: z.string().optional(), DEPLOY_REGISTRY_PASSWORD: z.string().optional(), DEPLOY_REGISTRY_NAMESPACE: z.string().min(1).default("trigger"), DEPLOY_REGISTRY_ECR_TAGS: z.string().optional(), // csv, for example: "key1=value1,key2=value2" DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z.string().optional(), DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z.string().optional(), DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z.string().optional(), // raw IAM policy JSON applied to every repo created by the webapp // Deployment registry (v4) - falls back to v3 registry if not specified V4_DEPLOY_REGISTRY_HOST: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_HOST) .pipe(z.string().min(1)), // Ensure final type is required string V4_DEPLOY_REGISTRY_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_USERNAME), V4_DEPLOY_REGISTRY_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_PASSWORD), V4_DEPLOY_REGISTRY_NAMESPACE: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_NAMESPACE) .pipe(z.string().min(1).default("trigger")), // Ensure final type is required string V4_DEPLOY_REGISTRY_ECR_TAGS: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_TAGS), V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_ARN), V4_DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID), V4_DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY: z .string() .optional() .transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_DEFAULT_REPOSITORY_POLICY), // Compute gateway (template creation during deploy finalize) COMPUTE_GATEWAY_URL: z.string().optional(), COMPUTE_GATEWAY_AUTH_TOKEN: z.string().optional(), COMPUTE_TEMPLATE_SHADOW_ROLLOUT_PCT: z.string().optional(), // Comma-separated machine preset names to build boot snapshots for on // deploy (e.g. "small-1x,small-2x,medium-1x"). Default: "small-1x". COMPUTE_TEMPLATE_MACHINE_PRESETS: z .string() .default("small-1x") .transform(parseMachinePresetCsv), // Subset of COMPUTE_TEMPLATE_MACHINE_PRESETS that must succeed for a // required-mode deploy to be considered successful. Failures of presets // outside this list are logged but don't fail the deploy. Defaults to the // full COMPUTE_TEMPLATE_MACHINE_PRESETS list when unset (everything required). COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED: z .string() .optional() .transform((v, ctx) => parseMachinePresetCsv(v ?? process.env.COMPUTE_TEMPLATE_MACHINE_PRESETS ?? "small-1x", ctx) ), DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"), DEPLOY_TIMEOUT_MS: z.coerce .number() .int() .default(60 * 1000 * 8), // 8 minutes DEPLOY_QUEUE_TIMEOUT_MS: z.coerce .number() .int() .default(60 * 1000 * 15), // 15 minutes // When enabled, reject deploys made by v3 CLI versions (i.e. payloads that // omit the `type` field). v4 CLI versions always send `type` ("MANAGED" or "V1"), // so they are unaffected. Defaults to off so detection can run in // log-only mode before enforcement. DEPRECATE_V3_CLI_DEPLOYS_ENABLED: z.string().default("0"), // Master switch for the v3 engine (RunEngineVersion.V1) shutdown. When // enabled it: rejects triggers that resolve to V1 (single, batch, schedule, // replay, triggerAndWait) with a graceful error pointing at the v4 migration // guide; closes the legacy `trigger dev` websocket used by v3 CLIs; and turns // the V1 run-lifecycle background jobs (heartbeat timeout, TTL expiry, retry, // resume, scheduled fires) into no-ops so abandoned V1 runs stop generating // database load. v4 (V2) is never affected (every gate also checks the run is // V1). Defaults to off so self-hosted instances still on V1 keep working. DEPRECATE_V3_ENABLED: z.string().default("0"), // Verify the deploy image exists before promoting. Disable for out-of-band/air-gapped push. ECR only. DEPLOY_IMAGE_VERIFICATION_ENABLED: BoolEnv.default(true), OBJECT_STORE_BASE_URL: z.string().optional(), OBJECT_STORE_BUCKET: z.string().optional(), OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), OBJECT_STORE_REGION: z.string().optional(), OBJECT_STORE_SERVICE: z.string().default("s3"), // Protocol to use for new uploads (e.g., "s3", "r2"). Data without protocol uses default provider above. // If specified, you must configure the corresponding provider using OBJECT_STORE_{PROTOCOL}_* env vars. // Example: OBJECT_STORE_DEFAULT_PROTOCOL=s3 requires OBJECT_STORE_S3_BASE_URL, OBJECT_STORE_S3_ACCESS_KEY_ID, etc. // Enables zero-downtime migration between providers (old data keeps working, new data uses new provider). OBJECT_STORE_DEFAULT_PROTOCOL: z .string() .regex(/^[a-z0-9]+$/) .optional(), ARTIFACTS_OBJECT_STORE_BUCKET: z.string().optional(), ARTIFACTS_OBJECT_STORE_BASE_URL: z.string().optional(), ARTIFACTS_OBJECT_STORE_ACCESS_KEY_ID: z.string().optional(), ARTIFACTS_OBJECT_STORE_SECRET_ACCESS_KEY: z.string().optional(), ARTIFACTS_OBJECT_STORE_REGION: z.string().optional(), EVENTS_BATCH_SIZE: z.coerce.number().int().default(100), EVENTS_BATCH_INTERVAL: z.coerce.number().int().default(1000), EVENTS_DEFAULT_LOG_RETENTION: z.coerce.number().int().default(7), EVENTS_MIN_CONCURRENCY: z.coerce.number().int().default(1), EVENTS_MAX_CONCURRENCY: z.coerce.number().int().default(10), EVENTS_MAX_BATCH_SIZE: z.coerce.number().int().default(500), EVENTS_MEMORY_PRESSURE_THRESHOLD: z.coerce.number().int().default(5000), EVENTS_LOAD_SHEDDING_THRESHOLD: z.coerce.number().int().default(100000), EVENTS_LOAD_SHEDDING_ENABLED: z.string().default("1"), SHARED_QUEUE_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), SHARED_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(100), SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS: z.coerce.number().int().default(100), SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS: z.coerce.number().int().default(1000), SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE: z.coerce.number().int().default(25), MANAGED_WORKER_SECRET: z.string().default("managed-secret"), // Development OTEL environment variables DEV_OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(), DEV_OTEL_METRICS_ENDPOINT: z.string().optional(), // If this is set to 1, then the below variables are used to configure the batch processor for spans and logs DEV_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), DEV_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), DEV_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), DEV_OTEL_METRICS_EXPORT_INTERVAL_MILLIS: z.string().optional(), DEV_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS: z.string().optional(), DEV_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS: z.string().optional(), PROD_OTEL_BATCH_PROCESSING_ENABLED: z.string().default("0"), PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS: z.string().default("200"), PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), PROD_OTEL_SPAN_MAX_QUEUE_SIZE: z.string().default("512"), PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE: z.string().default("64"), PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS: z.string().default("200"), PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS: z.string().default("30000"), PROD_OTEL_LOG_MAX_QUEUE_SIZE: z.string().default("512"), PROD_OTEL_METRICS_EXPORT_INTERVAL_MILLIS: z.string().optional(), PROD_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS: z.string().optional(), PROD_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS: z.string().optional(), TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT: z.string().default("1024"), TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT: z.string().default("131072"), TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT: z.string().default("10"), TRIGGER_OTEL_LINK_COUNT_LIMIT: z.string().default("2"), TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT: z.string().default("10"), TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT: z.string().default("10"), CHECKPOINT_THRESHOLD_IN_MS: z.coerce.number().int().default(30000), // Internal OTEL environment variables INTERNAL_OTEL_TRACE_EXPORTER_URL: z.string().optional(), INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADERS: z.string().optional(), INTERNAL_OTEL_TRACE_LOGGING_ENABLED: z.string().default("1"), // this means 1/20 traces or 5% of traces will be sampled (sampled = recorded) INTERNAL_OTEL_TRACE_SAMPLING_RATE: z.string().default("20"), INTERNAL_OTEL_TRACE_INSTRUMENT_PRISMA_ENABLED: z.string().default("0"), INTERNAL_OTEL_TRACE_DISABLED: z.string().default("0"), DISABLE_HTTP_INSTRUMENTATION: BoolEnv.default(false), INTERNAL_OTEL_LOG_EXPORTER_URL: z.string().optional(), INTERNAL_OTEL_METRIC_EXPORTER_URL: z.string().optional(), INTERNAL_OTEL_METRIC_EXPORTER_AUTH_HEADERS: z.string().optional(), INTERNAL_OTEL_METRIC_EXPORTER_ENABLED: z.string().default("0"), INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL_MS: z.coerce.number().int().default(30_000), INTERNAL_OTEL_HOST_METRICS_ENABLED: BoolEnv.default(true), INTERNAL_OTEL_NODEJS_METRICS_ENABLED: BoolEnv.default(true), INTERNAL_OTEL_ADDITIONAL_DETECTORS_ENABLED: BoolEnv.default(true), ORG_SLACK_INTEGRATION_CLIENT_ID: z.string().optional(), ORG_SLACK_INTEGRATION_CLIENT_SECRET: z.string().optional(), /** Vercel integration OAuth credentials */ VERCEL_INTEGRATION_CLIENT_ID: z.string().optional(), VERCEL_INTEGRATION_CLIENT_SECRET: z.string().optional(), VERCEL_INTEGRATION_APP_SLUG: z.string().optional(), /** These enable the alerts feature in v3 */ ALERT_EMAIL_TRANSPORT: z.enum(["resend", "smtp", "aws-ses"]).optional(), ALERT_FROM_EMAIL: z.string().optional(), ALERT_REPLY_TO_EMAIL: z.string().optional(), ALERT_RESEND_API_KEY: z.string().optional(), ALERT_SMTP_HOST: z.string().optional(), ALERT_SMTP_PORT: z.coerce.number().optional(), ALERT_SMTP_SECURE: BoolEnv.optional(), ALERT_SMTP_USER: z.string().optional(), ALERT_SMTP_PASSWORD: z.string().optional(), ALERT_RATE_LIMITER_EMISSION_INTERVAL: z.coerce.number().int().default(2_500), ALERT_RATE_LIMITER_BURST_TOLERANCE: z.coerce.number().int().default(10_000), ALERT_RATE_LIMITER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), ALERT_RATE_LIMITER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), ALERT_RATE_LIMITER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), ALERT_RATE_LIMITER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), ALERT_RATE_LIMITER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), ALERT_RATE_LIMITER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), ALERT_RATE_LIMITER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), ALERT_RATE_LIMITER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), LOOPS_API_KEY: z.string().optional(), ATTIO_API_KEY: z.string().optional(), MARQS_DISABLE_REBALANCING: BoolEnv.default(false), MARQS_VISIBILITY_TIMEOUT_MS: z.coerce .number() .int() .default(60 * 1000 * 15), MARQS_SHARED_QUEUE_LIMIT: z.coerce.number().int().default(1000), MARQS_MAXIMUM_QUEUE_PER_ENV_COUNT: z.coerce.number().int().default(50), MARQS_DEV_QUEUE_LIMIT: z.coerce.number().int().default(1000), MARQS_MAXIMUM_NACK_COUNT: z.coerce.number().int().default(64), MARQS_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), MARQS_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), MARQS_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), MARQS_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), MARQS_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(250), MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT: z.coerce.number().int().default(10), MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED: z.string().default("0"), MARQS_WORKER_ENABLED: z.string().default("0"), MARQS_WORKER_COUNT: z.coerce.number().int().default(2), MARQS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), MARQS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(5), MARQS_WORKER_POLL_INTERVAL_MS: z.coerce.number().int().default(100), MARQS_WORKER_IMMEDIATE_POLL_INTERVAL_MS: z.coerce.number().int().default(100), MARQS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), MARQS_SHARED_WORKER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), MARQS_SHARED_WORKER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(5_000), PROD_TASK_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), VERBOSE_GRAPHILE_LOGGING: z.string().default("false"), V2_MARQS_ENABLED: z.string().default("0"), V2_MARQS_CONSUMER_POOL_ENABLED: z.string().default("0"), V2_MARQS_CONSUMER_POOL_SIZE: z.coerce.number().int().default(10), V2_MARQS_CONSUMER_POLL_INTERVAL_MS: z.coerce.number().int().default(1000), V2_MARQS_QUEUE_SELECTION_COUNT: z.coerce.number().int().default(36), V2_MARQS_VISIBILITY_TIMEOUT_MS: z.coerce .number() .int() .default(60 * 1000 * 15), V2_MARQS_DEFAULT_ENV_CONCURRENCY: z.coerce.number().int().default(100), V2_MARQS_VERBOSE: z.string().default("0"), V3_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), V2_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"), /* Usage settings */ USAGE_EVENT_URL: z.string().optional(), PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(), CENTS_PER_RUN: z.coerce.number().default(0), EVENT_LOOP_MONITOR_ENABLED: z.string().default("1"), RESOURCE_MONITOR_ENABLED: z.string().default("0"), MAXIMUM_LIVE_RELOADING_EVENTS: z.coerce.number().int().default(1000), MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000), MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(10_000), // Emergency circuit breaker: when set, clamps the trace summary and detailed // summary span limits on both event store paths to this value. Unset = disabled. TRACE_VIEW_EMERGENCY_SPAN_CAP: z.coerce.number().int().positive().optional(), TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB BATCH_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().optional(), // Defaults to TASK_PAYLOAD_OFFLOAD_THRESHOLD if not set TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB TASK_RUN_METADATA_MAXIMUM_SIZE: z.coerce.number().int().default(262_144), // 256KB MAXIMUM_DEV_QUEUE_SIZE: z.coerce.number().int().optional(), MAXIMUM_DEPLOYED_QUEUE_SIZE: z.coerce.number().int().optional(), QUEUE_SIZE_CACHE_TTL_MS: z.coerce.number().int().optional().default(1_000), // 1 second QUEUE_SIZE_CACHE_MAX_SIZE: z.coerce.number().int().optional().default(5_000), QUEUE_SIZE_CACHE_ENABLED: z.coerce.number().int().optional().default(1), MAX_BATCH_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), MAX_BATCH_AND_WAIT_V2_TRIGGER_ITEMS: z.coerce.number().int().default(500), // 2-phase batch API settings STREAMING_BATCH_MAX_ITEMS: z.coerce.number().int().default(1_000), // Max items in streaming batch STREAMING_BATCH_ITEM_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // Number of streamed batch items ingested concurrently in Phase 2. Peak // in-flight memory per request ≈ this × STREAMING_BATCH_ITEM_MAXIMUM_SIZE, // so raise with care. Set to 1 for fully sequential ingestion. STREAMING_BATCH_INGEST_CONCURRENCY: z.coerce.number().int().positive().default(10), BATCH_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(100), BATCH_RATE_LIMIT_MAX: z.coerce.number().int().default(1200), BATCH_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), BATCH_CONCURRENCY_LIMIT_DEFAULT: z.coerce.number().int().default(5), REALTIME_STREAM_VERSION: z.enum(["v1", "v2"]).default("v1"), REALTIME_STREAM_MAX_LENGTH: z.coerce.number().int().default(1000), REALTIME_STREAM_TTL: z.coerce .number() .int() .default(60 * 60 * 24), // 1 day in seconds BATCH_METADATA_OPERATIONS_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), BATCH_METADATA_OPERATIONS_FLUSH_ENABLED: z.string().default("1"), BATCH_METADATA_OPERATIONS_FLUSH_LOGGING_ENABLED: z.string().default("1"), // Run Engine 2.0 RUN_ENGINE_WORKER_COUNT: z.coerce.number().int().default(4), RUN_ENGINE_TASKS_PER_WORKER: z.coerce.number().int().default(10), RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(10), RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(100), RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), RUN_ENGINE_TIMEOUT_PENDING_EXECUTING: z.coerce.number().int().default(60_000), RUN_ENGINE_TIMEOUT_PENDING_CANCEL: z.coerce.number().int().default(60_000), RUN_ENGINE_TIMEOUT_EXECUTING: z.coerce.number().int().default(300_000), // 5 minutes RUN_ENGINE_TIMEOUT_EXECUTING_WITH_WAITPOINTS: z.coerce.number().int().default(300_000), // 5 minutes RUN_ENGINE_TIMEOUT_SUSPENDED: z.coerce .number() .int() .default(60_000 * 10), RUN_ENGINE_DEBUG_WORKER_NOTIFICATIONS: BoolEnv.default(false), RUN_ENGINE_PARENT_QUEUE_LIMIT: z.coerce.number().int().default(1000), RUN_ENGINE_CONCURRENCY_LIMIT_BIAS: z.coerce.number().default(0.75), RUN_ENGINE_AVAILABLE_CAPACITY_BIAS: z.coerce.number().default(0.3), RUN_ENGINE_QUEUE_AGE_RANDOMIZATION_BIAS: z.coerce.number().default(0.25), RUN_ENGINE_REUSE_SNAPSHOT_COUNT: z.coerce.number().int().default(0), RUN_ENGINE_MAXIMUM_ENV_COUNT: z.coerce.number().int().optional(), RUN_ENGINE_RUN_QUEUE_SHARD_COUNT: z.coerce.number().int().default(4), RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), RUN_ENGINE_RETRY_WARM_START_THRESHOLD_MS: z.coerce.number().int().default(30_000), RUN_ENGINE_PROCESS_WORKER_QUEUE_DEBOUNCE_MS: z.coerce.number().int().default(200), RUN_ENGINE_DEQUEUE_BLOCKING_TIMEOUT_SECONDS: z.coerce.number().int().default(10), RUN_ENGINE_DEQUEUE_DISABLED_WORKER_QUEUES: z.string().optional(), RUN_ENGINE_MASTER_QUEUE_CONSUMERS_INTERVAL_MS: z.coerce.number().int().default(1000), // Off by default. Enable on a single service (e.g. the engine worker) so only one // instance reports worker queue length, rather than every replica. RUN_ENGINE_WORKER_QUEUE_OBSERVER_ENABLED: z.string().default("0"), RUN_ENGINE_WORKER_QUEUE_OBSERVER_INTERVAL_MS: z.coerce.number().int().default(30_000), // Comma-separated cloud providers to exclude from worker queue length observation. RUN_ENGINE_WORKER_QUEUE_OBSERVER_EXCLUDED_CLOUD_PROVIDERS: z.string().default("digitalocean"), RUN_ENGINE_MASTER_QUEUE_COOLOFF_PERIOD_MS: z.coerce.number().int().default(10_000), RUN_ENGINE_MASTER_QUEUE_COOLOFF_COUNT_THRESHOLD: z.coerce.number().int().default(10), RUN_ENGINE_MASTER_QUEUE_CONSUMER_DEQUEUE_COUNT: z.coerce.number().int().default(10), RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_SCHEDULE: z.string().optional(), RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_SCHEDULE: z.string().optional(), RUN_ENGINE_CONCURRENCY_SWEEPER_SCAN_JITTER_IN_MS: z.coerce.number().int().optional(), RUN_ENGINE_CONCURRENCY_SWEEPER_PROCESS_MARKED_JITTER_IN_MS: z.coerce.number().int().optional(), // TTL System settings for automatic run expiration RUN_ENGINE_TTL_SYSTEM_DISABLED: BoolEnv.default(false), RUN_ENGINE_TTL_SYSTEM_SHARD_COUNT: z.coerce.number().int().optional(), RUN_ENGINE_TTL_SYSTEM_POLL_INTERVAL_MS: z.coerce.number().int().default(1_000), RUN_ENGINE_TTL_SYSTEM_BATCH_SIZE: z.coerce.number().int().default(100), RUN_ENGINE_TTL_WORKER_CONCURRENCY: z.coerce.number().int().default(1), RUN_ENGINE_TTL_WORKER_BATCH_MAX_SIZE: z.coerce.number().int().default(50), RUN_ENGINE_TTL_CONSUMERS_DISABLED: BoolEnv.default(false), RUN_ENGINE_TTL_WORKER_BATCH_MAX_WAIT_MS: z.coerce.number().int().default(5_000), /** Optional maximum TTL for all runs (e.g. "14d"). If set, runs without an explicit TTL * will use this as their TTL, and runs with a TTL larger than this will be clamped. */ RUN_ENGINE_DEFAULT_MAX_TTL: z.string().optional(), RUN_ENGINE_RUN_LOCK_DURATION: z.coerce.number().int().default(5000), RUN_ENGINE_RUN_LOCK_AUTOMATIC_EXTENSION_THRESHOLD: z.coerce.number().int().default(1000), RUN_ENGINE_RUN_LOCK_MAX_RETRIES: z.coerce.number().int().default(10), RUN_ENGINE_RUN_LOCK_BASE_DELAY: z.coerce.number().int().default(100), RUN_ENGINE_RUN_LOCK_MAX_DELAY: z.coerce.number().int().default(3000), RUN_ENGINE_RUN_LOCK_BACKOFF_MULTIPLIER: z.coerce.number().default(1.8), RUN_ENGINE_RUN_LOCK_JITTER_FACTOR: z.coerce.number().default(0.15), RUN_ENGINE_RUN_LOCK_MAX_TOTAL_WAIT_TIME: z.coerce.number().int().default(15000), RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_COUNT: z.coerce.number().int().default(12), RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_MAX_DELAY_MS: z.coerce .number() .int() .default(60_000 * 60 * 6), RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_INITIAL_DELAY_MS: z.coerce .number() .int() .default(60_000), RUN_ENGINE_SUSPENDED_HEARTBEAT_RETRIES_FACTOR: z.coerce.number().default(2), /** Maximum duration in milliseconds that a run can be debounced. Default: 1 hour (3,600,000ms) */ RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS: z.coerce .number() .int() .default(60_000 * 60), // 1 hour /** * Bucket size in milliseconds used to quantize the newly computed `delayUntil` * in the debounce system. Quantization collapses concurrent triggers on the * same hot debounce key onto the same target time so the unlocked fast-path * skip is effective. Set to 0 to disable. Default: 1000ms (1s). */ RUN_ENGINE_DEBOUNCE_QUANTIZE_NEW_DELAY_UNTIL_MS: z.coerce.number().int().min(0).default(1000), /** * Whether the unlocked fast-path skip is enabled in the debounce system. * Acts as a kill switch in case the fast-path needs to be disabled in * production without a redeploy. Default: "1" (enabled). */ RUN_ENGINE_DEBOUNCE_FAST_PATH_SKIP_ENABLED: z.string().default("1"), RUN_ENGINE_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RUN_ENGINE_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RUN_ENGINE_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RUN_ENGINE_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RUN_ENGINE_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), RUN_ENGINE_RUN_QUEUE_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RUN_ENGINE_RUN_QUEUE_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RUN_ENGINE_RUN_QUEUE_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RUN_ENGINE_RUN_QUEUE_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RUN_ENGINE_RUN_QUEUE_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RUN_ENGINE_RUN_QUEUE_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RUN_ENGINE_RUN_QUEUE_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), RUN_ENGINE_RUN_LOCK_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RUN_ENGINE_RUN_LOCK_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RUN_ENGINE_RUN_LOCK_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RUN_ENGINE_RUN_LOCK_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RUN_ENGINE_RUN_LOCK_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RUN_ENGINE_RUN_LOCK_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RUN_ENGINE_RUN_LOCK_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), RUN_ENGINE_DEV_PRESENCE_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RUN_ENGINE_DEV_PRESENCE_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RUN_ENGINE_DEV_PRESENCE_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RUN_ENGINE_DEV_PRESENCE_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), //API Rate limiting /** * @example "60s" * @example "1m" * @example "1h" * @example "1d" * @example "1000ms" * @example "1000s" */ RUN_ENGINE_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds RUN_ENGINE_RATE_LIMIT_MAX: z.coerce.number().int().default(1200), // allow bursts of 750 requests RUN_ENGINE_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(400), // refix 250 tokens every 10 seconds RUN_ENGINE_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"), RUN_ENGINE_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"), RUN_ENGINE_RATE_LIMIT_LIMITER_LOGS_ENABLED: z.string().default("0"), RUN_ENGINE_RELEASE_CONCURRENCY_ENABLED: z.string().default("0"), RUN_ENGINE_RELEASE_CONCURRENCY_DISABLE_CONSUMERS: z.string().default("0"), RUN_ENGINE_RELEASE_CONCURRENCY_MAX_TOKENS_RATIO: z.coerce.number().default(1), RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_MAX_AGE: z.coerce .number() .int() .default(60_000 * 30), RUN_ENGINE_RELEASE_CONCURRENCY_RELEASINGS_POLL_INTERVAL: z.coerce .number() .int() .default(60_000), RUN_ENGINE_RELEASE_CONCURRENCY_MAX_RETRIES: z.coerce.number().int().default(3), RUN_ENGINE_RELEASE_CONCURRENCY_CONSUMERS_COUNT: z.coerce.number().int().default(1), RUN_ENGINE_RELEASE_CONCURRENCY_POLL_INTERVAL: z.coerce.number().int().default(500), RUN_ENGINE_RELEASE_CONCURRENCY_BATCH_SIZE: z.coerce.number().int().default(10), RUN_ENGINE_WORKER_ENABLED: z.string().default("1"), RUN_ENGINE_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), RUN_ENGINE_RUN_QUEUE_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), RUN_ENGINE_TREAT_PRODUCTION_EXECUTION_STALLS_AS_OOM: z.string().default("0"), RUN_ENGINE_READ_REPLICA_SNAPSHOTS_SINCE_ENABLED: z.string().default("0"), RUN_ENGINE_SNAPSHOTS_SINCE_REPLICA_RETRY_MIN_MS: z.coerce.number().int().default(50), RUN_ENGINE_SNAPSHOTS_SINCE_REPLICA_RETRY_MAX_MS: z.coerce.number().int().default(200), RUN_ENGINE_DEBOUNCE_USE_REPLICA_FOR_FAST_PATH_READ: z.string().default("0"), /** How long should the presence ttl last */ DEV_PRESENCE_SSE_TIMEOUT: z.coerce.number().int().default(30_000), DEV_PRESENCE_TTL_MS: z.coerce.number().int().default(5_000), DEV_PRESENCE_POLL_MS: z.coerce.number().int().default(1_000), /** How many ms to wait until dequeuing again, if there was a run last time */ DEV_DEQUEUE_INTERVAL_WITH_RUN: z.coerce.number().int().default(250), /** How many ms to wait until dequeuing again, if there was no run last time */ DEV_DEQUEUE_INTERVAL_WITHOUT_RUN: z.coerce.number().int().default(1_000), /** The max number of runs per API call that we'll dequeue in DEV */ DEV_DEQUEUE_MAX_RUNS_PER_PULL: z.coerce.number().int().default(10), /** The maximum concurrent local run processes executing at once in dev. This is a hard limit */ DEV_MAX_CONCURRENT_RUNS: z.coerce.number().int().optional(), /** The CLI should connect to this for dev runs */ DEV_ENGINE_URL: z.string().default(process.env.APP_ORIGIN ?? "http://localhost:3030"), LEGACY_RUN_ENGINE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(1), LEGACY_RUN_ENGINE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), LEGACY_RUN_ENGINE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), LEGACY_RUN_ENGINE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), LEGACY_RUN_ENGINE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), LEGACY_RUN_ENGINE_WORKER_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), LEGACY_RUN_ENGINE_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), LEGACY_RUN_ENGINE_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), LEGACY_RUN_ENGINE_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), LEGACY_RUN_ENGINE_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), LEGACY_RUN_ENGINE_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), LEGACY_RUN_ENGINE_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), LEGACY_RUN_ENGINE_WORKER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), LEGACY_RUN_ENGINE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_SIZE: z.coerce.number().int().default(100), LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_BATCH_STAGGER_MS: z.coerce.number().int().default(1_000), LEGACY_RUN_ENGINE_WAITING_FOR_DEPLOY_DISABLED: z.string().default("0"), COMMON_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), COMMON_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), COMMON_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), COMMON_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), COMMON_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), COMMON_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), COMMON_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), COMMON_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), COMMON_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), COMMON_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), COMMON_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), COMMON_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), COMMON_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), COMMON_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), COMMON_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), // Global default for the scheduled worker-queue split. When "1", runs in a // scheduled lineage (rootTriggerSource === "schedule") are routed to a // dedicated `:scheduled` worker queue so a separate consumer fleet // can dequeue them independently of standard/agent runs. The per-org // `workerQueueScheduledSplitEnabled` feature flag overrides this default in // BOTH directions (an org set to false stays on the single queue even when // this is "1"; an org set to true splits even when this is "0"). Never // applies to DEVELOPMENT environments. TRIGGER_WORKER_QUEUE_SCHEDULED_SPLIT_ENABLED: z.string().default("0"), TRIGGER_MOLLIFIER_ENABLED: z.string().default("0"), // Separate switch for the drainer (consumer side) so it can be split // off onto a dedicated worker service. Unset → inherits // TRIGGER_MOLLIFIER_ENABLED, so single-container self-hosters don't have to // flip two switches. Multi-replica drainers are correct — `popAndMarkDraining` // is an atomic RPOP + status flip in one Lua call, so only one replica // can win any given entry — but inefficient: polling load (SMEMBERS + // per-env scans) multiplies by N, and `TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY` // is per-process so engine load also multiplies. Splitting the drainer // onto a dedicated worker keeps that traffic off the request-serving // replicas. `TRIGGER_MOLLIFIER_ENABLED` is still the master kill switch; // setting this to "1" while `TRIGGER_MOLLIFIER_ENABLED` is "0" is a // no-op because the gate-side singleton refuses to construct a buffer // when the system is off. TRIGGER_MOLLIFIER_DRAINER_ENABLED: z .string() .default(process.env.TRIGGER_MOLLIFIER_ENABLED ?? "0"), TRIGGER_MOLLIFIER_SHADOW_MODE: z.string().default("0"), TRIGGER_MOLLIFIER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), TRIGGER_MOLLIFIER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), TRIGGER_MOLLIFIER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), TRIGGER_MOLLIFIER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), TRIGGER_MOLLIFIER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), TRIGGER_MOLLIFIER_TRIP_WINDOW_MS: z.coerce.number().int().positive().default(200), TRIGGER_MOLLIFIER_TRIP_THRESHOLD: z.coerce.number().int().positive().default(100), TRIGGER_MOLLIFIER_HOLD_MS: z.coerce.number().int().positive().default(500), TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY: z.coerce.number().int().positive().default(50), TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3), TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000), TRIGGER_MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK: z.coerce.number().int().positive().default(500), // Per-env per-tick pop cap. The drainer rotates one env per org per // tick; this bounds how many entries it pops from that env before // dispatching them through the shared `DRAIN_CONCURRENCY`-bounded // limiter. Default matches `DRAIN_CONCURRENCY` so a single-env burst // uses the full handler-parallelism budget — for 20k buffered on one // env this is the difference between ~17m (one-pop-per-tick × ~50ms) // and ~20s (400 ticks × concurrent engine.trigger). Org/env fairness // is preserved because the per-tick env selection is unchanged; only // the in-env pop count grows. TRIGGER_MOLLIFIER_DRAIN_BATCH_SIZE: z.coerce.number().int().positive().default(50), // Periodic sweep that scans buffer queue LISTs for entries whose // dwell exceeds the stale threshold. Independent of the drainer — // its job is exactly to make a stuck/offline drainer visible to // ops. Defaults: explicitly opt-in (a separate kill switch from // the mollifier itself), run every 5 minutes, alert on anything // that's been dwelling for 5+ minutes (matches the sweep interval // — "anything still here when we check" is the simplest threshold // that converges). // // The sweep was previously defaulting to inherit // `TRIGGER_MOLLIFIER_ENABLED`, which meant any deployment already // running with the mollifier on would auto-start the sweep worker // on upgrade — turning on new background load with no explicit // rollout step. Hard-defaulting to "0" preserves the intent of // exposing the sweep as a separate switch. TRIGGER_MOLLIFIER_STALE_SWEEP_ENABLED: z.string().default("0"), TRIGGER_MOLLIFIER_STALE_SWEEP_INTERVAL_MS: z.coerce .number() .int() .positive() .default(5 * 60_000), TRIGGER_MOLLIFIER_STALE_SWEEP_THRESHOLD_MS: z.coerce .number() .int() .positive() .default(5 * 60_000), // Bounds for one stale-sweep pass (see mollifierStaleSweep.server.ts). // Max entries scanned per env and max orgs visited per tick — together // they cap the Redis traffic / wall-time of a single sweep pass. TRIGGER_MOLLIFIER_STALE_SWEEP_MAX_ENTRIES_PER_ENV: z.coerce .number() .int() .positive() .default(1000), TRIGGER_MOLLIFIER_STALE_SWEEP_MAX_ORGS_PER_PASS: z.coerce .number() .int() .positive() .default(100), // --- Mollifier buffer internals (wired into MollifierBuffer in // mollifierBuffer.server.ts). --- // Grace TTL applied to the entry hash on drainer ack so direct reads // (retrieve, trace) have a safety net while PG replica lag settles. TRIGGER_MOLLIFIER_ACK_GRACE_TTL_SECONDS: z.coerce.number().int().positive().default(30), // ioredis per-request retry limit on the buffer's Redis client. TRIGGER_MOLLIFIER_REDIS_MAX_RETRIES_PER_REQUEST: z.coerce.number().int().positive().default(20), // ioredis reconnect backoff envelope for the buffer client: the base // grows by `STEP_MS` per attempt, capped at `MAX_MS`, then equal-jittered. TRIGGER_MOLLIFIER_REDIS_RECONNECT_STEP_MS: z.coerce.number().int().positive().default(50), TRIGGER_MOLLIFIER_REDIS_RECONNECT_MAX_MS: z.coerce.number().int().positive().default(1000), // --- Mollifier drainer loop internals (wired into MollifierDrainer in // mollifierDrainer.server.ts). --- // Tick gap when a tick drained nothing; under backlog ticks run back-to-back. TRIGGER_MOLLIFIER_DRAIN_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(100), // Cap on the drainer's exponential backoff after consecutive runOnce errors. TRIGGER_MOLLIFIER_DRAIN_MAX_BACKOFF_MS: z.coerce.number().int().positive().default(5_000), // Floor for the drainer's backoff base (so a tiny poll interval doesn't // collapse the backoff to near-zero during a sustained outage). TRIGGER_MOLLIFIER_DRAIN_BACKOFF_FLOOR_MS: z.coerce.number().int().positive().default(100), // Required margin between the drainer's shutdown deadline and // GRACEFUL_SHUTDOWN_TIMEOUT; boot fails loud if the timeout leaves less. TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_MARGIN_MS: z.coerce.number().int().positive().default(1_000), // How often the draining-tracker ZSET cardinality is polled into the gauge. TRIGGER_MOLLIFIER_DRAINING_GAUGE_INTERVAL_MS: z.coerce .number() .int() .positive() .default(15_000), // --- Pre-gate idempotency claim (idempotencyClaim.server.ts). --- // TTL on the claim key (and the upper clamp on the customer-derived // claim TTL), how long a waiter blocks before timing out, and the // waiter poll interval. TRIGGER_MOLLIFIER_CLAIM_TTL_SECONDS: z.coerce.number().int().positive().default(30), TRIGGER_MOLLIFIER_CLAIM_WAIT_MS: z.coerce.number().int().positive().default(5_000), TRIGGER_MOLLIFIER_CLAIM_POLL_MS: z.coerce.number().int().positive().default(25), // --- Buffered-run mutate-with-fallback wait loop (mutateWithFallback.server.ts). --- // Ceiling on the wait-for-materialisation loop, initial poll gap, the // backoff ceiling, and the exponential growth factor (a float). TRIGGER_MOLLIFIER_MUTATE_SAFETY_NET_MS: z.coerce.number().int().positive().default(2_000), TRIGGER_MOLLIFIER_MUTATE_POLL_STEP_MS: z.coerce.number().int().positive().default(20), TRIGGER_MOLLIFIER_MUTATE_MAX_POLL_STEP_MS: z.coerce.number().int().positive().default(250), TRIGGER_MOLLIFIER_MUTATE_BACKOFF_FACTOR: z.coerce.number().gt(1).default(1.7), // --- Buffered-run metadata CAS retry loop (applyMetadataMutation.server.ts). --- // Retry budget for concurrent metadata writers, and the jittered // conflict-backoff envelope: random in [0, base + attempt * step) ms. TRIGGER_MOLLIFIER_METADATA_MAX_RETRIES: z.coerce.number().int().positive().default(12), TRIGGER_MOLLIFIER_METADATA_BACKOFF_BASE_MS: z.coerce.number().int().positive().default(5), TRIGGER_MOLLIFIER_METADATA_BACKOFF_STEP_MS: z.coerce.number().int().positive().default(5), BATCH_TRIGGER_PROCESS_JOB_VISIBILITY_TIMEOUT_MS: z.coerce .number() .int() .default(60_000 * 5), // 5 minutes BATCH_TRIGGER_CACHED_RUNS_CHECK_ENABLED: BoolEnv.default(false), BATCH_TRIGGER_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), BATCH_TRIGGER_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), BATCH_TRIGGER_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), BATCH_TRIGGER_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), BATCH_TRIGGER_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), BATCH_TRIGGER_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), BATCH_TRIGGER_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), BATCH_TRIGGER_WORKER_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), BATCH_TRIGGER_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), BATCH_TRIGGER_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), BATCH_TRIGGER_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), BATCH_TRIGGER_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), BATCH_TRIGGER_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), BATCH_TRIGGER_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), BATCH_TRIGGER_WORKER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), BATCH_TRIGGER_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), // BatchQueue DRR settings (Run Engine v2) BATCH_QUEUE_DRR_QUANTUM: z.coerce.number().int().default(25), BATCH_QUEUE_MAX_DEFICIT: z.coerce.number().int().default(100), BATCH_QUEUE_CONSUMER_COUNT: z.coerce.number().int().default(3), BATCH_QUEUE_CONSUMER_INTERVAL_MS: z.coerce.number().int().default(50), BATCH_QUEUE_WORKER_ENABLED: BoolEnv.default(true), // Number of master queue shards for horizontal scaling BATCH_QUEUE_SHARD_COUNT: z.coerce.number().int().default(1), // Maximum queues to fetch from master queue per iteration BATCH_QUEUE_MASTER_QUEUE_LIMIT: z.coerce.number().int().default(1000), // Enable worker queue for two-stage processing (claim messages, push to worker queue, process from worker queue) BATCH_QUEUE_WORKER_QUEUE_ENABLED: BoolEnv.default(true), // Worker queue blocking timeout in seconds (for two-stage processing, only used when BATCH_QUEUE_WORKER_QUEUE_ENABLED is true) BATCH_QUEUE_WORKER_QUEUE_TIMEOUT_SECONDS: z.coerce.number().int().default(10), // Global rate limit: max items processed per second across all consumers // If not set, no global rate limiting is applied BATCH_QUEUE_GLOBAL_RATE_LIMIT: z.coerce.number().int().positive().optional(), // Max items in the worker queue before claiming pauses (protects visibility timeouts) // If not set, no depth limit is applied BATCH_QUEUE_WORKER_QUEUE_MAX_DEPTH: z.coerce.number().int().positive().optional(), ADMIN_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), ADMIN_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), ADMIN_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), ADMIN_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), ADMIN_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), ADMIN_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), ADMIN_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), ADMIN_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), ADMIN_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), ADMIN_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), ADMIN_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), ADMIN_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), ADMIN_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), ADMIN_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), ADMIN_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), ADMIN_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), ALERTS_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), ALERTS_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), ALERTS_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), ALERTS_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), ALERTS_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(100), ALERTS_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), ALERTS_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), ALERTS_WORKER_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), ALERTS_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), ALERTS_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), ALERTS_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), ALERTS_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), ALERTS_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), ALERTS_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), ALERTS_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"), ALERTS_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), BILLING_LIMIT_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), BILLING_LIMIT_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), BILLING_LIMIT_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), BILLING_LIMIT_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), BILLING_LIMIT_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), BILLING_LIMIT_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(20), BILLING_LIMIT_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(60_000), BILLING_LIMIT_WORKER_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), BILLING_LIMIT_RECONCILE_INTERVAL_MS: z.coerce.number().int().default(90_000), BILLING_LIMIT_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), BILLING_LIMIT_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), BILLING_LIMIT_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), BILLING_LIMIT_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), BILLING_LIMIT_WORKER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), SCHEDULE_ENGINE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), SCHEDULE_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), SCHEDULE_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), SCHEDULE_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), SCHEDULE_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), SCHEDULE_WORKER_IMMEDIATE_POLL_INTERVAL: z.coerce.number().int().default(50), SCHEDULE_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), SCHEDULE_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(30_000), SCHEDULE_WORKER_DISTRIBUTION_WINDOW_SECONDS: z.coerce.number().int().default(30), SCHEDULE_WORKER_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), SCHEDULE_WORKER_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), SCHEDULE_WORKER_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), SCHEDULE_WORKER_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), SCHEDULE_WORKER_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), SCHEDULE_WORKER_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), SCHEDULE_WORKER_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), SCHEDULE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), TASK_EVENT_PARTITIONING_ENABLED: z.string().default("0"), TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute DEPLOYMENTS_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000), BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(1_000), QUEUES_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000), SLACK_BOT_TOKEN: z.string().optional(), SLACK_SIGNUP_REASON_CHANNEL_ID: z.string().optional(), // kapa.ai KAPA_AI_WEBSITE_ID: z.string().optional(), // BetterStack BETTERSTACK_API_KEY: z.string().optional(), BETTERSTACK_STATUS_PAGE_ID: z.string().optional(), RUN_REPLICATION_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), RUN_REPLICATION_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), RUN_REPLICATION_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), RUN_REPLICATION_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), RUN_REPLICATION_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), RUN_REPLICATION_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), RUN_REPLICATION_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), RUN_REPLICATION_CLICKHOUSE_URL: z.string().optional(), RUN_REPLICATION_ENABLED: z.string().default("0"), RUN_REPLICATION_SLOT_NAME: z.string().default("task_runs_to_clickhouse_v1"), RUN_REPLICATION_PUBLICATION_NAME: z.string().default("task_runs_to_clickhouse_v1_publication"), RUN_REPLICATION_MAX_FLUSH_CONCURRENCY: z.coerce.number().int().default(2), RUN_REPLICATION_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), RUN_REPLICATION_FLUSH_BATCH_SIZE: z.coerce.number().int().default(100), RUN_REPLICATION_LEADER_LOCK_TIMEOUT_MS: z.coerce.number().int().default(30_000), RUN_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS: z.coerce.number().int().default(10_000), RUN_REPLICATION_ACK_INTERVAL_SECONDS: z.coerce.number().int().default(10), RUN_REPLICATION_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), RUN_REPLICATION_CLICKHOUSE_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), RUN_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS: z.coerce.number().int().default(10_000), RUN_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS: z.coerce.number().int().default(500), RUN_REPLICATION_WAIT_FOR_ASYNC_INSERT: z.string().default("0"), RUN_REPLICATION_KEEP_ALIVE_ENABLED: z.string().default("0"), RUN_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), RUN_REPLICATION_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), // Retry configuration for insert operations RUN_REPLICATION_INSERT_MAX_RETRIES: z.coerce.number().int().default(3), RUN_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), RUN_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), RUN_REPLICATION_INSERT_STRATEGY: z.enum(["insert", "insert_async"]).default("insert"), RUN_REPLICATION_DISABLE_PAYLOAD_INSERT: z.string().default("0"), RUN_REPLICATION_DISABLE_ERROR_FINGERPRINTING: z.string().default("0"), // --- Run-ops DB split — second replication source (the NEW dedicated run-ops DB). --- // Cloud-only; only consulted when isSplitEnabled() is true. Self-host never sets these. // Connection URL for the run-ops DB used by the runs-replication source. Required when the split is // enabled (unset → boot fails via SplitReplicationMisconfiguredError, no silent fallback). Kept // separate from the app's RUN_OPS_DATABASE_URL: replication can't run over a pooler, so this is direct. RUN_REPLICATION_RUN_OPS_DATABASE_URL: z .string() .refine(isValidDatabaseUrl, "RUN_REPLICATION_RUN_OPS_DATABASE_URL is invalid") .optional(), RUN_REPLICATION_NEW_SLOT_NAME: z.string().default("task_runs_to_clickhouse_v2"), RUN_REPLICATION_NEW_PUBLICATION_NAME: z .string() .default("task_runs_to_clickhouse_v2_publication"), RUN_REPLICATION_NEW_ENABLED: z.string().default("0"), // Origin generations packed into _version via composeTaskRunVersion. // Legacy DB = 0, new dedicated run-ops DB = 1. Exposed as env so the mapping is auditable // per-deploy, but DEFAULTS encode the canonical legacy=0 / new=1 contract. RUN_REPLICATION_LEGACY_ORIGIN_GENERATION: z.coerce.number().int().default(0), RUN_REPLICATION_NEW_ORIGIN_GENERATION: z.coerce.number().int().default(1), // Run-ops id mint cutover — per-env, canary-first, OFF by default. // Even when on, an env mints run-ops ids only if its per-org runOpsMintKind flag is // "runOpsId" AND isSplitEnabled() is true. Cache mirrors REALTIME_BACKEND_FLAG_CACHE_*. RUN_OPS_MINT_ENABLED: BoolEnv.default(false), RUN_OPS_MINT_FLAG_CACHE_TTL_MS: z.coerce.number().int().default(30_000), RUN_OPS_MINT_FLAG_CACHE_MAX_ENTRIES: z.coerce.number().int().default(10_000), // Session replication (Postgres → ClickHouse sessions_v1). Shares Redis // with the runs replicator for leader locking but has its own slot and // publication so the two consume independently. SESSION_REPLICATION_CLICKHOUSE_URL: z.string().optional(), SESSION_REPLICATION_ENABLED: z.string().default("0"), SESSION_REPLICATION_SLOT_NAME: z.string().default("sessions_to_clickhouse_v1"), SESSION_REPLICATION_PUBLICATION_NAME: z .string() .default("sessions_to_clickhouse_v1_publication"), SESSION_REPLICATION_MAX_FLUSH_CONCURRENCY: z.coerce.number().int().default(1), SESSION_REPLICATION_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), SESSION_REPLICATION_FLUSH_BATCH_SIZE: z.coerce.number().int().default(100), SESSION_REPLICATION_LEADER_LOCK_TIMEOUT_MS: z.coerce.number().int().default(30_000), SESSION_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS: z.coerce.number().int().default(10_000), SESSION_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS: z.coerce.number().int().default(10_000), SESSION_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS: z.coerce.number().int().default(500), SESSION_REPLICATION_ACK_INTERVAL_SECONDS: z.coerce.number().int().default(10), SESSION_REPLICATION_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), SESSION_REPLICATION_CLICKHOUSE_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), SESSION_REPLICATION_WAIT_FOR_ASYNC_INSERT: z.string().default("0"), SESSION_REPLICATION_KEEP_ALIVE_ENABLED: z.string().default("0"), SESSION_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), SESSION_REPLICATION_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), SESSION_REPLICATION_INSERT_STRATEGY: z.enum(["insert", "insert_async"]).default("insert"), SESSION_REPLICATION_INSERT_MAX_RETRIES: z.coerce.number().int().default(3), SESSION_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), SESSION_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), // Clickhouse CLICKHOUSE_URL: z.string(), CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), // Logs Query Settings CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE: z.coerce.number().int().default(1_000_000_000), CLICKHOUSE_LOGS_LIST_MAX_BYTES_BEFORE_EXTERNAL_SORT: z.coerce .number() .int() .default(256_000_000), CLICKHOUSE_LOGS_LIST_MAX_THREADS: z.coerce.number().int().default(2), CLICKHOUSE_LOGS_LIST_MAX_ROWS_TO_READ: z.coerce.number().int().default(10_000_000), CLICKHOUSE_LOGS_LIST_MAX_EXECUTION_TIME: z.coerce.number().int().default(120), // Bound read-in-order memory on object-storage reads: each part opens a per-column read // stream, and the default ~1 MiB+ S3 buffers dominate peak memory. These two byte sizes // cap the per-stream buffers and exist on every supported ClickHouse, so they are always on. CLICKHOUSE_LOGS_LIST_PREFETCH_BUFFER_SIZE: z.coerce .number() .int() .nonnegative() .default(262_144), CLICKHOUSE_LOGS_LIST_MAX_READ_BUFFER_SIZE: z.coerce .number() .int() .nonnegative() .default(262_144), // The decisive lever on Cloud SharedMergeTree, but it only exists on newer ClickHouse and // is a no-op on local-disk MergeTree, so it is opt-in: unset means it is never sent (safe on // any self-hosted version). Set to 0 on object-storage deployments to get the memory win. CLICKHOUSE_LOGS_LIST_FILESYSTEM_CACHE_PREFER_BIGGER_BUFFER_SIZE: z.coerce .number() .int() .nonnegative() .optional(), // Logs list pagination tuning (page sizing + recent-first probe windows). LOGS_LIST_DEFAULT_PAGE_SIZE: z.coerce.number().int().positive().default(50), LOGS_LIST_MAX_PAGE_SIZE: z.coerce.number().int().positive().default(100), // Days back from the page ceiling to probe before widening to the full requested window, // comma-separated. Empty disables narrowing (a single full-window query). LOGS_LIST_RECENT_FIRST_PROBE_DAYS: z .string() .default("1,7") .transform((s) => s .split(",") .map((v) => Number(v.trim())) .filter((n) => Number.isFinite(n) && n > 0) ), // Query feature flag QUERY_FEATURE_ENABLED: z.string().default("1"), // AI features (Prompts, Models, AI Metrics sidebar section) AI_FEATURES_ENABLED: z.string().default("0"), // Logs page ClickHouse URL (for logs queries) LOGS_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), // Query page ClickHouse limits (for TSQL queries) QUERY_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), QUERY_CLICKHOUSE_MAX_EXECUTION_TIME: z.coerce.number().int().default(10), QUERY_CLICKHOUSE_MAX_MEMORY_USAGE: z.coerce.number().int().default(1_073_741_824), // 1GB in bytes QUERY_CLICKHOUSE_MAX_AST_ELEMENTS: z.coerce.number().int().default(4_000_000), QUERY_CLICKHOUSE_MAX_EXPANDED_AST_ELEMENTS: z.coerce.number().int().default(4_000_000), QUERY_CLICKHOUSE_MAX_BYTES_BEFORE_EXTERNAL_GROUP_BY: z.coerce.number().int().default(0), QUERY_CLICKHOUSE_MAX_RETURNED_ROWS: z.coerce.number().int().default(10_000), // Query page concurrency limits QUERY_DEFAULT_ORG_CONCURRENCY_LIMIT: z.coerce.number().int().default(3), QUERY_GLOBAL_CONCURRENCY_LIMIT: z.coerce.number().int().default(100), // Metric widget concurrency limits METRIC_WIDGET_DEFAULT_ORG_CONCURRENCY_LIMIT: z.coerce.number().int().default(30), // Admin ClickHouse URL (for admin dashboard queries like missing models) ADMIN_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), EVENTS_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), EVENTS_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), EVENTS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), EVENTS_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), EVENTS_CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), EVENTS_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), // ClickHouse client used by @internal/run-engine's PendingVersionSystem. // Kept on its own URL + pool so this low-QPS path can't contend with // the main analytics client (CLICKHOUSE_URL). Falls back to the main // URL when unset so unconfigured environments still work. RUN_ENGINE_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), RUN_ENGINE_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), RUN_ENGINE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(), RUN_ENGINE_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(5), RUN_ENGINE_CLICKHOUSE_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), RUN_ENGINE_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), // Dedicated ClickHouse pool for the native backend's tag/batch id resolution; falls back to CLICKHOUSE_URL. REALTIME_BACKEND_NATIVE_CLICKHOUSE_URL: z .string() .optional() .transform((v) => v ?? process.env.CLICKHOUSE_URL), REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"), REALTIME_BACKEND_NATIVE_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce .number() .int() .optional(), REALTIME_BACKEND_NATIVE_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10), REALTIME_BACKEND_NATIVE_CLICKHOUSE_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), REALTIME_BACKEND_NATIVE_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"), EVENTS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(1000), EVENTS_CLICKHOUSE_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), METRICS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(10000), METRICS_CLICKHOUSE_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), METRICS_CLICKHOUSE_MAX_CONCURRENCY: z.coerce.number().int().default(3), EVENTS_CLICKHOUSE_INSERT_STRATEGY: z.enum(["insert", "insert_async"]).default("insert"), EVENTS_CLICKHOUSE_WAIT_FOR_ASYNC_INSERT: z.string().default("1"), EVENTS_CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: z.coerce.number().int().default(10485760), EVENTS_CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: z.coerce.number().int().default(5000), EVENTS_CLICKHOUSE_START_TIME_MAX_AGE_MS: z.coerce .number() .int() .default(60_000 * 5), // 5 minutes EVENT_REPOSITORY_DEFAULT_STORE: z .enum(["postgres", "clickhouse", "clickhouse_v2"]) .default("postgres"), EVENT_REPOSITORY_DEBUG_LOGS_DISABLED: BoolEnv.default(false), EVENTS_CLICKHOUSE_MAX_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000), EVENTS_CLICKHOUSE_MAX_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(5_000), EVENTS_CLICKHOUSE_MAX_LIVE_RELOADING_SETTING: z.coerce.number().int().default(2000), // OTLP ingest transform worker pool (opt-in). When enabled, decode/convert/enrich run in a // worker_threads pool instead of the request event loop; the single consolidated insert path // is unchanged. OTEL_TRANSFORM_WORKER_POOL_ENABLED: BoolEnv.default(false), OTEL_TRANSFORM_WORKER_POOL_SIZE: z.coerce.number().int().optional(), OTEL_TRANSFORM_WORKER_PATH: z.string().optional(), // Organization data stores registry ORGANIZATION_DATA_STORES_RELOAD_INTERVAL_MS: z.coerce .number() .int() .default(60 * 1000), // 1 minute // LLM cost tracking LLM_COST_TRACKING_ENABLED: BoolEnv.default(true), LLM_PRICING_RELOAD_INTERVAL_MS: z.coerce .number() .int() .default(5 * 60 * 1000), // 5 minutes LLM_PRICING_RELOAD_CHANNEL: z.string().default("llm-registry:reload"), LLM_PRICING_RELOAD_DEBOUNCE_MS: z.coerce.number().int().default(1000), // Whether to subscribe this process to the LLM_PRICING_RELOAD_CHANNEL. // Default off — only OTel-ingesting services need real-time pricing // freshness; dashboard/worker processes are fine on the existing // 5-minute periodic reload. In multi-service deployments, set this to // true on the span-ingesting services. LLM_PRICING_RELOAD_PUBSUB_ENABLED: BoolEnv.default(false), LLM_PRICING_SEED_ON_STARTUP: BoolEnv.default(false), LLM_PRICING_READY_TIMEOUT_MS: z.coerce.number().int().default(500), LLM_METRICS_BATCH_SIZE: z.coerce.number().int().default(5000), LLM_METRICS_FLUSH_INTERVAL_MS: z.coerce.number().int().default(2000), LLM_METRICS_MAX_BATCH_SIZE: z.coerce.number().int().default(10000), LLM_METRICS_MAX_CONCURRENCY: z.coerce.number().int().default(2), // Bootstrap TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"), TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(), TRIGGER_BOOTSTRAP_WORKER_TOKEN_PATH: z.string().optional(), // Machine presets MACHINE_PRESETS_OVERRIDE_PATH: z.string().optional(), // CLI package tag (e.g. "latest", "v4-beta", "4.0.0") - used for setup commands TRIGGER_CLI_TAG: z.string().default("latest"), HEALTHCHECK_DATABASE_DISABLED: z.string().default("0"), REQUEST_IDEMPOTENCY_REDIS_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_HOST), REQUEST_IDEMPOTENCY_REDIS_READER_HOST: z .string() .optional() .transform((v) => v ?? process.env.REDIS_READER_HOST), REQUEST_IDEMPOTENCY_REDIS_READER_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_READER_PORT ? parseInt(process.env.REDIS_READER_PORT) : undefined) ), REQUEST_IDEMPOTENCY_REDIS_PORT: z.coerce .number() .optional() .transform( (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) ), REQUEST_IDEMPOTENCY_REDIS_USERNAME: z .string() .optional() .transform((v) => v ?? process.env.REDIS_USERNAME), REQUEST_IDEMPOTENCY_REDIS_PASSWORD: z .string() .optional() .transform((v) => v ?? process.env.REDIS_PASSWORD), REQUEST_IDEMPOTENCY_REDIS_TLS_DISABLED: z .string() .default(process.env.REDIS_TLS_DISABLED ?? "false"), REQUEST_IDEMPOTENCY_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), REQUEST_IDEMPOTENCY_TTL_IN_MS: z.coerce .number() .int() .default(60_000 * 60 * 24), // Bulk action BULK_ACTION_BATCH_SIZE: z.coerce.number().int().default(100), BULK_ACTION_BATCH_DELAY_MS: z.coerce.number().int().default(200), BULK_ACTION_SUBBATCH_CONCURRENCY: z.coerce.number().int().default(5), /// Max number of concurrent in-flight (PENDING) bulk replays per environment. BULK_ACTION_MAX_CONCURRENT_REPLAYS: z.coerce.number().int().default(3), /// Max number of explicit run IDs accepted in a single bulk action create request. BULK_ACTION_MAX_RUN_IDS: z.coerce.number().int().default(500), // AI Run Filter AI_RUN_FILTER_MODEL: z.string().optional(), EVENT_LOOP_MONITOR_THRESHOLD_MS: z.coerce.number().int().default(100), EVENT_LOOP_MONITOR_UTILIZATION_INTERVAL_MS: z.coerce.number().int().default(1000), EVENT_LOOP_MONITOR_UTILIZATION_SAMPLE_RATE: z.coerce.number().default(0.05), EVENT_LOOP_MONITOR_NOTIFY_ENABLED: z.string().default("0"), VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(), REALTIME_STREAMS_S2_BASIN: z.string().optional(), REALTIME_STREAMS_S2_ACCESS_TOKEN: z.string().optional(), REALTIME_STREAMS_S2_ENDPOINT: z.string().optional(), REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS: z.enum(["true", "false"]).default("false"), REALTIME_STREAMS_S2_ACCESS_TOKEN_EXPIRATION_IN_MS: z.coerce .number() .int() .default(60_000 * 60 * 24), // 1 day REALTIME_STREAMS_S2_LOG_LEVEL: z .enum(["log", "error", "warn", "info", "debug"]) .default("info"), REALTIME_STREAMS_S2_FLUSH_INTERVAL_MS: z.coerce.number().int().default(100), REALTIME_STREAMS_S2_MAX_RETRIES: z.coerce.number().int().default(10), REALTIME_STREAMS_S2_WAIT_SECONDS: z.coerce.number().int().default(60), // When "true", provision a dedicated S2 basin per org and stamp // `streamBasinName` on new rows. Off keeps everything on the single // basin defined by `REALTIME_STREAMS_S2_BASIN`. REALTIME_STREAMS_PER_ORG_BASINS_ENABLED: z.enum(["true", "false"]).default("false"), // Per-org basin name = `{prefix}-{env}-org-{orgId}`. REALTIME_STREAMS_BASIN_NAME_PREFIX: z.string().default("triggerdotdev"), REALTIME_STREAMS_BASIN_NAME_ENV: z.string().default("dev"), REALTIME_STREAMS_BASIN_DEFAULT_RETENTION: durationString().default("30d"), REALTIME_STREAMS_BASIN_STORAGE_CLASS: z.enum(["express", "standard"]).default("express"), REALTIME_STREAMS_BASIN_DELETE_ON_EMPTY_MIN_AGE: durationString().default("1h"), REALTIME_STREAMS_DEFAULT_VERSION: z.enum(["v1", "v2"]).default("v1"), WAIT_UNTIL_TIMEOUT_MS: z.coerce.number().int().default(600_000), // Private connections PRIVATE_CONNECTIONS_ENABLED: z.string().optional(), PRIVATE_CONNECTIONS_AWS_ACCOUNT_IDS: z.string().optional(), // Force RBAC to not use the plugin RBAC_FORCE_FALLBACK: BoolEnv.default(false), // Per-process pool sizes for an RBAC plugin that owns its own database // client (the fallback queries through Prisma and ignores these). Writes // are rare role mutations; reads run on the per-request auth hot path. RBAC_DATABASE_WRITER_CONNECTION_LIMIT: z.coerce.number().int().default(2), RBAC_DATABASE_READER_CONNECTION_LIMIT: z.coerce.number().int().default(5), // Force SSO to not use the plugin (contributors without the cloud // plugin installed can opt in to a clean OSS-only experience). SSO_FORCE_FALLBACK: BoolEnv.default(false), // Per-process pool sizes for an SSO plugin that owns its own database // client (the fallback queries through Prisma and ignores these). Writes // are rare config mutations and webhook processing; reads run on the // login path. SSO_DATABASE_WRITER_CONNECTION_LIMIT: z.coerce.number().int().default(2), SSO_DATABASE_READER_CONNECTION_LIMIT: z.coerce.number().int().default(5), // Emit a console.log when the SSO fallback is selected because no // plugin is installed. Default off so OSS deployments stay quiet. SSO_LOG_FALLBACK: BoolEnv.default(false), // Master deploy gate for the whole SSO feature. Default OFF so the // image can ship dark and be flipped on only once the SSO plugin's // backing services are available. When false, the SSO controller is // forced to the OSS fallback — login link hidden, SSO login disabled, // settings inert, and session re-validation skipped. SSO_ENABLED: BoolEnv.default(false), // How often (seconds) a live SSO session is re-validated against the // identity provider. The check is single-flight per user, so this is // the minimum interval between plugin round-trips, not a per-request // cost. Defaults to 5 minutes: every active SSO user drives one // billing→IdP round-trip per window, so a seconds-scale default // exhausts vendor rate limits at trivial user counts (masked by // fail-open, so it degrades silently). SSO_SESSION_REVALIDATION_INTERVAL_SECONDS: z.coerce.number().int().positive().default(300), // Hard timeout (ms) on the re-validation round-trip. If the SSO plugin // doesn't answer within this window the check fails OPEN (session kept) // and emits a `sso.revalidation.timeout` warn log — alert on an // elevated rate of those to catch a slow/unhealthy SSO dependency. SSO_SESSION_REVALIDATION_TIMEOUT_MS: z.coerce.number().int().positive().default(2000), }) .and(GithubAppEnvSchema) .and(S2EnvSchema) .superRefine((env, ctx) => { const presets = new Set(env.COMPUTE_TEMPLATE_MACHINE_PRESETS); for (const required of env.COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED) { if (!presets.has(required)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["COMPUTE_TEMPLATE_MACHINE_PRESETS_REQUIRED"], message: `"${required}" is not in COMPUTE_TEMPLATE_MACHINE_PRESETS`, }); } } }); export type Environment = z.infer; export const env = EnvironmentSchema.parse(process.env);