2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Firebase v12.14+】FCM登録トークン非推奨化!新方式「FID」移行時の落とし穴とDB設計の全容

2
Posted at

「Firebase SDKをアップデートしたら、なぜか通知が届かなくなった……」
Firebase JS SDK v12.14.0で導入された新方式(FID)への移行は、単なるAPIの書き換えだけでは通知の二重送信や配信漏れを引き起こす罠が潜んでいます。本番環境で安全に移行するための設計と注意点をまとめました。

TL;DR

  • 何が起きたか: Firebase JS SDK v12.14.0でFCM登録トークン取得(getToken())が非推奨となり、FID(Installation ID)方式へ移行するAPIが追加されました。
  • 何が問題か: 単にメソッド名だけ書き換えても動きません。フロント・バックエンド・DBの設計を正しく見直さないと、通知の二重送信や配信漏れといった致命的なバグを引き起こします。
  • 対策: 新旧APIの混在防止、Service Worker購読の個別制御、Feature Flag移行時の注意点を把握して、安全に本番移行するための勘所を解説します。

1. 概要:なぜ「トークン」から「FID」へ移行するのか?

Firebase JavaScript SDK v12.14.0(2026年5月28日リリース)にて、Cloud Messaging(FCM)の送信先指定方式が大きく刷新されました。

領域 従来の方式 新しい推奨方式
送信先識別子 FCM 登録トークン (Registration Token) Firebase Installation ID (FID)
クライアント登録 getToken() (非推奨) register()
識別子の受取 Promiseの返り値 onRegistered() コールバック
登録解除 deleteToken() (非推奨) unregister()
サーバー送信先指定 message.token message.fid
自動更新 手動で定期的な getToken() の再実行が必要 SDKがFID変更や pushsubscriptionchange を監視・自動再登録

移行による最大のメリット

従来の方式では、FCMトークンの失効や変更(ブラウザのPushSubscriptionの更新など)を検知し、自社サーバーへ最新のトークンを登録し直す仕組みをアプリ開発者が自前で構築する必要がありました。
FID方式では、SDKがFirebase InstallationsのFID変更や、Service Worker内の pushsubscriptionchange イベントを自動的に監視し、必要に応じてバックエンドへの再登録フロー(onRegistered() コールバック)を自動的に実行します。


2. クライアント側(JS/TS SDK)の実装比較

Before:従来のトークン方式

従来の方式では、アプリ起動時などに非同期でトークンを要求し、取得したトークンをそのままサーバーに送っていました。

import { getMessaging, getToken } from "firebase/messaging";

const messaging = getMessaging();

async function initializeFcm() {
  try {
    // トークンをPromiseの返り値として直接取得
    const token = await getToken(messaging, {
      vapidKey: "YOUR_PUBLIC_VAPID_KEY"
    });
    
    if (token) {
      await registerTokenOnServer(token);
    }
  } catch (error) {
    console.error("FCM Token generation failed:", error);
  }
}

After:新しいFID方式

FID方式では、トークンは直接返されません。まずFIDを受け取るイベントハンドラー(onRegistered)を登録し、その後に register() を呼び出します。

import {
  getMessaging,
  register,
  onRegistered,
  onUnregistered
} from "firebase/messaging";

const messaging = getMessaging();

// 1. register() の呼び出し「前」に必ずハンドラーを設定する
onRegistered(messaging, (fid) => {
  // FIDの同期処理はコールバック内で実行する
  syncFidToServer(fid).catch(handleRegistrationError);
});

onUnregistered(messaging, (fid) => {
  // 登録解除されたFIDをサーバーから削除する
  removeFidFromServer(fid).catch(handleRegistrationError);
});

// 2. 登録プロセスを開始する
export async function initializeFcmFid() {
  await register(messaging, {
    vapidKey: "YOUR_PUBLIC_VAPID_KEY"
  });
}

3. サーバー側(Admin SDK / HTTP v1 API)の変更点

クライアント側をFID方式に変更した場合、送信側のバックエンドも対応する必要があります。

Firebase Admin Node.js SDK の対応

FIDでの送信は Firebase Admin Node.js SDK v14.1.0(2026年6月24日リリース)以上で公式にサポートされています。

Firebase Admin SDK v14の破壊的変更に注意
FIDでの送信に必要な Admin SDK v14系は、Node.js 22以上 および モジュラーAPIへの移行 が必須となります。バックエンド環境のシステム要件(Node.jsのバージョンや既存の共通送信処理のインポート方式など)に大きな影響を与えるため、アップデートは慎重に行ってください。

import { getMessaging } from "firebase-admin/messaging";

// FIDを指定して送信(Admin SDK v14.1.0以降)
await getMessaging().send({
  fid: "Firebase_Installation_Id_From_Client",
  notification: {
    title: "新方式の通知",
    body: "FID宛てに送信されました"
  }
});

