feat: explicit environment variables

#15934

Closing issue

Pull request

Merged
R
Rich-Harris
May 31, 2026, 4:05 AM

Today, environment variables are implicitly available: anything in process.env can be imported directly (as long as the name is a valid identifier) from $env/static/private, or accessed via env imported from $env/dynamic/private. Environment variables beginning with publicPrefix are available from the public counterparts.

This is... fine, but there is some room for improvement:

  • You get random auto-import suggestions — if I type H to begin importing Header.svelte, my editor will helpfully suggest that I meant HOMEBREW_CELLAR or something else improbable
  • There's inconsistency between static (named imports) and dynamic (env.BLAH), which feels weird. If for some reason you use both $env/dynamic/public and $env/dynamic/private in the same module, you will need to rename them since you can't have two imports called env
  • It's weird that you can reference a given env var both statically (inlined into the build) and dynamically (evaluated at runtime)
  • There's a lot of modules!
  • There's no central place that describes which environment variables are expected
  • No validation — if a critical env var is missing or malformed, you might not find out until you start seeing errors in your production logs
  • No type safety or inline documentation. Everything is just a string whose purpose you have to infer from the name

This PR presents an explicit alternative. Opt in by adding the following flag to your svelte.config.js...

export default {
  kit: {
    experimental: {
      explicitEnvironmentVariables: true
    }
  }
};

...and creating a src/env.ts (or src/env.js) that exports variables:

import { defineEnvVars } from '@sveltejs/kit/hooks';
import * as v from 'valibot';

export const variables = defineEnvVars({
  // by default, env vars are a) secret, b) evaluated when the app starts, and c) required
  POSTGRES_URL: {},

  // some environment variables are safe to be used in the client
  GOOGLE_ANALYTICS_ID: {
    public: true,

    // env vars can be validated, using any https://standardschema.dev library
    // (here, we're using https://valibot.dev)
    schema: v.pipe(v.string(), v.regex(/G-[A-Z0-9]+/))
  },

  // some can be evaluated at build time, meaning they can be used for dead code elimination
  SHOW_DEBUGGING_OVERLAY: {
    public: true,
    static: true,

    // if a description is provided, it will be visible when the imported variable is hovered
    description: 'If enabled, will show a FPS meter in the corner of the page',

    // validators can transform inputs, e.g. to a boolean
    schema: v.pipe(
      v.optional(v.string(), ''),
      v.transform((str) => str !== '')
    )
  }
});

The defineEnvVars helper just returns its argument, but is useful as it enforces the correct types (and provides autocomplete, etc). I'm tempted to add similar helpers for other hooks, though that's a conversation for another time.

With those variables defined, we can import them via two new modules — $app/env/private and $app/env/public. As with existing server-only modules, $app/env/private cannot be imported into code that can run in the client.

Types are inferred from validators, where provided, and inline documentation is attached to the variables:

image

If environment variables are missing, the app errors on startup (or build, for static env vars).

The validators (and indeed the entire src/env module) only ever run on the server. Public variables are sent to the client with the initial server rendered HTML, or (if the initial document is prerendered) by importing a generated module, the same as happens today.

The $app/environment module is aliased as $app/env, to make everything feel a bit more cohesive. The goal is to remove $app/environment and the four $env/* modules (and the associated configuration) in SvelteKit 3.

TODO:

  • generateEnvModule for adapter-static
  • Populate %sveltekit.env% with this rather than the existing env vars, where appropriate
  • I just realised that public static vars don't work as advertised right now — need to fix that
  • Don't bother sending static vars from the server to the client
  • Make the docs nice

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.
👍 50🎉 21❤️ 50🚀 8👀 9

Info

Merged at Jun 2, 2026, 11:37 PM
Merged by Rich-Harris
Assignees None
Reviewers None
Labels None
Milestone None

Pro tip: You can prefix GitHub URLs of issues, PRs or discussions with svcl.dev/ to view them on this page! Also try it on a GitHub release ;)