はじめに

DX開発事業部の北村です。

Next.js 15 の middleware は Edge Runtime で動作するため、Firebase SDK が使えません。そのため、Firebase Remote Config を middleware から利用しようとすると、以下のようなエラーに直面します。

Error: Node.js crypto module is not available

この記事では、なぜ Edge Runtime で Firebase SDK が動かないのかを解説した上で、REST API を使って Firebase Remote Config を middleware から呼び出す実装方法を紹介します。今回の実装では、メンテナンスモードの ON/OFF をコードのデプロイなしに切り替える仕組みを構築します。

今回解説する手順は、以下の通りです。

  • Firebase Remote Config とは
  • Edge Runtime で Firebase SDK が使えない理由と解決策
  • Firebase Remote Config の設定
  • アクセストークンの取得(Cloud Run / ローカル両対応)
  • REST API でメンテナンス設定を取得する実装
  • middleware への組み込み
  • インメモリキャッシュによるパフォーマンス最適化

バージョンに関する注記
本記事は Next.js 15.4 以前(middleware で Node.js runtime が本番利用できなかった時代)の実装を解説しています。

  • Next.js 15.2〜15.4experimental.nodeMiddleware: true フラグで Node.js runtime が試験的に使用可能になりました(Next.js 15.2 リリースブログ
  • Next.js 15.5 以降:Node.js runtime が stable になり、middleware 内で Firebase SDK を含む Node.js ライブラリが使用可能になりました(Next.js 15.5 リリースブログ
  • Next.js 16 以降:middleware は “Proxy” にリネームされ、Node.js runtime がデフォルトになりました(Next.js 16 リリースブログ

ただし、REST API を使った本記事のアプローチはいずれのバージョンでも有効であり、Edge Runtime を使い続けたい場面でも引き続き参考になります。

Firebase Remote Config とは

Firebase Remote Config は、Google が提供する Firebase のサービスの一つで、アプリのコードを変更・再デプロイすることなく、アプリの動作や見た目をリアルタイムに変更できるクラウドベースの設定管理サービスです。(参考:Firebase Remote Config 公式ドキュメント

設定値は Firebase コンソール上で管理し、アプリ側からはキーを指定して値を取得します。変更はコンソールを操作するだけで即座に反映でき、特定のユーザーセグメントや条件に応じて異なる値を返すこともできます。

主なユースケース

  • 機能フラグ(Feature Flag)管理 — 新機能を特定のユーザーだけに公開する A/B テストや段階的リリース
  • メンテナンスモードの制御 — コードのデプロイなしにサービスを一時停止・再開する
  • テキストや設定値の変更 — お知らせバナーの文言やキャンペーン期間などを動的に切り替える

パラメータの種類

Remote Config のパラメータには以下のデータ型が設定できます。

データ型 用途例
文字列(String) バナーのテキスト、URL
数値(Number) タイムアウト値、表示件数
ブール値(Boolean) 機能の ON/OFF
JSON 複数の設定をまとめて管理

今回はメンテナンスモードの有効/無効・開始日時・終了日時をまとめて管理するため、JSON 型を使用します。

Edge Runtime で Firebase SDK が使えない理由と解決策

なぜ middleware で Firebase SDK が動かないのか

Next.js の middleware はデフォルトで Edge Runtime 上で動作します。Edge Runtime は V8 エンジンをベースにした軽量な実行環境で、Node.js とは異なり、使用できる API に制約があります。

具体的には fschild_processnet などの Node.js 組み込みモジュールが使用できません。Web 標準の API(fetchcrypto.subtleURL など)のみ利用可能です。

機能 Node.js Runtime Edge Runtime
fs モジュール
crypto モジュール
crypto.subtle(Web Crypto API)
fetch
Firebase Admin SDK
Firebase Client SDK(Remote Config)

Firebase Admin SDK は Node.js 固有のモジュールに依存しており、Edge Runtime では動作しません。

Firebase Client SDK の Remote Config も同様です。firebase/remote-config パッケージはブラウザ環境を前提に設計されているため、サーバーサイドの Edge Runtime 上では動作しません。どちらを使っても冒頭のエラーが発生します。

解決策:REST API を直接呼び出す

Firebase SDK の代わりに、Firebase Remote Config REST API を直接呼び出します。REST API の呼び出しには Web 標準の fetch を使用するため、Edge Runtime でも問題なく動作します。

なぜ middleware に実装するのか

メンテナンス中にユーザーがサービスにアクセスした際、どのページに遷移しようとしてもメンテナンスページに自動リダイレクトさせたい場合、全リクエストに対してインターセプトできる middleware が最適な実装場所です。

page.tsxlayout.tsx に個別にメンテナンスチェックを実装すると、追加するたびに漏れが生じるリスクがあります。middleware に集約することで、一箇所の変更だけで全ページに適用できます。

Firebase Remote Config の設定

まず、Firebase コンソールから Remote Config にメンテナンス設定を追加します。

Firebase コンソール → Remote Config → 「パラメータを追加」から、以下の内容でパラメータを作成します。

項目
パラメータキー webMaintenanceConfig
データ型 JSON
デフォルト値 下記参照

webMaintenanceConfig(デフォルト値)

{
  "isMaintenanceMode": false,
  "maintenanceStart": "2025/01/01 00:00",
  "maintenanceEnd": "2025/01/01 06:00"
}

isMaintenanceModetrue にするだけでメンテナンスモードが有効になる設計です。設定を変更したら「変更を公開」を押して保存します。

アクセストークンの取得

Firebase Remote Config REST API の呼び出しには OAuth 2.0 のアクセストークンが必要です。取得方法は実行環境によって異なります。

Cloud Run 環境:メタデータサーバーから取得

Cloud Run 上では、Google Cloud のメタデータサーバーから直接トークンを取得できます。サービスアカウントキーのファイルは不要です。これは ADC(Application Default Credentials) の仕組みを活用しています。

src/utils/maintenanceConfig.ts

async function getTokenFromMetadataServer(): Promise<string> {
  const response = await fetch(
    'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token',
    {
      headers: { 'Metadata-Flavor': 'Google' },
    },
  );

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(
      `Failed to get token from metadata server: ${response.status} - ${errorText}`,
    );
  }

  const data = await response.json();
  return data.access_token;
}

ローカル環境:サービスアカウントキーから JWT を生成


ローカル開発環境では Google Cloud のメタデータサーバーに接続できないため、サービスアカウントキー(JSON ファイル)を使って手動で JWT を生成し、アクセストークンに交換します。

ここで Edge Runtime 対応の重要なポイントが2つあります。

1つ目は署名生成です。Node.js の crypto モジュールは使えないため、Web 標準の crypto.subtle(Web Crypto API) を使って RS256 署名を生成します。

2つ目は Base64URL エンコードです。Node.js の Buffer も Edge Runtime では利用できないため、Web 標準の btoa を使って実装します。

まず、Base64URL エンコードのヘルパー関数を定義します。

src/utils/maintenanceConfig.ts(ヘルパー関数)

// 文字列を Base64URL エンコード(JWT ヘッダー・ペイロード用)
function toBase64Url(str: string): string {
  return btoa(str)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// ArrayBuffer を Base64URL エンコード(署名用)
function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  const base64 = btoa(String.fromCharCode(...bytes));
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

次に、crypto.subtle を使って RS256 署名を生成します。

src/utils/maintenanceConfig.ts(署名生成)

async function createSignature(
  data: string,
  privateKey: string,
): Promise<string> {
  // PEM 形式のヘッダー・フッターと改行を除去して DER バイナリに変換
  const pemKey = privateKey
    .replace('-----BEGIN PRIVATE KEY-----', '')
    .replace('-----END PRIVATE KEY-----', '')
    .replace(/\s/g, '');

  const binaryKey = Uint8Array.from(atob(pemKey), (c) => c.charCodeAt(0));

  // crypto.subtle で秘密鍵をインポート
  const cryptoKey = await crypto.subtle.importKey(
    'pkcs8',
    binaryKey,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false,
    ['sign'],
  );

  // RS256 で署名を生成
  const signature = await crypto.subtle.sign(
    'RSASSA-PKCS1-v1_5',
    cryptoKey,
    new TextEncoder().encode(data),
  );

  return arrayBufferToBase64Url(signature);
}

ヘルパー関数を使って JWT を組み立て、OAuth 2.0 のアクセストークンに交換します。

src/utils/maintenanceConfig.ts(JWT 生成 → トークン取得)

async function getTokenFromServiceAccountKey(): Promise<string> {
  const serviceAccountKey = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
  if (!serviceAccountKey) {
    throw new Error('FIREBASE_SERVICE_ACCOUNT_KEY is not set');
  }

  const serviceAccount = JSON.parse(serviceAccountKey);
  const now = Math.floor(Date.now() / 1000);

  // JWT ヘッダーとペイロードを Base64URL エンコード
  const jwtHeader = toBase64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
  const jwtPayload = toBase64Url(
    JSON.stringify({
      iss: serviceAccount.client_email,
      scope:
        'https://www.googleapis.com/auth/firebase.remoteconfig https://www.googleapis.com/auth/cloud-platform',
      aud: 'https://oauth2.googleapis.com/token',
      exp: now + 3600,
      iat: now,
    }),
  );

  // JWT に署名して assertion を作成
  const signature = await createSignature(
    `${jwtHeader}.${jwtPayload}`,
    serviceAccount.private_key,
  );
  const assertion = `${jwtHeader}.${jwtPayload}.${signature}`;

  // assertion を OAuth 2.0 トークンエンドポイントに送信してアクセストークンを取得
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion: assertion,
    }),
  });

  const tokenData = await tokenResponse.json();

  if (!tokenResponse.ok || !tokenData.access_token) {
    throw new Error(`Failed to get access token: ${JSON.stringify(tokenData)}`);
  }

  return tokenData.access_token;
}