FCM HTTP v1 API を直接叩く場合

SDKを使わず、HTTP v1 APIへ直接リクエストしている場合は、payloadの送信先キーを token から fid に変更します。

現状の互換性について:
FCM HTTP v1 APIでは、送信先指定キーを fid に変更しなくても、従来の token キーにFID(Installation ID)の文字列を指定して送信するだけで正常に動作します。
バックエンドのAPIスキーマをすぐに変更できない事情がある場合や、既存の汎用送信処理を使い回したい場合は、token キーにクライアントから送られてきたFIDをそのまま流し込むことで、バックエンド側の修正を最小限に抑えて移行することも可能です(ただし、長期的には fid キーへ移行することが推奨されます)。

POST https://fcm.googleapis.com/v1/projects/{project-id}/messages:send
Authorization: Bearer {access_token}
Content-Type: application/json
{
  "message": {
    "fid": "Firebase_Installation_Id_From_Client",
    "notification": {
      "title": "新方式の通知",
      "body": "HTTP v1 APIで直接FIDを指定"
    }
  }
}

4. 移行における「6つの注意点」

メソッド名が変更されただけのアップデートに見えますが、内部仕様の変更に起因する重要な注意点があります。

注意点1:unregister() は PushSubscription を解除しない!

これが最も大きな挙動変更です。

  • 従来の deleteToken(): FCM登録を解除すると同時に、ブラウザの PushSubscription.unsubscribe() も内部で呼び出していました。
  • 新しい unregister(): FCM登録(FID mapping)は解除しますが、ブラウザのPushSubscriptionは解除せずそのまま維持します。

アプリの機能として「通知機能を完全にオフにする(ユーザーがオプトアウトする)」要件がある場合、単に unregister() を呼ぶだけではプッシュ通知購読(Subscription)がブラウザに残ったままになります。オプトアウト時には独自に PushSubscription.unsubscribe() を実行する処理を追加する必要があります。

注意点2:onRegistered() は複数登録できない「単一スロット」

onRegistered() および onUnregistered() の登録は、内部的にマルチキャストのイベントリスナーではなく、単一の変数(スロット)への代入として実装されています。

// Firebase SDK内部の実装イメージ(後から登録したものが上書きされる)
export function onRegistered(messaging, nextOrObserver) {
  messaging.onRegisteredHandler = nextOrObserver;
}

異なる画面や別々のコンポーネントで onRegistered() をそれぞれ呼び出すと、最後に呼び出したハンドラーで上書きされ、他の場所のコールバックが動かなくなります。FCMの初期化やハンドリングは、必ずアプリ全体で「単一のモジュール」に集約するように設計してください。

注意点3:新旧APIの混在による「追跡不能トークンのゾンビ化」

Q: 送信先として「FID」と「トークン」の共存は可能なのか?
結論から言うと、送信側(バックエンド)での共存は可能ですが、クライアント側(フロントエンド)でのAPI混在は厳禁です。

  • 送信側(共存可能):
    FCMの内部仕様として、すべてのFCM登録トークンは裏側でFirebase Installations(FIS)が払い出した「FID」に紐付いて発行されています。そのため、FCMサーバーとしては同一端末に対して「登録トークン宛て(message.token)」と「新FID宛て(message.fid)」のどちらで送信リクエストを送っても、最終的には同じブラウザのPushSubscriptionへ正しくルーティングされます。バックエンド送信側での両用化(ハイブリッド運用)は完全に動作します。
  • クライアント側(混在はNG):
    しかし、同一クライアントインスタンス(同一ブラウザ環境)で、旧API(getToken())と新API(register())の両方を交互に呼び出すと、SDK内のローカルキャッシュ(IndexedDB等)に保持されていた登録状態やメタデータが上書き・破棄されます。
    • getToken() の後に register() を呼ぶと、ローカルの旧トークン情報は破棄されますが、FCMサーバー上の旧トークン登録を明示的に削除するAPIは呼ばれません
    • 同様に register() の後に getToken() を呼んだ場合も、ローカルのFID情報は消えますが、サーバー上のFID登録は残ります。

この状態になると、ローカルから切り離された古いマッピング情報がFCMサーバー上に孤立して残り続け(ゾンビ化)、意図しない二重送信や配信不整合の原因になります。
対策: 特定のクライアントバージョンでは「どちらか一方の方式(API)だけを使用する」ように制御し、混在させないようにしてください。

注意点4:onRegistered() の非同期処理はSDKが待ってくれない

onRegistered() コールバックは同期的に呼び出されるだけで、その中で返された Promise (サーバーへのFID送信処理など)をSDK側が await することはありません。

// コールバック内で非同期処理を走らせても、SDKはその完了を待たない
onRegistered(messaging, async (fid) => {
  await syncFidToServer(fid); // ここでエラーが起きても register() 自体は完了してしまう
});

