Describe the bug
When using @sveltejs/adapter-cloudflare with a wrangler.json file, environment variables defined under vars are only available via platform.env during local development. They are not bridged into process.env, which means $env/dynamic/private cannot see them.
This creates a split where:
vars in wrangler.json → available via platform.env (local + production), invisible to $env/dynamic/private during dev
- values in
.env → available via $env/dynamic/private during dev (Vite loads them into process.env), not available at runtime on Cloudflare
In production on Cloudflare, $env/dynamic/private correctly resolves both vars and secrets from the Worker runtime. The inconsistency is local-dev only, but it forces developers into one of these workarounds:
- Duplicate every var in both
wrangler.json and .env, keeping them manually in sync.
- Abandon
$env/dynamic/private and access everything through event.platform.env, losing the ergonomics of SvelteKit's env module and requiring null-safety guards since platform can be undefined.
- Write a custom Vite plugin that reads
wrangler.json and injects vars into process.env at dev time.
None of these are obvious, and developers hit this wall only after migrating away from $env/static/private (which is the recommended move on Cloudflare Pages — see context below).
Context: why developers end up here
On Cloudflare Pages, adding a wrangler.json to your project causes the dashboard-defined plaintext environment variables to no longer be injected into the build container's process.env. This means $env/static/private breaks at build time for any variable not defined as an encrypted secret.
The natural fix is to:
- Move plaintext config into
wrangler.json vars (the intended source of truth for runtime config)
- Switch from
$env/static/private to $env/dynamic/private (since vars are now runtime bindings)
- Keep secrets in
.env / .dev.vars for local dev and in the Cloudflare dashboard for production
This is the correct architecture — build once, resolve at runtime. But step 2 silently breaks local dev because the adapter doesn't bridge wrangler.json vars into the env system that $env/dynamic/private reads from.
Suggested approach
In the adapter's Vite plugin (or dev hook), read the resolved wrangler config and merge vars into process.env with lower precedence than existing values:
This keeps wrangler.json as the single source of truth for runtime config, .env as the source for local secrets, and $env/dynamic/private works everywhere without duplication or custom plugins.
Reproduction
- Create a SvelteKit project with
@sveltejs/adapter-cloudflare
- Define a var in
wrangler.json:
{
"name": "my-app",
"pages_build_output_dir": ".svelte-kit/cloudflare",
"vars": {
"API_HOST": "https://api.example.com"
}
}
- In a server endpoint or hook, import and log it:
import { env } from '$env/dynamic/private';
console.log(env.API_HOST); // undefined during `vite dev`
- Run
npm run dev — env.API_HOST is undefined
- Add
API_HOST=https://api.example.com to .env — now it works, but you have the value in two places
Expected behavior
During local dev, $env/dynamic/private should include variables defined in wrangler.json vars. The adapter already reads the wrangler config (via platformProxy.configPath) to populate platform.env — it should also inject those values into process.env so that SvelteKit's own env module works consistently.
Values from .env / .dev.vars should take precedence over wrangler.json vars to allow local secret overrides.
Logs
System Info
@sveltejs/adapter-cloudflare 7.2.8
Severity
annoyance
Additional Information
No response
Describe the bug
When using
@sveltejs/adapter-cloudflarewith awrangler.jsonfile, environment variables defined undervarsare only available viaplatform.envduring local development. They are not bridged intoprocess.env, which means$env/dynamic/privatecannot see them.This creates a split where:
varsinwrangler.json→ available viaplatform.env(local + production), invisible to$env/dynamic/privateduring dev.env→ available via$env/dynamic/privateduring dev (Vite loads them intoprocess.env), not available at runtime on CloudflareIn production on Cloudflare,
$env/dynamic/privatecorrectly resolves bothvarsand secrets from the Worker runtime. The inconsistency is local-dev only, but it forces developers into one of these workarounds:wrangler.jsonand.env, keeping them manually in sync.$env/dynamic/privateand access everything throughevent.platform.env, losing the ergonomics of SvelteKit's env module and requiring null-safety guards sinceplatformcan be undefined.wrangler.jsonand injectsvarsintoprocess.envat dev time.None of these are obvious, and developers hit this wall only after migrating away from
$env/static/private(which is the recommended move on Cloudflare Pages — see context below).Context: why developers end up here
On Cloudflare Pages, adding a
wrangler.jsonto your project causes the dashboard-defined plaintext environment variables to no longer be injected into the build container'sprocess.env. This means$env/static/privatebreaks at build time for any variable not defined as an encrypted secret.The natural fix is to:
wrangler.jsonvars(the intended source of truth for runtime config)$env/static/privateto$env/dynamic/private(since vars are now runtime bindings).env/.dev.varsfor local dev and in the Cloudflare dashboard for productionThis is the correct architecture — build once, resolve at runtime. But step 2 silently breaks local dev because the adapter doesn't bridge wrangler.json vars into the env system that
$env/dynamic/privatereads from.Suggested approach
In the adapter's Vite plugin (or dev hook), read the resolved wrangler config and merge
varsintoprocess.envwith lower precedence than existing values:This keeps
wrangler.jsonas the single source of truth for runtime config,.envas the source for local secrets, and$env/dynamic/privateworks everywhere without duplication or custom plugins.Reproduction
@sveltejs/adapter-cloudflarewrangler.json:{ "name": "my-app", "pages_build_output_dir": ".svelte-kit/cloudflare", "vars": { "API_HOST": "https://api.example.com" } }npm run dev—env.API_HOSTisundefinedAPI_HOST=https://api.example.comto.env— now it works, but you have the value in two placesExpected behavior
During local dev,
$env/dynamic/privateshould include variables defined inwrangler.jsonvars. The adapter already reads the wrangler config (viaplatformProxy.configPath) to populateplatform.env— it should also inject those values intoprocess.envso that SvelteKit's own env module works consistently.Values from
.env/.dev.varsshould take precedence overwrangler.jsonvarsto allow local secret overrides.Logs
System Info
Severity
annoyance
Additional Information
No response