ブログ一覧に戻る
モノレポNext.jsElectronExpoNestJS

Next.js / Electron / Expo / NestJS が混在するモノレポで env vars を統一できない 4 つの規約

はじめに

私たちのモノレポには、Next.js(複数の Web アプリ)+ Electron(Desktop)+ Expo(Mobile)+ NestJS(API + Worker)が共存しています。

ある日、Sentry DSN をこれら全アプリに投入する必要が出ました。1 つの値を全アプリで使うだけなのに、フレームワークごとに env vars の規約が違って統一できない という現実に直面しました。

本記事では 4 つの規約の違いと、共存させる実用パターンを共有します。

4 つの規約

アプリ規約評価タイミング
Next.jsNEXT_PUBLIC_* (build 時置換)buildNEXT_PUBLIC_SENTRY_DSN
Electron + Viteimport.meta.env.MAIN_VITE_*build (Vite)MAIN_VITE_SENTRY_DSN
ExpoEXPO_PUBLIC_* (Metro 置換)bundlingEXPO_PUBLIC_SENTRY_DSN
NestJS / Node.jsprocess.env.* (runtime 評価)runtimeSENTRY_DSN

それぞれ「なぜそうなったか」の歴史的経緯があり、統一しようとすると別の問題が生じます。順に説明します。

Next.js: NEXT_PUBLIC_*

Next.js は client bundle に埋め込む env vars を NEXT_PUBLIC_ prefix で明示 する設計です。

// app/components/Foo.tsx (client side)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // build 時に文字列に置換される

理由:

  • Client bundle にすべての env vars を埋め込むとセキュリティリスク (DB 接続文字列等が漏れる)
  • 「明示的に PUBLIC と marker した変数だけ client に出す」設計で安全側に倒している
  • Server-side では普通に process.env.SOMETHING が使える (build 時の .env ロード)

注意点:

  • build 時に文字列置換 されるため、deploy 後に値を変更するには 再 build が必要
  • runtime で動的に値を変えたい場合は API endpoint 経由で取得する別パターン

Electron + Vite: import.meta.env.MAIN_VITE_*

Electron + Vite (electron-vite plugin) では main process と renderer process で別の prefix を使い分けます:

// main process
const dsn = import.meta.env.MAIN_VITE_SENTRY_DSN;

// renderer process
const apiUrl = import.meta.env.RENDERER_VITE_API_URL;

理由:

  • Electron は main (Node.js) と renderer (Chromium) の 2 process 構成
  • 各 process で読める env vars を明示分離 (renderer に main 用 secret を漏らさない)
  • Vite の import.meta.env 規約に統合 (process.env ではない)

注意点 (実際につまずいた箇所です):

  • 最初に process.env.SENTRY_DSN で書いたら undefined で Sentry init が skip された
  • import.meta.env.MAIN_VITE_SENTRY_DSN に修正して動作
  • 加えて apps/desktop/src/main/env.d.ts に型定義を追加しないと TypeScript エラー
// env.d.ts
interface ImportMetaEnv {
  readonly MAIN_VITE_SENTRY_DSN?: string;
  readonly MAIN_VITE_GOOGLE_OAUTH_CLIENT_ID?: string;
}

Expo: EXPO_PUBLIC_*

Expo (React Native) は Metro bundler が EXPO_PUBLIC_ prefix の env vars を bundle に埋め込む 設計:

// app/_layout.tsx
const dsn = process.env.EXPO_PUBLIC_SENTRY_DSN;

理由:

  • React Native は client-only (server-side レンダリングなし)
  • すべての env vars が bundle に埋め込まれる前提
  • Next.js の NEXT_PUBLIC_ と思想は同じ (明示 PUBLIC marker)
  • EAS Build / EAS Update で profile (development / preview / production) ごとに別 env vars を投入可能