最後に、K_SERVICE 環境変数(Cloud Run が自動で設定する変数)で実行環境を判定し、トークン取得関数を呼び分けます。

src/utils/maintenanceConfig.ts(環境判定)

async function getAccessToken(): Promise<string> {
  const isCloudRun = process.env.K_SERVICE !== undefined;

  if (isCloudRun) {
    return await getTokenFromMetadataServer();
  }
  return await getTokenFromServiceAccountKey();
}

REST API でメンテナンス設定を取得する実装


アクセストークンが取得できたら、Firebase Remote Config REST API を呼び出します。

エンドポイントは以下の形式です。(参考:Firebase Remote Config REST API リファレンス

GET https://firebaseremoteconfig.googleapis.com/v1/projects/{projectId}/remoteConfig

src/utils/maintenanceConfig.ts(設定取得)

export async function getMaintenanceConfig(): Promise<MaintenanceConfig> {
  try {
    const accessToken = await getAccessToken();
    const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;

    const url = `https://firebaseremoteconfig.googleapis.com/v1/projects/${projectId}/remoteConfig`;

    const response = await fetch(url, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    const data = await response.json();

    // レスポンスの構造: data.parameters.{キー名}.defaultValue.value
    const parameterValue =
      data.parameters?.['webMaintenanceConfig']?.defaultValue;

    if (!parameterValue) {
      return { isMaintenanceMode: false, maintenanceStart: '', maintenanceEnd: '' };
    }

    const config = JSON.parse(parameterValue.value || '{}') as MaintenanceConfig;

    return {
      isMaintenanceMode: config.isMaintenanceMode ?? false,
      maintenanceStart: config.maintenanceStart || '',
      maintenanceEnd: config.maintenanceEnd || '',
    };
  } catch (error) {
    console.error('Failed to fetch maintenance config:', error);
    // エラー時はメンテナンスモードをオフにしてサービスを継続
    return { isMaintenanceMode: false, maintenanceStart: '', maintenanceEnd: '' };
  }
}

ℹ️ 解説
REST API のレスポンスは以下の構造になっています。defaultValue.value に JSON 文字列が格納されているため、JSON.parse でパースして取り出します。

レスポンス例

{
  "parameters": {
    "webMaintenanceConfig": {
      "defaultValue": {
        "value": "{\"isMaintenanceMode\":true,\"maintenanceStart\":\"2025/01/01 00:00\",\"maintenanceEnd\":\"2025/01/01 06:00\"}"
      }
    }
  }
}

middleware への組み込み


getMaintenanceConfig を middleware から呼び出し、メンテナンスモードに応じてリダイレクト処理を実装します。

src/middleware.ts

import { getMaintenanceConfig } from '@/utils/maintenanceConfig';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  const maintenanceConfig = await getMaintenanceConfig();

  // Cloud Run では x-forwarded-host ヘッダーからホスト名を取得する
  const buildUrl = (path: string): URL => {
    const protocol = request.headers.get('x-forwarded-proto') || 'https';
    const host =
      request.headers.get('x-forwarded-host') ||
      request.headers.get('host') ||
      request.nextUrl.host;
    return new URL(path, `${protocol}://${host}`);
  };

  // メンテナンスモードが有効 → メンテナンスページ以外はリダイレクト
  if (maintenanceConfig.isMaintenanceMode && pathname !== '/maintenance') {
    return NextResponse.redirect(buildUrl('/maintenance'));
  }

  // メンテナンスモードが無効 → メンテナンスページへのアクセスはリダイレクト
  if (!maintenanceConfig.isMaintenanceMode && pathname === '/maintenance') {
    return NextResponse.redirect(buildUrl('/dashboard'));
  }

  return NextResponse.next();
}

export const config = {
  // 静的ファイルや API ルートは除外
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico|manifest.json|images|public).*)',
  ],
};