このため、自社バックエンドへのFID保存が通信エラー等で失敗した場合でも、SDK内では register() が正常に完了したとみなされます。
対策: コールバック内の非同期処理には必ず自前で .catch() によるエラーハンドリングを仕込み、失敗した場合は「未同期状態」としてローカルに保存し、次回起動時などに再送するリトライロジックを独自に組み込む必要があります。

// 対策コード例
import { getMessaging, onRegistered } from "firebase/messaging";

const messaging = getMessaging();
const STORAGE_KEY = "pending_fcm_fid";

onRegistered(messaging, async (fid) => {
  try {
    await syncFidToServer(fid);
    localStorage.removeItem(STORAGE_KEY); // 同期成功で削除
  } catch (error) {
    console.error("FID sync failed, saving for retry:", error);
    localStorage.setItem(STORAGE_KEY, fid); // ローカルに一時保存
  }
});

// アプリ起動時のリトライ処理
export async function retryPendingFidSync() {
  const pendingFid = localStorage.getItem(STORAGE_KEY);
  if (pendingFid) {
    try {
      await syncFidToServer(pendingFid);
      localStorage.removeItem(STORAGE_KEY);
    } catch (e) {
      // 次回起動時またはオンライン復帰時に再試行
    }
  }
}

注意点5:httpsCallable() による自動的な旧トークン登録

実はFirebase Functions(第2世代含む)の httpsCallable() を使用している環境では、SDK内部のコンテキスト設定やアプリチェック等の処理で、自動的に内部の messaging.getToken() が呼ばれてしまう仕様(バグに近い挙動)があります。
これにより、フロントエンド側でせっかく新方式のFIDへ移行しても、Functionsを実行した瞬間に強制的に旧トークンがFCMサーバーへ再登録され、FIDでの送信が messaging/installation-id-not-registered エラーで失敗する不整合が起きます。

対策:
FCM移行を行う際は、WebPush通知を呼び出すAPI(およびFCM関連情報をやり取りするAPI)との通信において httpsCallable() の使用を避け、生 fetch または axios などを利用して、自前のAPIエンドポイントと認証ヘッダーを用いて通信を行う構成を推奨します。

注意点6:IndexedDB の破壊的スキーマ変更とデータベースバージョン上昇 (v1 ➔ v2)

FID方式に移行すると、Firebase SDKがブラウザ内部でトークン情報をキャッシュするために使用する IndexedDB(firebase-messaging-database)の構成が裏側で破壊的に変更されます。

項目 従来方式(Before / v1) 新方式(After / v2)
データベースバージョン 1 2
firebase-messaging-store token (FCMトークン文字列)
createTime
subscriptionOptions(VAPIDキー、エンドポイント等)
レコード数: 0 (空になり、以降使用されません)
firebase-messaging-fid-registration-store (存在しない) fid (Firebase Installation ID)
lastRegisterTime
vapidKey

開発上の注意点

従来のFCMトークンを、クライアント側でローカルの IndexedDB から直接読み取って自社サーバーへ送信するようなハック的な実装をしているレガシーコードが存在する場合、移行後は該当ストアが空になり、例外エラーを吐いてスクリプトの実行が止まる原因になります。
FCMトークンの IndexedDB からの直接参照は絶対に避け、必ずSDK公式のAPI(新方式では onRegistered()、旧方式では getToken())を経由してください。


5. 安全に本番環境を移行するための「段階的移行プロセス」

本番システムで無停止かつ安全にトークンからFIDへ移行するための推奨プロセスです。

ステップ1:サーバーとDBの準備(最優先)

※本セクションで提示するDB構成やコードは、移行イメージを説明するための擬似コードおよび概念図です。実際のシステム構成や使用するデータベースエンジンに合わせて調整してください。

多くの既存システムでは、DBに user_idfcm_token のペアだけを保存しており、ブラウザのインストールインスタンスを一意に特定する installationKey のような概念が存在しません。そのため、単純に新FIDを受け取っても、どの旧トークンを無効化(retired)すべきかの突合が困難です。

この課題を解決するため、以下のいずれかのアプローチでDBを拡張し、新旧の紐付けを管理します。

  1. クライアント主導の紐付け: クライアント側の移行処理時に、ローカルに残っている旧トークンと新規に生成されたFIDの両方を一度にサーバーに送信し、サーバー側でマッピングを完了させる。
  2. ユーザー単位での一括移行: サーバー側で user_id 単位で移行を行う(同一ユーザーが複数端末を持つ場合、一時的に重複送信が発生する可能性がありますが、最も確実に移行できます)。

以下は、このマッピングを処理するためのDB構造およびステータス管理の設計例(擬似コード)です。