注意点:

  • process.env.EXPO_PUBLIC_* という書き方だが、これは build 時に置換される (runtime 評価ではない)
  • 値を変えるには再 build & 再 deploy が必要 (Next.js と同じ)
  • EAS Secrets で投入する変数も Build 時に env として渡される

NestJS / Node.js: process.env.*

最もシンプルで歴史ある規約。runtime に process.env を評価:

// apps/api/src/instrument.ts
const dsn = process.env.SENTRY_DSN;

理由:

  • Node.js 標準
  • 何も特別な仕組み不要、prefix 規約もない
  • runtime 評価なので deploy 後に env vars を変えれば値が変わる (再 build 不要)
  • Cloud Run / Kubernetes 等で env vars を動的に差し替えるユースケースに最適

注意点:

  • Server-side only (browser からは見えない、漏洩リスクなし)
  • 全 env vars が runtime に利用可 → 逆に「どれが public」「どれが secret」の区別が言語レベルで存在しない (運用ルールで管理)

統一できない理由

4 つの規約を 1 つに統一できればシンプルですが、現実には不可能:

観点Next.js NEXT_PUBLIC_*Electron MAIN_VITE_*Expo EXPO_PUBLIC_*NestJS process.env.*
Browser に漏れる設計で許可設計で許可設計で許可漏れない
Build 時 / RuntimeBuildBuildBuildRuntime
再 deploy 必要YesYesYesNo
Prefix 必須YesYesYesNo

Web の build 時置換 vs Server の runtime 評価という根本的な実行モデル差があるため、規約の統一は不可能です。

共存させる実用パターン

私たちは以下のパターンで運用しています:

1 つの値 (例: Sentry DSN) を全アプリで使う場合

各アプリの .env または環境変数で 異なる名前で同じ値 を持つ:

# apps/cloud/.env
NEXT_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/cloud-project-id

# apps/desktop/.env
MAIN_VITE_SENTRY_DSN=https://xxx@sentry.io/desktop-project-id

# apps/mobile/.env
EXPO_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/mobile-project-id

# docker-compose.yml (api)
environment:
  - SENTRY_DSN=${SENTRY_DSN_API:-}

本番環境の env vars 投入

  • Cloud Run (api / Worker): GCP Secret Manager で sentry-dsn-api 等を管理、gcloud run services update--set-secrets 注入
  • Cloud Run (Next.js): 同様、--set-env-vars NEXT_PUBLIC_* (build 時に注入が必要なら CI で .env 生成)
  • EAS Build (Mobile): eas secret:create --name EXPO_PUBLIC_SENTRY_DSN --value ...
  • electron-builder (Desktop): GitHub Actions Secret から .env を build step で生成 → electron-vite build

Build 時 vs Runtime の差分を意識

  • Server-side (NestJS / Cloud Run) は runtime 評価: env 変更だけで反映、再 build 不要
  • Web / Mobile / Desktop は build 時置換: env 変更には再 build & 再 deploy 必要

私たちには Sentry DSN を runtime で動的変更したくなったケースは無いので問題ありませんが、API URL や feature flag を runtime で切り替えたい場合は build 時 env vars には載せず、API endpoint 経由で取得する 別パターンを検討してください。

まとめ

アプリ規約コード書き方
Next.jsNEXT_PUBLIC_*process.env.NEXT_PUBLIC_FOO
Electron + ViteMAIN_VITE_*import.meta.env.MAIN_VITE_FOO
ExpoEXPO_PUBLIC_*process.env.EXPO_PUBLIC_FOO
NestJS / Node.js(prefix なし)process.env.FOO

4 規約を「統一できないもの」と受け入れて、各アプリの .env で異なる名前で同じ値を持つ 運用が現実解です。本番 deploy では GCP Secret Manager / EAS Secrets / GitHub Actions Secrets の 3 経路で投入することになります。

モノレポを始める際にこの差を知っておくと、後から「全アプリで Sentry DSN 投入」のような横断作業の見積もりが正確になります。