Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
031fda6
first draft
Rich-Harris May 30, 2026
455baaa
evaluate env file
Rich-Harris May 30, 2026
4a1e2bf
tweak
Rich-Harris May 30, 2026
ff05b96
warn if using src/env without the experimental flag
Rich-Harris May 30, 2026
3c75dcf
regenerate
Rich-Harris May 30, 2026
f46f724
adjust test
Rich-Harris May 30, 2026
70cb5b4
fix
Rich-Harris May 30, 2026
d4a527e
fix
Rich-Harris May 30, 2026
f0ec4ad
fix
Rich-Harris May 30, 2026
415a4e4
fix
Rich-Harris May 30, 2026
f108206
unused
Rich-Harris May 30, 2026
cc75201
unused
Rich-Harris May 30, 2026
d2b2eb0
revert
Rich-Harris May 30, 2026
36b62ff
revert
Rich-Harris May 30, 2026
4dc66a5
fix
Rich-Harris May 30, 2026
069ec47
fixes
Rich-Harris May 30, 2026
5b8a007
tweak
Rich-Harris May 30, 2026
c93fb59
fix
Rich-Harris May 30, 2026
6bfa006
add a TODO
Rich-Harris May 30, 2026
ae092dc
fix
Rich-Harris May 30, 2026
f902326
temp
Rich-Harris May 30, 2026
fecf3ce
revert
Rich-Harris May 30, 2026
84e0110
revert
Rich-Harris May 30, 2026
0956317
add TODO
Rich-Harris May 30, 2026
d4a590c
fix
Rich-Harris May 31, 2026
f1e28f3
fix
Rich-Harris May 31, 2026
e979fab
fix
Rich-Harris May 31, 2026
74ea8da
unused
Rich-Harris May 31, 2026
4b39d5e
use dev server to load src/env
Rich-Harris May 31, 2026
3ba0181
simplify
Rich-Harris May 31, 2026
7731ae3
more
Rich-Harris May 31, 2026
2b25c57
generics
Rich-Harris May 31, 2026
0c5b63b
fix
Rich-Harris May 31, 2026
22f3d99
changeset
Rich-Harris May 31, 2026
136dd9b
lol we dont need this any more
Rich-Harris May 31, 2026
5d48d5a
oops
Rich-Harris May 31, 2026
84d5f38
prettier
Rich-Harris May 31, 2026
2b12cac
fix
Rich-Harris May 31, 2026
d5ced50
prettier
Rich-Harris May 31, 2026
cec84e1
remove default property, we decided against that
Rich-Harris May 31, 2026
57a4614
fix
Rich-Harris May 31, 2026
30e4211
make client-side dead-code-elimination work
Rich-Harris May 31, 2026
16438f9
omit static vars from rendered env
Rich-Harris May 31, 2026
f3306bd
update docs
Rich-Harris May 31, 2026
34f946c
fix
Rich-Harris May 31, 2026
db79793
fix generateEnvModule
Rich-Harris May 31, 2026
de517ff
lint
Rich-Harris May 31, 2026
6a7a0eb
testing a hypothesis
Rich-Harris May 31, 2026
0a12ee1
gah
Rich-Harris May 31, 2026
0c9203e
what the actual hell
Rich-Harris May 31, 2026
01ae2cc
please continue to work
Rich-Harris May 31, 2026
e5dbb29
docs
Rich-Harris May 31, 2026
1f78e97
fix
Rich-Harris May 31, 2026
3087c4c
tweak
Rich-Harris May 31, 2026
b0b6008
more tweakage
Rich-Harris May 31, 2026
8f6133a
oops revert
Rich-Harris May 31, 2026
e6cb05d
fix
Rich-Harris May 31, 2026
dd68c21
Update packages/kit/src/exports/internal/env.js
teemingc Jun 1, 2026
2c3eab3
Update documentation/docs/10-getting-started/30-project-structure.md
dummdidumm Jun 1, 2026
440cf48
don't create /... modules if explicit env vars are enabled
Rich-Harris Jun 1, 2026
6c1deb5
add note
Rich-Harris Jun 1, 2026
110d1f2
validate everything at once
Rich-Harris Jun 1, 2026
de5fe51
Update packages/kit/src/exports/internal/env.js
Rich-Harris Jun 1, 2026
15dd8f5
fix
Rich-Harris Jun 1, 2026
54d7b95
Merge branch 'explicit-env-vars' of github.com:sveltejs/kit into expl…
Rich-Harris Jun 1, 2026
eb8bb9f
hide stack trace
Rich-Harris Jun 1, 2026
e77c40a
make server-side treeshaking work
Rich-Harris Jun 1, 2026
7adef36
looks like we can revert this
Rich-Harris Jun 1, 2026
70dd946
validate -> schema
Rich-Harris Jun 1, 2026
1278393
regenerate
Rich-Harris Jun 2, 2026
09f2282
Update packages/kit/src/core/env.js
Rich-Harris Jun 2, 2026
392a553
allow $app/env, $lib, and user aliases in src/env
Rich-Harris Jun 2, 2026
dd1b716
oops need mocks
Rich-Harris Jun 2, 2026
403b5fc
unused
Rich-Harris Jun 2, 2026
1702214
fix
Rich-Harris Jun 2, 2026
a1692f3
small bit of polish
Rich-Harris Jun 2, 2026
ab0db74
merge main
Rich-Harris Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-dolls-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: explicit env vars
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The `src` directory contains the meat of your project. Everything except `src/ro
- `%sveltekit.body%` — the markup for a rendered page. This should live inside a `<div>` or other element, rather than directly inside `<body>`, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process. SvelteKit will warn you in development if this is not the case
- `%sveltekit.assets%` — either [`paths.assets`](configuration#paths), if specified, or a relative path to [`paths.base`](configuration#paths)
- `%sveltekit.nonce%` — a [CSP](configuration#csp) nonce for manually included links and scripts, if used
- `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](configuration#env) (usually `PUBLIC_`). It will fallback to `''` if not matched.
- `%sveltekit.env.[NAME]%` - this will be replaced at render time with the `[NAME]` environment variable, which must begin with the [`publicPrefix`](configuration#env) (usually `PUBLIC_`), or be defined as a public variable in `src/env` if using [`experimental.explicitEnvironmentVariables`](environment-variables). It will fallback to `''` if not matched.
- `%sveltekit.version%` — the app version, which can be specified with the [`version`](configuration#version) configuration
- `error.html` is the page that is rendered when everything else fails. It can contain the following placeholders:
- `%sveltekit.status%` — the HTTP status
Expand Down
205 changes: 205 additions & 0 deletions documentation/docs/20-core-concepts/70-environment-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Environment variables
---

Environment variables are values your app needs that exist separately from the app's source code. They allow you to use sensitive information like API keys and database credentials without storing them in version control.

During development, and at build time, variables defined in a `.env` or `.env.local` file will be added to the environment:

```env
API_KEY=19f401ba-e8b0-48c4-8c77-b0ebb26d97fe
```

By default, every environment variable is implicitly available inside your app via the following modules:

- [`$env/static/private`]($env-static-private)
- [`$env/static/public`]($env-static-public)
- [`$env/dynamic/private`]($env-dynamic-private)
- [`$env/dynamic/public`]($env-dynamic-public)

## Explicit environment variables

As of SvelteKit 2.62, you can opt into _explicit_ environment variables, in which case you instead import environment variables from these modules:

- [`$app/env/private`]($app-env-private)
- [`$app/env/public`]($app-env-public)

Additionally, the [`$app/environment`]($app-environment) module is renamed to [`$app/env`]($app-env).

> [!NOTE] Explicit environment variables will become the default in SvelteKit 3. The `$env/*` modules, along with `$app/environment`, will be removed.

### Setup

To opt in, update your configuration...

```js
/// file: svelte.config.js
export default {
kit: {
experimental: {
+++explicitEnvironmentVariables: true+++
}
}
};
```

...and add a `src/env.ts` (or `src/env.js`) file that exports a `variables` object:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';

export const variables = defineEnvVars({
// ...
});
```

Each value in the object passed to [`defineEnvVars`](@sveltejs-kit-hooks#defineEnvVars) is an [`EnvVarConfig`](@sveltejs-kit#EnvVarConfig) object that configures the environment variable.
Comment thread
Rich-Harris marked this conversation as resolved.

> [!NOTE] `defineEnvVars` returns its argument unaltered — it exists purely to help with type safety.

### Private variables

By default, all variables are considered private. For example, you don't want to reveal your `API_KEY`:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';

export const variables = defineEnvVars({
+++API_KEY: {}+++
});
```

> [!NOTE] Since no configuration is needed for this variable, we can use an empty object (`{}`).

Now that `API_KEY` is defined, it can be imported into app code via `$app/env/private`:

```js
import { API_KEY } from '$app/env/private';
```

The `$app/env/private` module cannot be imported into code that runs in the browser, so that you can't accidentally reveal your secrets in a JavaScript bundle.

### Public variables

Some variables are perfectly safe — necessary, even — to expose to the browser. For these, we can specify `public: true`:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';

export const variables = defineEnvVars({
GOOGLE_ANALYTICS_ID: {
+++public: true+++
}
});
```

`GOOGLE_ANALYTICS_ID` can now be imported from `$app/env/public`, or used in your `app.html` template as `%sveltekit.env.GOOGLE_ANALYTICS_ID%`:

```html
<!--- file: src/app.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%

+++ <script async src="https://www.googletagmanager.com/gtag/js?id=%sveltekit.env.GOOGLE_ANALYTICS_ID%"></script>
<script>
window.dataLayer ??= [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '%sveltekit.env.GOOGLE_ANALYTICS_ID%');
</script>+++
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
```

### Validation

You can specify a [Standard Schema](https://standardschema.dev/) validator such as [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/) to check that an environment variable value is correct:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';
+++import * as v from 'valibot';+++

export const variables = defineEnvVars({
GOOGLE_ANALYTICS_ID: {
public: true,
+++validate: v.pipe(v.string(), v.regex(/G-[A-Z0-9]+/))+++
}
});
```

If a value is invalid, the app will fail to start (or build).

You can use validators to make values optional, or transform them (such as turning a string into a boolean, or parsing JSON) — see your validation library's documentation to learn how.

### Static variables

If a variable is configured with `static: true`, it will be inlined into your application code, enabling optimisations like dead-code elimination:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';
import * as v from 'valibot';

export const variables = defineEnvVars({
SHOW_DEBUG_OVERLAY: {
public: true,
+++static: true,+++

// coerce to true/false
validate: v.pipe(
v.optional(v.string(), ''),
v.transform((str) => str !== '')
)
}
});
```

Because this variable is `static`, the `<DebugOverlay>` component shown here will be excluded from the JavaScript bundle unless `SHOW_DEBUG_OVERLAY` is truthy:

```svelte
<script>
import { SHOW_DEBUG_OVERLAY } from '$app/env/public';
import DebugOverlay from '$lib/components/DebugOverlay.svelte';
</script>

{#if SHOW_DEBUG_OVERLAY}
<DebugOverlay />
{/if}
```

But if the variable is set before building the app...

```bash
SHOW_DEBUG_OVERLAY=true npm run build
```

...then the component will be included and shown.

### Documenting variables

You can document the purpose of an environment variable by adding a `description`:

```ts
/// file: src/env.ts
import { defineEnvVars } from '@sveltejs/kit/hooks';

export const variables = defineEnvVars({
CACHE_TTL_SECONDS: {
description: 'How long to cache responses, in seconds'
}
});
```

Hovering over `CACHE_TTL_SECONDS` in your app code will show the description.
7 changes: 7 additions & 0 deletions documentation/docs/98-reference/19-$app-env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: $app/env
---

> [!NOTE] This is an alias of [`$app/environment`]($app-environment), used when [explicit environment variables](environment-variables#Explicit-environment-variables) are enabled.

> MODULE: $app/env
9 changes: 9 additions & 0 deletions documentation/docs/98-reference/20-$app-env-private.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: $app/env/private
---

Private [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`).

To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration.


9 changes: 9 additions & 0 deletions documentation/docs/98-reference/20-$app-env-public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: $app/env/public
---

Public [environment variables](environment-variables) defined in `src/env.ts` (or `src/env.js`).

To use this module, you must enable the `experimental.explicitEnvironmentVariables` flag in your project configuration.


3 changes: 2 additions & 1 deletion packages/kit/kit.vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export default defineConfig({
alias: {
// Order matters: vite prefix-matches with trailing-slash, so longer keys must
// come first to avoid `$app/paths` matching `$app/paths/internal/client`.
'$app/env/internal': mock('app-env-internal'),
'$app/env': mock('app-env'),
'$app/paths/internal/client': mock('app-paths-internal-client'),
'$app/paths/internal/server': mock('app-paths-internal-server'),
'$app/paths': mock('app-paths'),
'__sveltekit/environment': mock('sveltekit-environment'),
'__sveltekit/paths': mock('sveltekit-paths')
},
projects: [
Expand Down
11 changes: 11 additions & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
"#app/paths": {
"browser": "./src/runtime/app/paths/client.js",
"default": "./src/runtime/app/paths/server.js"
},
"#app/env/public": {
"browser": "./src/runtime/app/env/public/client.js",
"default": "./src/runtime/app/env/public/server.js"
}
},
"exports": {
Expand All @@ -106,6 +110,13 @@
"types": "./types/index.d.ts",
"import": "./src/exports/internal/index.js"
},
"./internal/env": {
"types": "./types/index.d.ts",
"import": "./src/exports/internal/env.js"
},
"./internal/types": {
"import": "./src/exports/internal/types.js"
},
"./internal/server": {
"types": "./types/index.d.ts",
"import": "./src/exports/internal/server.js"
Expand Down
1 change: 1 addition & 0 deletions packages/kit/scripts/generate-dts.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ await createBundle({
'@sveltejs/kit/node': 'src/exports/node/index.js',
'@sveltejs/kit/node/polyfills': 'src/exports/node/polyfills.js',
'@sveltejs/kit/vite': 'src/exports/vite/index.js',
'$app/env': 'src/runtime/app/env/types.d.ts',
'$app/environment': 'src/runtime/app/environment/types.d.ts',
'$app/forms': 'src/runtime/app/forms.js',
'$app/navigation': 'src/runtime/app/navigation.js',
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { parseArgs } from 'node:util';
import colors from 'kleur';
import { load_config } from './core/config/index.js';
import { coalesce_to_error } from './utils/error.js';
import { resolve_explicit_env_entry } from './core/env.js';

/** @param {unknown} e */
function handle_error(e) {
Expand Down Expand Up @@ -77,6 +78,9 @@ if (command === 'sync') {
const config = await load_config();
const sync = await import('./core/sync/sync.js');
sync.all_types(config, values.mode);

const explicit_env_entry = resolve_explicit_env_entry(config.kit);
await sync.env(config.kit, explicit_env_entry, values.mode);
} catch (error) {
handle_error(error);
}
Expand Down
31 changes: 26 additions & 5 deletions packages/kit/src/core/adapt/builder.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
/** @import { Builder } from '@sveltejs/kit' */
/** @import { ResolvedConfig } from 'vite' */
/** @import { RouteDefinition } from '@sveltejs/kit' */
/** @import { RouteDefinition, EnvVarConfig } from '@sveltejs/kit' */
/** @import { RouteData, ValidatedConfig, BuildData, ServerMetadata, ServerMetadataRoute, Prerendered, PrerenderMap, Logger, RemoteChunk } from 'types' */
import colors from 'kleur';
import * as devalue from 'devalue';
import { createReadStream, createWriteStream, existsSync, statSync } from 'node:fs';
import { extname, resolve, join, dirname, relative } from 'node:path';
import { pipeline } from 'node:stream';
Expand All @@ -17,6 +19,7 @@ import { write } from '../sync/utils.js';
import { list_files } from '../utils.js';
import { find_server_assets } from '../generate_manifest/find_server_assets.js';
import { reserved } from '../env.js';
import { handle_issues, validate } from '../../exports/internal/env.js';

const pipe = promisify(pipeline);
const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.wasm', '.txt'];
Expand All @@ -32,7 +35,8 @@ const extensions = ['.html', '.js', '.mjs', '.json', '.css', '.svg', '.xml', '.w
* prerender_map: PrerenderMap;
* log: Logger;
* vite_config: ResolvedConfig;
* remotes: RemoteChunk[]
* remotes: RemoteChunk[];
* explicit_env_config: Record<string, EnvVarConfig<any>> | null;
* }} opts
* @returns {Builder}
*/
Expand All @@ -45,7 +49,8 @@ export function create_builder({
prerender_map,
log,
vite_config,
remotes
remotes,
explicit_env_config
}) {
/** @type {Map<RouteDefinition, RouteData>} */
const lookup = new Map();
Expand Down Expand Up @@ -168,7 +173,7 @@ export function create_builder({

const fallback = await generate_fallback({
manifest_path,
env: { ...env.private, ...env.public },
env: env.all,
out_dir: config.kit.outDir,
origin: config.kit.prerender.origin,
assets: config.kit.files.assets
Expand All @@ -193,7 +198,23 @@ export function create_builder({
const dest = `${config.kit.outDir}/output/prerendered/dependencies/${config.kit.appDir}/env.js`;
const env = get_env(config.kit.env, vite_config.mode);

write(dest, `export const env=${JSON.stringify(env.public)}`);
const values = config.kit.experimental.explicitEnvironmentVariables ? {} : env.public;

if (config.kit.experimental.explicitEnvironmentVariables) {
const variables = explicit_env_config ?? {};

/** @type {Record<string, StandardSchemaV1.Issue[]>} */
const issues = {};

for (const [name, config] of Object.entries(variables)) {
if (config.static || !config.public) continue;
values[name] = validate(variables, env.all[name], name, issues);
}

handle_issues(issues);
}

write(dest, `export const env=${devalue.uneval(values)}`);
},

generateManifest({ relativePath, routes: subset }) {
Expand Down
Loading
Loading