インメモリキャッシュによるパフォーマンス最適化


REST API を毎リクエストごとに呼ぶと、レスポンス速度の悪化や Firebase API のクォータ超過につながります。

⚠️ 注意
Next.js の fetchnext: { revalidate: N } を指定する方法はよく知られていますが、この設定は middleware 内では機能しません。 Next.js の Data Cache はサーバーコンポーネントやルートハンドラ向けの仕組みであり、Edge Runtime 上で動作する middleware では適用されません。そのままにすると、すべてのページ遷移のたびに外部 API への通信が発生し続けます。

解決策:モジュールレベルのインメモリキャッシュ


middleware 環境では、モジュールレベルのグローバル変数を使ったインメモリキャッシュが有効です。Cloud Run や Edge のインスタンスが起動している間、変数はメモリ上に保持されます。

処理 キャッシュ時間 理由
アクセストークン 3500秒(約1時間) トークンの有効期限に合わせる
Remote Config の取得 60秒 メンテナンス設定の反映を1分以内に抑える

ファイルの先頭でキャッシュ用の変数を宣言します。

src/utils/maintenanceConfig.ts(キャッシュ変数)

// インメモリキャッシュ(インスタンスが生きている間有効)
let cachedToken: { value: string; expiresAt: number } | null = null;
let cachedConfig: { value: MaintenanceConfig; expiresAt: number } | null = null;

