はじめに
私たちのモノレポには、Next.js(複数の Web アプリ)+ Electron(Desktop)+ Expo(Mobile)+ NestJS(API + Worker)が共存しています。
ある日、Sentry DSN をこれら全アプリに投入する必要が出ました。1 つの値を全アプリで使うだけなのに、フレームワークごとに env vars の規約が違って統一できない という現実に直面しました。
本記事では 4 つの規約の違いと、共存させる実用パターンを共有します。
4 つの規約
| アプリ | 規約 | 評価タイミング | 例 |
|---|---|---|---|
| Next.js | NEXT_PUBLIC_* (build 時置換) | build | NEXT_PUBLIC_SENTRY_DSN |
| Electron + Vite | import.meta.env.MAIN_VITE_* | build (Vite) | MAIN_VITE_SENTRY_DSN |
| Expo | EXPO_PUBLIC_* (Metro 置換) | bundling | EXPO_PUBLIC_SENTRY_DSN |
| NestJS / Node.js | process.env.* (runtime 評価) | runtime | SENTRY_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 時 / Runtime | Build | Build | Build | Runtime |
| 再 deploy 必要 | Yes | Yes | Yes | No |
| Prefix 必須 | Yes | Yes | Yes | No |
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.js | NEXT_PUBLIC_* | process.env.NEXT_PUBLIC_FOO |
| Electron + Vite | MAIN_VITE_* | import.meta.env.MAIN_VITE_FOO |
| Expo | EXPO_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 投入」のような横断作業の見積もりが正確になります。