diff --git a/apps/api/src/lib/config.ts b/apps/api/src/lib/config.ts index daa7fc3..3d77d25 100644 --- a/apps/api/src/lib/config.ts +++ b/apps/api/src/lib/config.ts @@ -8,6 +8,24 @@ */ import { z } from 'zod'; +/** + * Parse a boolean from an environment variable. + * + * NOT `z.coerce.boolean()`, which calls `Boolean(value)` — so the string + * `"false"` becomes `true`, along with `"0"`, `"no"` and `"off"`. Every + * feature flag set to `false` would silently be on, which is exactly the + * class of bug that gets discovered in production by reading a startup + * warning that should not have appeared. + */ +const envBoolean = (defaultValue: boolean) => + z + .string() + .optional() + .transform((value) => { + if (value == null || value.trim() === '') return defaultValue; + return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase()); + }); + const schema = z.object({ DATABASE_URL: z.string().min(1, 'DATABASE_URL is required.'), @@ -24,10 +42,10 @@ const schema = z.object({ PRIME_API_KEY: z.string().optional(), PRIME_API_BASE: z.string().default('https://api.primeintellect.ai'), - PRIME_SYNC_ENABLED: z.coerce.boolean().default(false), + PRIME_SYNC_ENABLED: envBoolean(false), PRIME_SYNC_INTERVAL_MINUTES: z.coerce.number().int().positive().default(30), - PIGGY_ENABLED: z.coerce.boolean().default(false), + PIGGY_ENABLED: envBoolean(false), ANTHROPIC_API_KEY: z.string().optional(), PIGGY_MODEL: z.string().default('claude-sonnet-5'), PIGGY_LEASE_SECONDS: z.coerce.number().int().positive().default(300),