Environment Variables
Keeping secrets on the server with $env/static/private, and safely exposing config with the public variants.
2 menit membaca
Every real app needs configuration that changes between environments — a database URL, an API key, a feature flag — without being hardcoded or committed to source control. SvelteKit reads these from .env files and Vite, but it splits them into four distinct import sources specifically to prevent secrets from leaking into client-side JavaScript.
The .env file
# .env
DATABASE_URL="postgres://localhost/myapp"
STRIPE_SECRET_KEY="sk_test_..."
PUBLIC_ANALYTICS_ID="UA-12345"Any variable prefixed PUBLIC_ is safe to expose to the browser. Anything without that prefix is treated as a secret, and SvelteKit enforces the boundary at the import level, not just by convention.
Four ways to import them
// $env/static/private — secrets, inlined at build time, server-only
import { STRIPE_SECRET_KEY } from '$env/static/private';
// $env/static/public — public values, inlined at build time
import { PUBLIC_ANALYTICS_ID } from '$env/static/public';
// $env/dynamic/private — secrets read at runtime, server-only
import { env } from '$env/dynamic/private';
console.log(env.DATABASE_URL);
// $env/dynamic/public — public values read at runtime
import { env } from '$env/dynamic/public';
console.log(env.PUBLIC_ANALYTICS_ID);Trying to import $env/static/private (or $env/dynamic/private) from a .svelte file or a +page.js — code that could run in the browser — fails the build. This is the same protection you saw with $lib/server: the sensitive module simply doesn't exist from the client's point of view.
static vs. dynamic
$env/static/* variables are substituted directly into your code at build time — faster, and dead-code-eliminable, but it means changing a value requires a rebuild. $env/dynamic/* reads process.env (or the platform equivalent) at request time, which matters for platforms where you set environment variables after the build is already produced, such as some container or edge deployments. Default to static unless you have a specific reason to need a value that can change without rebuilding.
Using a secret in a load function
// src/routes/checkout/+page.server.js
import Stripe from 'stripe';
import { STRIPE_SECRET_KEY } from '$env/static/private';
const stripe = new Stripe(STRIPE_SECRET_KEY);
export async function load({ url }) {
const session = await stripe.checkout.sessions.retrieve(url.searchParams.get('session_id'));
return { session };
}Because this lives in +page.server.js and imports from $env/static/private, there are two independent layers preventing STRIPE_SECRET_KEY from ever reaching the browser — even a mistake in one layer (say, accidentally importing this module from a shared $lib file used by a component) is caught by the other.
Using a public value in a component
<script>
import { PUBLIC_ANALYTICS_ID } from '$env/static/public';
</script>
<svelte:head>
<script>
window.analyticsId = '{PUBLIC_ANALYTICS_ID}';
</script>
</svelte:head>The rule to remember
If a value should never be visible to someone opening dev tools, it doesn't get the PUBLIC_ prefix, and it only gets imported from .private or server-only files. Treating this as a hard boundary — rather than "I'll just be careful" — is what makes it safe to have contributors work on client-side code without ever risking a leaked API key.