From 46d7d80f1b74dad59d1c00cd6ef91acaa1482ecb Mon Sep 17 00:00:00 2001 From: karti Date: Wed, 12 Aug 2026 19:46:21 -0700 Subject: [PATCH] =?UTF-8?q?Fix=20boolean=20env=20parsing=20=E2=80=94=20PRI?= =?UTF-8?q?ME=5FSYNC=5FENABLED=3Dfalse=20was=20reading=20as=20true?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit z.coerce.boolean() calls Boolean(value), so the string "false" is true. So are "0", "no" and "off". Every feature flag set to false was silently on. Caught by reading a startup warning that should not have been there: the deployment logged "PRIME_SYNC_ENABLED is on but PRIME_API_KEY is unset" while the .env plainly said false. Had the key been present, PIG would have started polling a third-party API nobody asked it to poll. Replaced with an explicit parser accepting 1/true/yes/on, treating an empty or absent value as the default. Demonstrated both behaviours side by side before committing. Co-Authored-By: Claude Opus 5 (1M context) --- apps/api/src/lib/config.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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),