Skip to content

fix(config): interpolate env() refs before schema decode#5341

Merged
Coly010 merged 1 commit into
developfrom
cli/cli-1489-env-pre-decode
May 22, 2026
Merged

fix(config): interpolate env() refs before schema decode#5341
Coly010 merged 1 commit into
developfrom
cli/cli-1489-env-pre-decode

Conversation

@Coly010
Copy link
Copy Markdown
Contributor

@Coly010 Coly010 commented May 22, 2026

Fixes the crash when supabase/config.toml uses env(VAR) on numeric or boolean fields (e.g. analytics.port = "env(SUPABASE_ANALYTICS_PORT)"). The strict Effect Schema decode ran immediately after raw TOML parse, with interpolateValue in project.ts only firing post-decode via resolveProjectValue — so it never got the chance to substitute the string before Schema.Number rejected it.

What changed

  • Pre-decode env() interpolation in packages/config/src/io.tsloadProjectConfigFile now loads the project environment (.env/.env.local/ambient) and runs a schema-aware walker on the parsed document before handing it to Schema.decodeUnknownSync(ProjectConfigSchema).
  • Schema-aware walker in packages/config/src/lib/env.ts — traverses both the parsed document and ProjectConfigSchema.ast in parallel. For string leaves matching env(VAR): substitutes against the env, then coerces to Number/Boolean if the schema at that path expects one. Mirrors Go's LoadEnvHook + mapstructure type chain (apps/cli-go/pkg/config/decode_hooks.go:14-21 → subsequent string→type conversion hooks).
  • Verbatim-on-missing semanticsinterpolateLeafValue in project.ts no longer throws MissingProjectEnvVarError when the referenced env var is unset. It returns the literal env(VAR) string, matching Go parity. The MissingProjectEnvVarError class and re-export are removed; resolveProjectValue / resolveProjectSubtree no longer have a failure channel.
  • Fields declared with the env() schema helper opt out via the x-env-deferred marker annotation. They still require the literal env(VAR) format for post-decode resolution by resolveProjectValue — the walker honors the marker and leaves those paths untouched.

Reviewer-relevant context

  • No schema-file edits. Coercion lives entirely in the walker, so future fields added to any of the section schemas (db.ts, analytics.ts, auth/*.ts, etc.) automatically work with env() references — no risk of a contributor forgetting to use a coerced primitive at declaration time.
  • The marker annotation lives on the check, not the outer AST. env() = Schema.String.check(isPattern(envRegex)).annotate({...}) attaches metadata to the resulting Filter rather than the base String AST, so isDeferredEnvField inspects both node.annotations and node.checks[].annotations. Caught by the @supabase/stack functions.unit.test.ts regression for functions.<name>.env (env() helper at a record value position).
  • Missing-env semantics in project.ts are now non-failing. Two existing "fails when missing env var" tests in project.unit.test.ts:223,255 are rewritten to assert verbatim preservation. redactValue already skips redaction when the value is still an env reference (!isEnvReference(value)), so unresolved literals flow through as plain strings — no Redacted wrapping on missing secrets.
  • The next/projectContextLayer workaround (PR refactor(cli): remove rawProjectConfig from ProjectContext #5281) is left in place. That layer dropped the loadProjectConfig call entirely to avoid the crash; reintroducing it is a follow-up refactor since the workaround still functions correctly (it only loads env, not config).
  • The existing CLI-1489 regression test at apps/cli/src/next/config/project-context.layer.unit.test.ts:30 is not modified — it asserts the workaround-era behavior of projectContextLayer, which is unchanged. Direct upstream regression coverage is added in packages/config/src/io.unit.test.ts: numeric coercion, boolean coercion, verbatim preservation, ambient fallback, and decode failure when an unset var is referenced from a numeric field.

Fixes CLI-1489

`env(VAR)` in `supabase/config.toml` numeric/boolean fields crashed
`loadProjectConfig` with `ProjectConfigParseError: Expected number`
because the strict Effect Schema decode ran immediately after raw TOML
parse — `interpolateValue` in `project.ts` exists but only fires
post-decode via `resolveProjectValue`, so it never got the chance to
substitute `"env(SUPABASE_ANALYTICS_PORT)"` on `analytics.port`.

`loadProjectConfigFile` now loads the project environment and runs a
schema-aware pre-decode walker that traverses the parsed document and
`ProjectConfigSchema.ast` in parallel:

- Substitutes `env(VAR)` string leaves against `.env` / `.env.local` /
  ambient env.
- For unset vars, preserves the literal verbatim — matches Go's
  `apps/cli-go/pkg/config/decode_hooks.go:14-21` (LoadEnvHook).
- After substitution, if the schema at that path expects Number or
  Boolean, coerces the substituted string to the primitive — mirroring
  Go's mapstructure chain where LoadEnvHook returns a string and
  subsequent hooks convert it to the target type.
- Fields declared with the `env()` helper opt out via a marker
  annotation; they continue to flow through post-decode resolution.

`interpolateLeafValue` in `project.ts` now returns the literal verbatim
when the referenced env var is missing instead of throwing
`MissingProjectEnvVarError` (also Go parity). The error class is
removed; `resolveProjectValue` / `resolveProjectSubtree` no longer have
a failure channel.

No schema-file edits — coercion lives in the walker, so future schemas
that add numeric/boolean fields automatically work with env() refs.

Fixes CLI-1489
@Coly010 Coly010 requested a review from a team as a code owner May 22, 2026 15:45
@Coly010 Coly010 self-assigned this May 22, 2026
@Coly010 Coly010 merged commit d9f31cc into develop May 22, 2026
15 of 17 checks passed
@Coly010 Coly010 deleted the cli/cli-1489-env-pre-decode branch May 22, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants