---
title: SvelteKit
description: How to integrate varlock with SvelteKit for secure, type-safe environment management
---
import { Steps } from "@astrojs/starlight/components";
import Badge from '@/components/Badge.astro';
import ExecCommandWidget from '@/components/ExecCommandWidget.astro';
import InstallJsDepsWidget from '@/components/InstallJsDepsWidget.astro';

[SvelteKit](https://svelte.dev/docs/kit) is built on [Vite](https://vite.dev), so there's no dedicated SvelteKit package. You use the [Vite integration](/integrations/vite/) the same way regardless of where you deploy.

Add `varlockVitePlugin()` to your Vite config ([setup below](#setup)) and it works across deploy targets. If you deploy to **Cloudflare Workers** (via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare)), the plugin detects the adapter and automatically wires up the runtime env-loader. You just need `@varlock/cloudflare-integration` installed as well (it ships `varlock-wrangler`). See [Cloudflare Workers setup](#setup-for-cloudflare-workers).

Check out the [SvelteKit example project](https://github.com/dmno-dev/varlock-examples/tree/main/examples/sveltekit) for a working reference.

---

## Setup

<div class="page-badges">
  <Badge npmPackage="@varlock/vite-integration" />
</div>

Use `varlockVitePlugin` exactly as you would in any Vite project. This is the setup for every deploy target. See [Cloudflare Workers](#setup-for-cloudflare-workers) below for the one extra package CF deployments need.

<Steps>

1. **Install packages**
    <InstallJsDepsWidget packages="@varlock/vite-integration varlock" />

1. **Run `varlock init` to set up your `.env.schema`**

    <ExecCommandWidget command="varlock init" showBinary={false} />

1. **Add the plugin to your Vite config.** It should come **before** `sveltekit()`:

    ```diff lang="ts" title="vite.config.ts"
    import { sveltekit } from '@sveltejs/kit/vite';
    import { defineConfig } from 'vite';
    +import { varlockVitePlugin } from '@varlock/vite-integration';

    export default defineConfig({
      plugins: [
    +    varlockVitePlugin(),
        sveltekit(),
      ],
    });
    ```

</Steps>

:::tip[Next steps]
See the [**Vite integration page**](/integrations/vite/) for configuration options, SSR injection modes, accessing env vars, managing environments, sensitive values, and more.
:::

---

## Setup for Cloudflare Workers

<div class="page-badges">
  <Badge npmPackage="@varlock/cloudflare-integration" />
</div>

For SvelteKit projects deploying to Cloudflare Workers via [`@sveltejs/adapter-cloudflare`](https://svelte.dev/docs/kit/adapter-cloudflare), use the same `varlockVitePlugin()` from the [setup above](#setup). When it detects the Cloudflare adapter, it automatically injects the runtime env-loader into SvelteKit's SSR entry so the resolved env is available inside the worker. You just need to additionally install `@varlock/cloudflare-integration`, which provides `varlock-wrangler` and the Cloudflare-specific loader the plugin pulls in. (The standard `varlockCloudflareVitePlugin` isn't used here because `@cloudflare/vite-plugin` doesn't currently support SvelteKit. See [cloudflare/workers-sdk#8922](https://github.com/cloudflare/workers-sdk/issues/8922).)

The adapter is detected whether you configure it in `svelte.config.js` or inline in your Vite config (SvelteKit ≥ 2.62), so no extra setup is needed either way.

:::note[Static-only SvelteKit sites]
If your SvelteKit app is fully static (no SSR, typically using `@sveltejs/adapter-static` and deployed to Cloudflare Pages or any CDN), you don't need `@varlock/cloudflare-integration` at all. The [base setup](#setup) is enough, since there's no server bundle for us to inject into.
:::

### Setup

<Steps>

1. **Install packages**
    <InstallJsDepsWidget packages="@varlock/vite-integration @varlock/cloudflare-integration varlock" />

1. **Run `varlock init` to set up your `.env.schema`**

    <ExecCommandWidget command="varlock init" showBinary={false} />

1. **Add the plugin to your Vite config.** This is the same as the base setup; it auto-detects the Cloudflare adapter:

    ```diff lang="ts" title="vite.config.ts"
    import { sveltekit } from '@sveltejs/kit/vite';
    import { defineConfig } from 'vite';
    +import { varlockVitePlugin } from '@varlock/vite-integration';

    export default defineConfig({
      plugins: [
    +    varlockVitePlugin(),
        sveltekit(),
      ],
    });
    ```

1. **Deploy with `varlock-wrangler`**

    Use `varlock-wrangler deploy` instead of `wrangler deploy` in your deploy script:

    ```diff lang="json" title="package.json" add="varlock-"
    {
      "scripts": {
        "dev": "vite dev",
        "build": "vite build",
        "deploy": "npm run build && varlock-wrangler deploy"
      }
    }
    ```

    If you deploy via [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) instead of a local/CI script, override the deploy command in your Cloudflare dashboard under **Settings → Build → Deploy command**. See [Workers Builds](#deploying-via-cloudflare-workers-builds) below.

</Steps>

### How it works

- **In dev**: `vite dev` runs SvelteKit's dev server; varlock resolves env via its normal flow and makes it available on `ENV.*`.
- **In production**: The SvelteKit SSR bundle has a `cloudflare:workers` runtime loader injected at the top of its server entry. At worker boot, it reads the `__VARLOCK_ENV` binding and hydrates varlock's runtime. The loader is guarded by a `navigator.userAgent === 'Cloudflare-Workers'` check so SvelteKit's Node-side postbuild steps (prerender, fallback) don't try to resolve `cloudflare:workers`.
- `varlock-wrangler deploy` uploads non-sensitive values as Cloudflare vars and sensitive values as secrets.

### Deploying via Cloudflare Workers Builds

If you deploy through [Cloudflare Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), two pieces of configuration must be set in the dashboard, and neither can be committed to the repo:

- **Override the Deploy command.** Under **Settings → Build → Deploy command**, replace the default `npx wrangler deploy` with `npx varlock-wrangler deploy`. Without this, Cloudflare runs stock `wrangler deploy`, which skips varlock resolution and leaves your worker without its resolved vars/secrets.
- **Set any _secret-zero_ vars under Build variables.** Any env vars varlock itself needs _during load_ (e.g. a 1Password service account token, a GCP key) must be set under **Settings → Build → Variables and Secrets** so they're available at build time. `varlock-wrangler deploy` then resolves your full env graph and uploads the result to the worker runtime as regular vars/secrets.

:::tip[Next steps]
See the [**Cloudflare integration page**](/integrations/cloudflare/) for more on `varlock-wrangler`, multi-environment setup, and [Workers Builds configuration](/integrations/cloudflare/#cloudflare-workers-builds-cicd).
:::

---

## Migrating from SvelteKit's `$env/*`

SvelteKit ships [four built-in env modules](https://svelte.dev/docs/kit/$env-static-private) (`$env/static/private`, `$env/static/public`, `$env/dynamic/private`, `$env/dynamic/public`) which split env vars along two axes: _static vs dynamic_ (inlined at build vs looked up at runtime) and _private vs public_ (server-only vs bundled for the browser, gated by the `PUBLIC_` prefix).

The key conceptual shift is that SvelteKit's system is **access-driven**: the same underlying variable behaves differently depending on which module you import it from, and public/private is inferred from the variable's name. Varlock is **schema-driven**: each item is declared once in `.env.schema` with decorators that determine its behavior (sensitivity, type, validation, whether it's inlined or resolved at runtime), and every access site uses the same `ENV` object. You describe the variable once, and that description is authoritative everywhere it's used.

Varlock replaces all four `$env/*` modules with a single `ENV` object from `varlock/env`. The same two axes still exist, but they're controlled by the schema rather than the import path:

| SvelteKit concept | Varlock equivalent |
| :---------------- | :----------------- |
| `$env/static/public` | `ENV.FOO` where `@sensitive=false`; non-sensitive values are inlined at build time on both client and server |
| `$env/static/private` | `ENV.FOO` where `@sensitive=true`, referenced from SSR/server code; inlined into the server bundle, build errors if referenced from client code |
| `$env/dynamic/public` | **Not yet supported.** Non-sensitive values are currently always inlined at build time (equivalent to `$env/static/public`). Runtime-resolved public values are on the roadmap. |
| `$env/dynamic/private` | `ENV.FOO`; in server contexts it's read from the live runtime env (e.g. Cloudflare bindings via `varlockCloudflareVitePlugin`, or Node's `process.env` elsewhere) rather than baked into the bundle |
| `PUBLIC_` prefix requirement | Per-item [`@sensitive`](/reference/item-decorators/#sensitive) decorator, or a schema-wide [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive) rule |

### Migrating imports

Everywhere you currently import from `$env/*`, switch to `varlock/env`:

```diff lang="ts" title="src/routes/+page.server.ts"
-import { API_KEY } from '$env/static/private';
-import { PUBLIC_API_URL } from '$env/static/public';
+import { ENV } from 'varlock/env';

 export const load = async () => {
-  const res = await fetch(`${PUBLIC_API_URL}/data`, {
-    headers: { Authorization: `Bearer ${API_KEY}` },
+  const res = await fetch(`${ENV.PUBLIC_API_URL}/data`, {
+    headers: { Authorization: `Bearer ${ENV.API_KEY}` },
   });
 };
```

`ENV` works from both `+page.svelte` (client) and `+page.server.ts`/`+server.ts` (server); there's no need to import from different paths depending on context. Varlock enforces the public/private boundary with both **build-time** checks (sensitive values are never included in the client bundle, and referencing one from client-reachable code surfaces as a build error) and **runtime** checks (log redaction and leak detection in server responses) so a mistake in either layer is caught rather than silently exposing a secret.

### Managing sensitive values

SvelteKit uses the `PUBLIC_` prefix to determine which vars are safe to bundle for the browser. Varlock uses decorators, either per-item or schema-wide via [`@defaultSensitive`](/reference/root-decorators/#defaultsensitive).

The simplest setup is to mark items explicitly:

```diff lang="env-spec" title=".env.schema"
+# @defaultSensitive=true
# ---
API_KEY=                # sensitive by default
# @sensitive=false
PUBLIC_API_URL=         # explicitly non-sensitive (bundled for browser)
```

If you'd rather keep SvelteKit's `PUBLIC_`-prefix convention, you can infer sensitivity from the name:

```diff lang="env-spec" title=".env.schema"
+# @defaultSensitive=inferFromPrefix('PUBLIC_')
# ---
API_KEY=                # sensitive (no prefix)
PUBLIC_API_URL=         # non-sensitive (has PUBLIC_ prefix)
```

:::caution[Bundling behavior]
All non-sensitive items are replaced at build time wherever `ENV.FOO` appears. Sensitive items are only available in server contexts: build-time checks block them from client bundles, and runtime log redaction / response leak detection catch any that slip past the build layer.
:::

:::note[Encrypting the env blob]
When deploying SSR SvelteKit apps to serverless platforms, varlock injects the resolved env into your server-side build output as plaintext JSON. This is generally safe since it only appears in server code, but you can encrypt it for extra protection (e.g., against sourcemap leaks). See the [encrypted deployments guide](/guides/encrypted-deployments/) for setup instructions.
:::
