環境変数管理

SvelteKitは環境変数を4つのモジュールに分類して提供します。「ビルド時 vs 実行時」と「プライベート vs パブリック」の2軸で整理されており、セキュリティとパフォーマンスを両立する設計です。

環境変数の4つのモジュール

ビルド時(static)実行時(dynamic)
プライベート(サーバー専用)$env/static/private$env/dynamic/private
パブリック(クライアントOK)$env/static/public$env/dynamic/public

.env ファイルの設定

SvelteKitはViteの仕組みを通じて.envファイルから環境変数を読み込みます。

# .env — 基本の環境変数ファイル
DATABASE_URL=postgresql://localhost:5432/myapp
API_SECRET_KEY=sk-secret-key-here

# PUBLIC_ プレフィックスで公開変数に
PUBLIC_APP_NAME=マイアプリ
PUBLIC_API_URL=https://api.example.com

環境別ファイル

Viteの規約に従い、環境別のファイルを設置できます。

.env                # 常に読み込まれる
.env.local          # 常に読み込まれる(.gitignore推奨)
.env.development    # dev時のみ
.env.production     # build/preview時のみ
.gitignore に追加

.env.env.localにはシークレットが含まれるため、必ず.gitignoreに追加してください。PUBLIC_プレフィックスの変数のみを含む.env.exampleをリポジトリに含めると、チーム内での共有に便利です。

$env/static/private — ビルド時プライベート変数

ビルド時に値が確定し、バンドルに静的に埋め込まれるサーバー専用の環境変数です。デッドコード除去などの最適化が効きます。

// +page.server.ts — サーバーサイドでのみ使用可能
import { DATABASE_URL, API_SECRET_KEY } from '$env/static/private';

export const load: PageServerLoad = async () => {
  // DATABASE_URLはビルド時の値で固定される
  const db = await connectDatabase(DATABASE_URL);

  // API_SECRET_KEYもビルド時に埋め込まれる
  const response = await fetch('https://api.example.com/data', {
    headers: { Authorization: `Bearer ${API_SECRET_KEY}` },
  });

  return { items: await response.json() };
};
<!-- ❌ クライアントサイドからはインポートできない -->
<script lang="ts">
  // コンパイルエラー: Cannot import $env/static/private into client-side code
  import { DATABASE_URL } from '$env/static/private';
</script>

$env/static/public — ビルド時パブリック変数

PUBLIC_プレフィックスを持つ変数のみが含まれ、クライアントサイドでも安全に使用できます。

// どこからでもインポート可能(サーバー・クライアント両方)
import { PUBLIC_APP_NAME, PUBLIC_API_URL } from '$env/static/public';
<script lang="ts">
  import { PUBLIC_APP_NAME, PUBLIC_API_URL } from '$env/static/public';

  // クライアントサイドでも安全に使用できる
  async function fetchPosts() {
    const response = await fetch(`${PUBLIC_API_URL}/posts`);
    return response.json();
  }
</script>

<h1>{PUBLIC_APP_NAME}</h1>
staticを使うメリット

$env/static/*はビルド時に値がインライン化されるため、Viteのツリーシェイキングやデッドコード除去の恩恵を受けられます。値がビルド後に変わることがなければ、staticを使うのがパフォーマンス上有利です。

$env/dynamic/private — 実行時プライベート変数

実行時のprocess.env(または各プラットフォーム固有の環境変数)にアクセスします。デプロイ先で値を変更可能です。

// +page.server.ts
import { env } from '$env/dynamic/private';

export const load: PageServerLoad = async () => {
  // envオブジェクト経由でアクセス(staticとは異なりnamed importではない)
  const dbUrl = env.DATABASE_URL;

  // 実行時に値が決まるため、環境ごとに異なる設定が可能
  if (env.FEATURE_FLAG === 'enabled') {
    // 新機能を有効化
  }

  return {
    /* ... */
  };
};
staticとの構文の違い