interface MessagingRegistration {
  installationKey: string; // 同一ブラウザ(インストールインスタンス)を識別するキー
  targetType: "token" | "fid";
  targetValue: string; // トークン文字列またはFID
  status: "pending" | "active" | "retired" | "invalid";
  updatedAt: Date;
}
  • installationKey ごとに active にできるレコードは最大1件と制約します。
  • クライアントからFIDが届いた場合、移行完了(トピックの再購読完了など)までは旧トークンを active、新FIDを pending として共存させます。

ステップ2:トピック(Topic)購読の注意点(FIDは非対応)

最重要の注意点:
Firebase Admin SDK の subscribeToTopic() および unsubscribeFromTopic() は、FCM登録トークン(Registration Token)のみを受け付けます。FID(Installation ID)を直接渡してトピックに登録することはできません(エラーになります)。

トピックによる一斉配信機能(Topic Messaging)を利用している場合、完全にFIDへの一本化を行うことはできません。以下のいずれかの代替アプローチを選択する必要があります。

アプローチA:トピック配信を廃止し、自社DB+マルチキャスト送信(FID指定)に移行する

トピックの購読・管理をFCMサーバーに任せるのをやめ、自社データベースでトピックに相当する「配信グループ」と「FID」の紐付けを管理します。

  1. クライアントから送信されたFIDを、自社DBの配信グループ(例:topics テーブルなど)に保存する。
  2. 該当グループに送信する際、DBから対象のFIDリストを抽出し、Admin SDKの sendEachForMulticast() を使用してFID宛てにメッセージを送信する(FIDでのマルチキャスト送信はAdmin SDK v14.1.0以降でサポートされています)。

アプローチB:トピック購読用に従来の getToken()(登録トークン)の取得と送信を維持する

トピック購読が必要なクライアントに限り、従来の登録トークンを取得・管理するロジックを一部維持します。

  1. クライアントで getToken() を呼び出して登録トークンを取得し、サーバーへ送信する。
  2. サーバーでその登録トークンを用いて subscribeToTopic() を呼び出す。
  3. トピック配信は従来の登録トークン宛て(message.token)、個別の動的配信は新方式のFID宛て(message.fid)、というように宛先を使い分けます。

アプローチBを採用する場合の重要注意点
この場合も「注意点3:新旧APIの混在NG」の原則は変わらず適用されます。同一ブラウザ環境(同一アプリインスタンス)で register()getToken() の両方を連続して呼び出すと、IndexedDBの内部状態が競合して破損します。
アプローチBを採用する際は、そのクライアントバージョンでは「トピック配信用(トークン方式)のみを使い続ける」など、処理の排他制御を徹底してください。

宛先の移行において「通知が届かない期間(欠落)」を避けるため、先に新宛先(FID)での配信体制を整えてから、旧登録トークンを解除または無効化(retired)する順序が鉄則です。

ステップ3:フロントエンドのFeature Flagによる段階リリース

※本セクションで提示するリリース管理やFeature Flagの手法は、移行プロセスを説明するための擬似的な解説です。

フロントエンド側で、installationKey に基づくFeature Flag(コホート制御)を使用し、1% -> 10% -> 50% -> 100% と段階的にFID方式を有効化します。

注意:
Feature Flagの割り当てが変更されたり、問題発生時にロールバック(切り戻し)を行ったりする場合、同一クライアントで「FID方式(新)」と「トークン方式(旧)」が交互に実行される可能性があります。これにより「注意点3:新旧APIの混在による『追跡不能トークンのゾンビ化』」が発生するため、以下の対策を行ってください。

  • コホート(配信セグメント)の割り当ては一度決定したら固定(Sticky)にする。
  • ロールバックを適用する際は、クライアント側でローカルストレージに保存されているFCM関連情報をクリアしてから旧方式で再登録する。

問題が発生した場合は直ちにFeature Flagをオフにし、未移行のクライアントを安全に従来の登録トークン方式のまま維持できるようにします。


まとめと次のステップ

Firebase Cloud Messagingの新方式(FID方式)は、Webプッシュ通知の自動更新監視をSDKが肩代わりしてくれる強力なアップデートです。しかし、移行プロセスにおける挙動の違い(特に unregister() のPushSubscription未解除など)を知らずに移行すると、重大な不具合に直面します。

まずはバックエンドDBに installationKeytargetType を追加するデータ構造の整理から始め、安全な段階的移行を計画していきましょう!


おわりに

FCMの新方式(FID)への移行は、IndexedDBの状態管理やサーバー側DBのデータ構造の拡張など、設計の難易度は上がります。しかし、Webプッシュ通知で最も壊れやすかった「トークンの自動更新と失効検知」をSDK側で安全に解決できるようになるメリットは計り知れません。

本番運用の移行計画を立てる際の参考になれば幸いです。役に立ったと感じた方は、いいねストックをしていただけると励みになります!


参考ドキュメント

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?