Object with both private and public env vars
#15414
Development PR
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
Hto begin importingHeader.svelte, my editor will helpfully suggest that I meantHOMEBREW_CELLARor 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/publicand$env/dynamic/privatein the same module, you will need to rename them since you can't have two imports calledenv - 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
defineEnvVarshelper 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 otherhooks, 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:
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:
-
generateEnvModuleforadapter-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 testand lint the project withpnpm lintandpnpm check
Changesets
- If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running
pnpm changesetand following the prompts. Changesets that add features should beminorand those that fix bugs should bepatch. Please prefix changeset messages withfeat:,fix:, orchore:.
Edits
- Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.
Issue
Describe the problem
I often need both a private and a public env var on the server, so I have to do something like this which is a bit tedious:
import { env as privateEnv } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";
Describe the proposed solution
Either include the public env vars in $env/dynamic/private, or create common import { env } from "$env/dynamic" that is only usable on the server and includes both private and public env vars.
Alternatives considered
No response
Importance
nice to have
Additional Information
Related: #8474
Info
I'm not strongly inclined to implement this as a framework feature given that it's as easy as this to do in userland if you want it:
import { env as privateEnv } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";
export const env = { ...privateEnv, ...publicEnv };@elliott-with-the-longest-name-on-github You are completely right, but on the other hand, in which case is it preferrable to exclude public env vars from an object containing env vars on the server? I think it would make sense to change it even though it is easy to do in userland.
The main reason I remember for designing it this way was for autocomplete: It's really annoying when you type PUBLIC_ and you have two import options for every PUBLIC_ environment variable -- one from $env/static/public and one from $env/static/private. This is less of an issue with dynamic environment variables because you only ever import one name from them, but I don't really like the idea of this being inconsistent between dynamic/static.
I agree that the way it currently works makes total sense for static. Also dislike that private should include stuff that is not private. Maybe it would make more sense with $env/dynamic/client and $env/dynamic/server?
I think private essentially means server and public essentially means browser. I don't know that there's really a use case for public on the server? And so I wouldn't expect people to be importing both public and private in the same file
Here are a few examples that come to mind:
- Langfuse allows scoring AI generations directly from the client. For that, the Langfuse base URL and public key must be exposed on the client, so
PUBLIC_LANGFUSE_PUBLIC_KEYandPUBLIC_LANGFUSE_BASE_URL. On the server, when setting up Langfuse, I now need both private and public env vars:LANGFUSE_SECRET_KEY,PUBLIC_LANGFUSE_PUBLIC_KEYandPUBLIC_LANGFUSE_BASE_URL. - Sentry runs both on both the client and the server, so env vars must be public. Other tracing libraries are designed to run on the server, for example Jaeger, so the env vars should be private. When setting up both Sentry and Jaeger in
instrumentation.server.ts, I need to import both public and private env vars in the same file. - I have a
PUBLIC_ENV_NAMEenv var holding the name of the environment, such asdev,staging,prod, used to tag any logs. It must be public since it is used by for example Langfuse, Sentry, and Plausible on the client. But I also need to import it on the server, when using Langfuse, Sentry, or Jaeger. - Feature flags. The flag must be public to be used in
.sveltefiles to hide/show UI related to the feature, but it must also be imported on the server to execute/skip logic in load functions, form actions, or remote functions.
We don't have the env object anymore with explicit environment variables. It's now directly importing the environment variable names
y'all might like varlock. There is a single ENV object and both buildtime and runtime leak detection of anything sensitive.
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 ;)