$env/static/*はnamed import(import { VAR } from ...)ですが、$env/dynamic/*はオブジェクト経由(env.VAR)でアクセスします。この違いに注意してください。

$env/dynamic/public — 実行時パブリック変数

実行時のPUBLIC_プレフィックス変数に、クライアントサイドからもアクセスできます。

<script lang="ts">
  import { env } from '$env/dynamic/public';

  // 実行時に解決される公開変数
  const apiUrl = env.PUBLIC_API_URL;
</script>

<p>API: {apiUrl}</p>

static vs dynamic の使い分け

// ✅ staticが適切なケース:ビルド時に確定する値
import { PUBLIC_APP_VERSION } from '$env/static/public';
// → バンドルにインライン化される。実行時オーバーヘッドなし

// ✅ dynamicが適切なケース:デプロイ先ごとに異なる値
import { env } from '$env/dynamic/private';
const dbUrl = env.DATABASE_URL;
// → staging/productionで異なるDBを使用可能
特性staticdynamic
値の決定タイミングビルド時実行時
デッドコード除去可能不可
デプロイ後の変更不可(再ビルドが必要)可能
インポート構文named importenvオブジェクト
主な用途バージョン番号、API URLDB接続文字列、フィーチャーフラグ

TypeScript での型定義

環境変数の型を正しく推論させるには、.envファイルで変数を宣言しておく必要があります。値が空でも型定義のために記載してください。

# .env — 型推論のために変数を宣言(値は空でもOK)
DATABASE_URL=
API_SECRET_KEY=
FEATURE_FLAG=
PUBLIC_APP_NAME=
PUBLIC_API_URL=

これにより、TypeScriptが$env/static/private$env/dynamic/privateのインポートに対して正しい補完と型チェックを提供します。

コマンドラインでのオーバーライド

# 開発時に特定の変数を上書き
FEATURE_FLAG="enabled" npm run dev

# 本番ビルド
DATABASE_URL="postgres://prod-server/db" npm run build

セキュリティのベストプラクティス

1. PUBLIC_ プレフィックスの厳格な管理

# ❌ シークレットにPUBLIC_を付けてはいけない
PUBLIC_DATABASE_URL=postgresql://...  # クライアントに漏洩する!

# ✅ シークレットはプレフィックスなし
DATABASE_URL=postgresql://...
API_SECRET_KEY=sk-secret...

2. サーバーサイドでのみシークレットを使用

// +page.server.ts(✅ サーバーサイド)
import { API_SECRET_KEY } from '$env/static/private';

export const load: PageServerLoad = async () => {
  // サーバーサイドでAPIを呼び出し、結果のみクライアントに返す
  const data = await fetch('https://api.example.com/secure', {
    headers: { Authorization: `Bearer ${API_SECRET_KEY}` },
  });

  return {
    items: await data.json(),
    // API_SECRET_KEY自体はクライアントに送信されない
  };
};

3. 環境変数の存在チェック

// src/lib/server/config.ts — 起動時バリデーション
import { DATABASE_URL, API_SECRET_KEY } from '$env/static/private';

// 必須の環境変数が設定されているか検証
function validateEnv() {
  const required = { DATABASE_URL, API_SECRET_KEY };

  for (const [name, value] of Object.entries(required)) {
    if (!value) {
      throw new Error(`必須の環境変数 ${name} が設定されていません`);
    }
  }
}

validateEnv();

export { DATABASE_URL, API_SECRET_KEY };

よくある間違い

クライアントコードでprivateモジュールをインポート

// ❌ +page.svelte や +page.ts ではインポートできない
import { DATABASE_URL } from '$env/static/private';
// → ビルドエラー

// ✅ サーバーファイル(+page.server.ts, +server.ts)で使用

staticとdynamicのインポート構文の混同

// ❌ dynamicでnamed importはできない
import { DATABASE_URL } from '$env/dynamic/private';

// ✅ dynamicはenvオブジェクト経由
import { env } from '$env/dynamic/private';
console.log(env.DATABASE_URL);

// ✅ staticはnamed import
import { DATABASE_URL } from '$env/static/private';

まとめ

SvelteKitの環境変数システムは「ビルド時/実行時」×「プライベート/パブリック」の4象限で整理されており、セキュリティとパフォーマンスを自然に両立します。PUBLIC_プレフィックスによる公開制御、サーバー専用モジュールのインポートガード、TypeScript型推論を活用して、安全で堅牢な環境変数管理を実現しましょう。

次のステップ