Deploy-Time Config Validation Doesn't Skip Dead Branches
Deploy-time config validation doesn't skip dead branches
A platform that statically validates a config file at deploy or build time will demand every environment variable the file references, even the ones inside a branch that can never run. "Gated off" and "absent" are the same thing to a static checker. The escape hatch is to set the var to an empty string: present enough to pass validation, falsy enough to stay disabled.
How this bit
The lcos CLI added a second Convex auth provider, and I wanted it off by default until the keys were set. So I gated it the obvious way:
providers: [
clerkProvider,
...(process.env.LCOS_CLI_JWT_ISSUER
? [cliJwtProvider] // only when the issuer is configured
: []),
]
The intent reads fine: unset issuer, no second provider. But Convex validates auth.config.ts at push time, and it reads every process.env reference in the file regardless of which branch they live in. With the issuer unset, convex deploy hard-failed: "Environment variable LCOS_CLI_JWT_ISSUER is used in auth config file but its value was not set." Worse, it cascaded — codegen never regenerated, so the whole generated API started throwing type errors that looked unrelated.
The fix
Set the var to an empty string. bunx convex env set LCOS_CLI_JWT_ISSUER "". Now the validator sees it as "set" and passes, while the ternary still reads it as falsy and leaves the provider out. To turn the feature on later, overwrite the empty string with the real issuer URL. Nothing about the gating logic changes.
Validation against code (2026-06-26)
The conditional spread is exactly as above in packages/backend/convex/auth.config.ts. The empty-string workaround is what's deployed on dev and prod, which is why the provider is present in the config but inert.
Why this generalises
This isn't a Convex quirk. Any tool that statically validates configuration before running it will walk dead branches: Terraform variable validation, CI schema linters, build-time env assertions, framework config loaders. A reachability check is a runtime concept; a static validator doesn't have one. When you gate a feature behind an env var and the platform validates config eagerly, expect to feed the validator a harmless empty value rather than nothing.
The broader lesson is small but sharp: a dead branch is still parsed. Don't assume the compiler or the deploy step shares your sense of what code can run.
See also
- Model Catalogs Need Runtime Smoke Tests — the runtime-availability sibling: config that passes static checks still needs a live probe
- Runtime Identity Is a Data Boundary — the other place env resolution decides behaviour
- Lcos