getAccessToken にキャッシュロジックを追加します。

src/utils/maintenanceConfig.ts(キャッシュ付きトークン取得)

async function getAccessToken(): Promise<string> {
  const now = Date.now();

  // キャッシュが有効であればそのまま返す
  if (cachedToken && now < cachedToken.expiresAt) {
    return cachedToken.value;
  }

  const isCloudRun = process.env.K_SERVICE !== undefined;
  const token = isCloudRun
    ? await getTokenFromMetadataServer()
    : await getTokenFromServiceAccountKey();

  // 取得したトークンをキャッシュ(有効期限の少し手前で破棄)
  cachedToken = { value: token, expiresAt: now + 3500 * 1000 };
  return token;
}

getMaintenanceConfig にも同様にキャッシュロジックを追加します。

src/utils/maintenanceConfig.ts(キャッシュ付き設定取得)

export async function getMaintenanceConfig(): Promise<MaintenanceConfig> {
  const now = Date.now();

  // キャッシュが有効であればそのまま返す
  if (cachedConfig && now < cachedConfig.expiresAt) {
    return cachedConfig.value;
  }

  try {
    const accessToken = await getAccessToken();
    const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID;
    const url = `https://firebaseremoteconfig.googleapis.com/v1/projects/${projectId}/remoteConfig`;

    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    const data = await response.json();
    const parameterValue =
      data.parameters?.['webMaintenanceConfig']?.defaultValue;

    if (!parameterValue) {
      return { isMaintenanceMode: false, maintenanceStart: '', maintenanceEnd: '' };
    }

    const config = JSON.parse(parameterValue.value || '{}') as MaintenanceConfig;
    const result = {
      isMaintenanceMode: config.isMaintenanceMode ?? false,
      maintenanceStart: config.maintenanceStart || '',
      maintenanceEnd: config.maintenanceEnd || '',
    };

    // 取得した設定をキャッシュ(60秒間保持)
    cachedConfig = { value: result, expiresAt: now + 60 * 1000 };
    return result;
  } catch (error) {
    console.error('Failed to fetch maintenance config:', error);
    return { isMaintenanceMode: false, maintenanceStart: '', maintenanceEnd: '' };
  }
}

ℹ️ 解説
インメモリキャッシュは Cloud Run のインスタンス単位で保持されます。複数インスタンスが起動している場合、各インスタンスが独立してキャッシュを持ちますが、Firebase API へのリクエスト数は大幅に削減できます。インスタンスを跨いで共有したい場合は Redis などの外部 KVS の利用を検討してください。

最後に

今回のポイントをまとめます。

  • Firebase Remote Config を使うことで、コードのデプロイなしにアプリの動作をリアルタイムに変更できる
  • Edge Runtime では Firebase SDK が使えないため、全ページ遷移前に処理を挟みたい middleware では REST API を直接呼ぶ必要がある
  • 認証トークンの取得方法は環境によって異なる(Cloud Run はメタデータサーバー、ローカルはサービスアカウントキー)
  • JWT 署名は crypto.subtle、Base64URL エンコードは btoa で行う(Edge Runtime では Node.js の cryptoBuffer が使えないため)
  • middleware では next: { revalidate } が効かないため、モジュールレベルのインメモリキャッシュで毎リクエストごとに外部 API を呼ぶオーバーヘッドを避ける

Firebase Remote Config を middleware で活用することで、Firebase コンソールから設定を変更するだけで、即座にメンテナンスモードの ON/OFF が可能になります。ぜひ参考にしてみてください!