2014年から個人で iPhone と Android のアプリを作り続けてきました。累計ダウンロードは 5,000万を超えていますが、その裏で一番試行錯誤してきたのは「プッシュ通知の文面」です。同じ機能を案内するのでも、文面ひとつで開封率が 0.8% から 4.2% まで変わります。実際の運用で、定型文の通知だけに頼っていたアプリは、ある時期から開封率が頭打ちになり、いくら配信頻度を変えても改善しなくなりました。
その壁を破ったきっかけが、Gemini 3 Pro と Firebase Cloud Messaging(以下 FCM)の組み合わせです。ユーザーごとの最終操作、滞在時間、好みのコンテンツジャンルを軽量な特徴量にして Gemini に渡し、文面だけを生成して FCM で配信するパイプラインを夜間バッチで回すだけで、平均開封率が 1.5 倍前後に持ち直しました。
ここでまとめるのは、その設計を「個人開発者が現実的に運用できる形」に削ぎ落とした本番ガイドです。Cloud Scheduler から Cloud Functions、Gemini 3 Pro、FCM までの構成、ユーザーセグメントの作り方、PII(個人情報)を Gemini に流さないためのガードレール、A/B テストでプロンプトを改善する方法、月いくらで何が変わるかの試算まで、実装コード付きで整理しています。
なぜ「定型文のプッシュ通知」では限界が来るのか
FCM のデフォルトは「全員に同じ文面」です。新機能のリリース告知や、休眠ユーザーへの呼び戻しで一斉配信を続けると、ユーザー側の通知許可率と開封率が緩やかに下がっていきます。Apple の通知設定 UI が高度化し、Android 16 以降ではユーザーが「通知の重要度」を細かく調整できるようになったことも、定型文の限界を早めています。
私自身が運営している壁紙アプリでは、過去 12 ヶ月の通知ログを分析したところ、以下のような傾向がはっきり見えました。
「お気に入り登録から 3 日以内」のユーザーには新着告知が刺さるが、それ以降では効果が急減する
「課金経験あり」のユーザーには新作の告知より、過去に保存した壁紙のテーマに似た新作の告知のほうが反応する
「最終起動から 7 日以上」のユーザーには、機能告知よりも具体的な作品名を入れた文面のほうが復帰率が高い
つまり、必要なのは「メッセージそのもののパーソナライズ」です。ユーザーごとに別の SQL を組むのではなく、特徴量を渡せば文面を返してくれるサーバーレスな仕組みがあれば、個人開発でも回せます。Gemini 3 Pro は構造化出力と日本語表現の自然さが両立しているため、まさにこの用途に向いています。
アーキテクチャ全体像
私が運用している構成は次の通りです。すべて Google Cloud と Firebase の枠内で完結し、個人開発者でも月額数千円程度で運用できます。
Cloud Scheduler が日次(または週次)でジョブをキックする
Cloud Functions(第2世代・Python)がトリガーされ、Firestore からセグメント別ユーザーリストを取得する
各セグメントに対して Gemini 3 Pro API を呼び、構造化された通知文面(タイトル+本文+ディープリンク)を生成する
生成結果を Firestore の notifications コレクションに保存する(送信前のレビュー用)
同じ Cloud Functions(または別ジョブ)が notifications を読み、FCM の HTTP v1 API でバッチ配信する
開封・タップは analytics_label をキーに BigQuery(Firebase 連携)で集計して A/B テストに反映
宮大工だった両祖父から受け継いだ「手を動かすことが一つの信心」という感覚で、最初は手動配信を1ヶ月続けて、自分の感覚で文面の善し悪しが分かるようになってから自動化しました。先に自動化してしまうと、「なぜこの文面が刺さったのか」という根拠が掴めません。個人開発の規模では、自動化は人間の勘が追いついた後で十分に間に合います。
ユーザーセグメントの設計
通知パーソナライズの土台はセグメント設計です。私はセグメントを「行動 × 状態 × 嗜好」の 3 軸で 6〜10 個に絞っています。これ以上細かくすると、個人開発のリソースでは検証回数が確保できません。
実際の壁紙アプリで使っているセグメントの例:
new_user_active: インストールから 3 日以内、起動回数 3 回以上
new_user_silent: インストールから 3 日以内、起動回数 1 回のみ
engaged_free: 過去 30 日に 5 回以上起動、未課金
engaged_paid: 過去 30 日に 5 回以上起動、課金経験あり
dormant_paid: 過去 30 日起動なし、過去課金経験あり
dormant_free: 過去 30 日起動なし、未課金
これらのセグメントには、それぞれ「Gemini に渡す特徴量」のセットを別に用意します。たとえば engaged_paid なら、ユーザーが直近 7 日でお気に入りに追加した壁紙のテーマタグ(例: nature, monochrome, abstract)を 3 件渡し、Gemini にそのテーマに沿った新着の通知文を生成してもらいます。
Gemini に渡す「特徴量」とプロンプト設計
ここで一番重要な原則を最初に書きます。Gemini にユーザーの個人情報(名前、メールアドレス、デバイス ID など)を絶対に渡してはいけません。 ハッシュ化したセグメント ID と、行動を抽象化した特徴量だけを渡します。これは PII 保護だけでなく、プロンプトのキャッシュ効率を上げるためにも有効です。
良い特徴量の例(個人を特定できない、抽象化された値):
segment: engaged_paid
top_themes: ["nature", "abstract"]
last_active_days_ago: 2
preferred_language: ja
recent_action_summary: saved_3_wallpapers_in_last_7_days
悪い特徴量の例:
email: user@example.com
device_token: <生のFCMトークン>
purchase_history: 個別の決済データ
プロンプトは「日本語版」「英語版」を別々に作り、Gemini 3 Pro の responseSchema で JSON 構造を強制します。返ってきた文面はそのまま FCM に渡せる形(タイトル 30 文字以内、本文 70 文字以内)にします。
動作するコード(Cloud Functions・Python)
以下は Cloud Functions(第2世代)で動作する実装の中核部分です。google-genai SDK と firebase-admin を使い、Gemini 3 Pro で文面生成、FCM で配信、Firestore で送信ログ記録を一気通貫で行います。
# main.py — Cloud Functions (Gen2) entry point
# 必要パッケージ: google-genai, firebase-admin, functions-framework
import json
import os
import logging
from datetime import datetime, timezone
from typing import Any
import functions_framework
from google import genai
from google.genai import types
from firebase_admin import initialize_app, firestore, messaging
# --- 初期化(コールドスタート時に一度だけ) ---
initialize_app()
db = firestore.client()
client = genai.Client( api_key = os.environ[ "YOUR_GEMINI_API_KEY" ])
MODEL = "gemini-3-pro"
# 通知文面の構造化出力スキーマ
NOTIFICATION_SCHEMA = {
"type" : "object" ,
"properties" : {
"title" : { "type" : "string" , "maxLength" : 30 },
"body" : { "type" : "string" , "maxLength" : 70 },
"deep_link" : { "type" : "string" },
"analytics_label" : { "type" : "string" },
},
"required" : [ "title" , "body" , "deep_link" , "analytics_label" ],
}
def build_prompt (features: dict[ str , Any]) -> str :
"""ユーザー個人情報を含めず、抽象化された特徴量だけを渡す"""
return f """あなたは壁紙アプリのプッシュ通知コピーライターです。
以下の「セグメント特徴量」だけを根拠にして、開封したくなる通知文面を1つだけ生成してください。
セグメント特徴量:
- segment: { features[ 'segment' ] }
- top_themes: { json.dumps(features[ 'top_themes' ], ensure_ascii = False ) }
- last_active_days_ago: { features[ 'last_active_days_ago' ] }
- preferred_language: { features[ 'preferred_language' ] }
- recent_action_summary: { features[ 'recent_action_summary' ] }
制約:
- title は30文字以内、body は70文字以内
- 命令調・煽り表現は避け、誠実なトーンで書く
- 絵文字は0〜1個まで(無くても良い)
- analytics_label は "notif_<segment>_<YYYYMMDD>" 形式
- deep_link は "myapp://gallery?theme=<最初のtheme>" 形式
"""
def generate_message (features: dict[ str , Any]) -> dict[ str , Any]:
"""Gemini 3 Pro で文面を生成"""
response = client.models.generate_content(
model = MODEL ,
contents = build_prompt(features),
config = types.GenerateContentConfig(
response_mime_type = "application/json" ,
response_schema = NOTIFICATION_SCHEMA ,
temperature = 0.8 ,
max_output_tokens = 400 ,
),
)
return json.loads(response.text)
def send_fcm (token: str , message: dict[ str , Any]) -> str :
"""FCM HTTP v1 API で配信"""
fcm_msg = messaging.Message(
token = token,
notification = messaging.Notification(
title = message[ "title" ],
body = message[ "body" ],
),
data = {
"deep_link" : message[ "deep_link" ],
"analytics_label" : message[ "analytics_label" ],
},
fcm_options = messaging.FCMOptions(
analytics_label = message[ "analytics_label" ],
),
)
return messaging.send(fcm_msg)
@functions_framework.http
def dispatch_notifications (request) -> tuple[ str , int ]:
"""Cloud Scheduler から呼ばれるエントリポイント"""
payload = request.get_json( silent = True ) or {}
segment = payload.get( "segment" , "engaged_paid" )
users = (
db.collection( "user_features" )
.where( "segment" , "==" , segment)
.where( "notifications_enabled" , "==" , True )
.limit( 1000 )
.stream()
)
sent, errors = 0 , 0
for user_doc in users:
features = user_doc.to_dict()
try :
msg = generate_message(features)
send_fcm(features[ "fcm_token" ], msg)
db.collection( "notifications" ).add({
"user_id_hash" : features[ "user_id_hash" ],
"message" : msg,
"sent_at" : datetime.now(timezone.utc),
"segment" : segment,
})
sent += 1
except Exception as e:
logging.exception( "dispatch failed: %s " , e)
errors += 1
return f "sent= { sent } errors= { errors } " , 200
このコードのポイントは 3 つあります。第一に、generate_message のプロンプトには fcm_token のような PII を一切渡していません。第二に、response_schema で JSON を強制することで、文字数オーバーや構造崩れによる FCM 配信失敗を未然に防いでいます。第三に、fcm_options.analytics_label を必ず付け、後で BigQuery で開封率を集計できるようにしています。
レート制限とリトライ — 個人開発でこそ重要
Gemini 3 Pro の有料ティアでも RPM(1分あたりリクエスト数)には上限があります。1,000 ユーザーへの配信を素直に直列で回すと、配信完了までに 10〜15 分かかったり、途中で 429 エラーが出たりします。私が落ち着いた構成は次の通りです。
1 ユーザーずつではなく「同じセグメントの 50 人」をまとめて 1 リクエストにする(プロンプトに「50 個のバリエーションを返してください」と指示する)
それでも 429 が返ったら指数バックオフ(1秒、2秒、4秒…)で 4 回まで自動リトライ
リトライ後も失敗したユーザーは Firestore の failed_notifications コレクションに記録し、翌日に再試行
Cloud Functions のメモリは 512MB、タイムアウトは 540 秒に設定(無料枠を超えにくい範囲)
「50 人まとめて生成」にすると、1 回の API コストはやや増えますが、トータルのコストは 1/40〜1/50 になります。私の壁紙アプリ(DAU 約 8,000)では、このバッチ化だけで月の Gemini 利用料が 4,800 円から 110 円程度まで下がりました。
配信時刻の最適化 — Gemini に「いつ送るか」を委ねない
文面のパーソナライズは Gemini に任せて良いのですが、配信時刻の決定は Gemini に投げないでください。 時刻は確率分布の問題なので、ルールベースの方が精度が高く、コストもゼロです。
私が使っているシンプルなルールはこうです。
ユーザーのタイムゾーン(OS から取得して Firestore に保存済み)の朝 8 時 / 昼 12 時 / 夜 19 時のうち、過去の起動時刻ヒストグラムの最頻値に最も近い時刻を選ぶ
ヒストグラムが取れていない新規ユーザーは「夜 19 時」固定
平日 / 休日で異なる場合があるため、曜日でも分岐
このルールを Cloud Functions の中で実装し、messaging.send() の代わりに FCM の apns.headers["apns-push-type"] = "background" と apns.payload.aps.badge を組み合わせた予約配信、または Cloud Tasks でスケジュールキックします。
A/B テストでプロンプトを改善する
パーソナライズの精度は「プロンプトをいかに改善し続けるか」で決まります。私のおすすめは、以下のシンプルな A/B テストです。
同じセグメントを prompt_v1 と prompt_v2 の 2 群に半々で割り当てる(ユーザーIDのハッシュ末尾の偶奇で決定すれば再現性がある)
それぞれに analytics_label を notif_<segment>_v1_<日付> notif_<segment>_v2_<日付> と分ける
BigQuery で firebase_messaging テーブルを SQL クエリし、開封率(message_open / message_delivered)を比較する
7 日間の集計で統計的に有意(少なくとも 1,000 配信ずつ)であれば勝者を採用、引き分けなら新たな v3 を試す
私の経験では、プロンプトの「制約条件」を増やすほうが、自由度を上げるよりも開封率は上がります。たとえば「煽り表現禁止」「敬体で書く」「具体的なテーマ名を1つ含める」を追加するだけで、CTR が 0.7 ポイント改善した例があります。
個人情報のガードレール — Gemini にも FCM にも渡してはいけないもの
これは個人開発であっても絶対に守るべき設計です。日本の個人情報保護法、GDPR、Apple の App Store Review Guidelines のいずれも、ユーザーの個人情報を AI モデルに学習目的で送ることを禁止または制限しています。Gemini API は学習に使われない契約条件になっていますが、それに依存せず、最初から PII を送らない設計にすべきです。
PII を送らないための実装ルール:
Firestore の user_features コレクションは「行動の集計値」だけを持つ。生のイベントログは別コレクションに分離する
ユーザー ID は SHA-256 でハッシュ化したものだけを Gemini への入出力で使う
fcm_token は Firestore の secrets/fcm_tokens に分離保存し、Gemini を呼び出す関数からは参照不可にする
Gemini への入力ペイロードを Cloud Logging に記録する場合は、ペイロード全体ではなく「セグメント名」「特徴量のキー一覧」だけを記録する
これらは Apple の App Store 審査でも質問されやすいポイントです。「お客様の個人情報を AI に送っているか?」という設問に「No」と明確に答えられる設計にしておくと、リジェクトリスクを下げられます。
コスト試算 — 個人開発で現実的か
私の壁紙アプリの規模で、月にいくら掛かるかを実測値で示します。これは DAU 約 8,000、週 3 回配信、各配信で 2,000〜4,000 ユーザーへ通知を送るケースです。
Gemini 3 Pro API(50 ユーザーまとめ生成 × 週 3 回 × 4 週): 月額 約 110 円
Cloud Functions 第2世代(実行回数 約 2,000 回 / 月 / 各 5 秒): 月額 約 80 円
Cloud Scheduler(ジョブ 4 個): 月額 0 円(無料枠内)
Firestore 読み書き(約 50,000 操作): 月額 0 円(無料枠内)
FCM 配信: 月額 0 円(FCM は無料)
合計で 月額 約 200 円 で運用できています。文面のパーソナライズで開封率が 1.5 倍になれば、AdMob や課金転換でこの 200 円は容易に回収できます。実際、私のアプリでは AdMob のインプレッション収益が月 7,000 円ほど増えました。
参考に、私が個人開発を始めた 2014 年頃の AdMob ピーク収益は月 150 万円を超えていましたが、その当時はまだプッシュ通知のパーソナライズという概念が個人開発者の手の届く範囲にありませんでした。Gemini 3 Pro の登場で、当時の試行錯誤が「設計可能な領域」に入ってきたのは、静かに大きな変化だと感じています。
よくある落とし穴
実装と運用の両面で、私が引っかかったポイントを共有します。
落とし穴1: responseSchema の maxLength が守られないことがある
Gemini 3 Pro でも、まれに maxLength を超える出力が返ります。Cloud Functions 側で必ずバリデーションし、超過時は title[:30] のように切り詰めるか、再生成するフォールバックを入れてください。私は最初これを軽視して、本文 90 文字の通知が iOS のロック画面で見切れる事故を起こしました。
落とし穴2: FCM トークンの期限切れを無視するとエラーが累積する
FCM トークンは、ユーザーがアプリを再インストールすると無効になります。messaging.send() が UNREGISTERED を返したら即座に Firestore の該当ユーザーから fcm_token を削除する処理を入れてください。これを怠ると、毎回 1,000 件の配信のうち 30〜50 件が失敗し続けることになります。
落とし穴3: Gemini の出力に絵文字が混じると iOS の通知センターで文字化けする
絵文字 1 個までは許容していますが、Gemini が稀に 4 個以上入れてくることがあります。これはプロンプトの「制約」セクションに「絵文字は0〜1個まで」と書いていても起きます。Cloud Functions 側で正規表現で絵文字数を数え、2 個以上なら 1 個だけ残す後処理を入れています。
落とし穴4: A/B テストの群分けを毎日変えると比較不能になる
ユーザーIDのハッシュで群を決めるとき、ハッシュ関数に日付を混ぜてはいけません。同じユーザーが日によって v1 と v2 を行き来すると、開封率の差がプロンプトの差なのかユーザーの差なのか分からなくなります。テスト期間中はハッシュ式を固定してください。
BigQuery で運用を見える化する 3 つのクエリ
パイプラインが回り始めたら、毎週月曜の朝に必ず見ているクエリを共有しておきます。Firebase の BigQuery エクスポートを有効にすれば、以下のクエリだけで開封率・A/B テスト勝敗・無効トークン率の 3 つを把握できます。
-- セグメント別の開封率(直近7日)
SELECT
REGEXP_EXTRACT(analytics_label, r '^notif_([^_]+)_' ) AS segment,
COUNTIF(message_type = 'MESSAGE_DELIVERED' ) AS delivered,
COUNTIF(message_type = 'MESSAGE_OPEN' ) AS opened,
SAFE_DIVIDE(
COUNTIF(message_type = 'MESSAGE_OPEN' ),
COUNTIF(message_type = 'MESSAGE_DELIVERED' )
) AS open_rate
FROM `your-project.firebase_messaging.data`
WHERE _PARTITIONTIME >= TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 7 DAY )
GROUP BY segment
ORDER BY open_rate DESC ;
-- A/B 勝敗(直近14日)
SELECT
REGEXP_EXTRACT(analytics_label, r '_(v\d)_' ) AS variant,
REGEXP_EXTRACT(analytics_label, r '^notif_([^_]+)_' ) AS segment,
COUNTIF(message_type = 'MESSAGE_OPEN' ) AS opens,
COUNTIF(message_type = 'MESSAGE_DELIVERED' ) AS deliveries
FROM `your-project.firebase_messaging.data`
WHERE _PARTITIONTIME >= TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 14 DAY )
AND analytics_label LIKE 'notif_%_v%'
GROUP BY variant, segment
ORDER BY segment, variant;
-- 無効トークン率(高ければ削除処理が漏れているサイン)
SELECT
DATE (event_timestamp) AS day ,
COUNTIF(message_type = 'MESSAGE_FAILED' AND error = 'UNREGISTERED' ) AS stale,
COUNTIF(message_type = 'MESSAGE_DELIVERED' ) AS delivered
FROM `your-project.firebase_messaging.data`
WHERE _PARTITIONTIME >= TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 30 DAY )
GROUP BY day
ORDER BY day DESC ;
私自身、3 つ目のクエリを作るまで、ゆっくり蓄積していた UNREGISTERED エラーを 3 ヶ月放置していました。アラートのしきい値には届かないけれど、毎日の有効リーチを 6〜8% 削っていました。直したのは Cloud Functions の例外ハンドラに db.collection("user_features").document(user_id).update({"fcm_token": firestore.DELETE_FIELD}) を 1 行足しただけです。「自分が定期的に走らせるクエリに乗っていない指標は、存在しないのと同じ」 という教訓を、その時に得ました。
段階的ロールアウト — 個人開発のカナリーパターン
初めてパーソナライズを本番で全ユーザーに当てたくなりますが、これは止めたほうが無難です。プロンプトが微妙に外している場合、通知をオフにしたユーザーはほぼ戻ってきません。私は 5% → 20% → 100% のカナリーリリースを必ず挟みます。
実装は dispatch_notifications の冒頭に判定を 1 つ追加するだけです。
import hashlib
def in_canary (user_id_hash: str , percent: int ) -> bool :
"""ユーザーIDハッシュで安定的にカナリー判定"""
h = int (hashlib.sha256(user_id_hash.encode()).hexdigest(), 16 )
return (h % 100 ) < percent
# dispatch_notifications の中:
CANARY_PCT = int (os.environ.get( "CANARY_PCT" , "5" ))
if not in_canary(features[ "user_id_hash" ], CANARY_PCT ):
send_legacy_notification(features[ "fcm_token" ], segment)
continue
CANARY_PCT を環境変数にしておけば、Cloud Console から再デプロイなしで段階を上げられます。私はこのパターンで 2 回、5% 段階で「微妙にズレた文面」を発見し、全ユーザー配信前に止められた経験があります。
なぜ OneSignal や Braze を選ばなかったか
「プッシュ通知を本格運用する」と検索すると、OneSignal、Braze、Iterable、CleverTap などのマネージドサービスが上位に出てきます。どれも素晴らしいプロダクトですが、価格設定が個人開発者向けではありません。
ざっくりした目安は次の通りです。
OneSignal: 10K MAU まで無料、その先 AI パーソナライズは Growth プラン(月額約 USD 99〜)
Braze: 中堅企業以上を想定した価格帯、個人では現実的でない
Iterable: Braze と類似、エンタープライズ営業
CleverTap: AI 機能は上位プランバンドル
個人開発で複数の小〜中規模アプリを抱えている立場としては、月 100 ドル超の機能を月 200 円で再現できるなら自前実装を選ぶのが合理的です。トレードオフは「自分でメンテナンスする」点ですが、Cloud Functions と Firestore に既に慣れているなら、その手間は学習コストとしても十分回収できます。
何より、自分で書いた制約のもとで生成された文面を送る、という感覚が個人開発では大事だと私は感じています。モデルは黒箱ではなく「指導下のジュニアコピーライター」になり、運用が長続きする手触りが残ります。
プロンプト改善の Before/After 実例
理論だけでは伝わりにくいので、壁紙アプリで実際に行ったプロンプト改善を 1 つだけ共有します。最初のプロンプトはこうでした。
あなたは壁紙アプリのプッシュ通知ライターです。
親しみやすく、興味を引く文面を書いてください。
ユーザーセグメント: {segment}
好きなテーマ: {top_themes}
このプロンプトでの 7 日間 1,000 配信あたりの開封率は 2.1% でした。
改善後のプロンプトはこちらです。
あなたは壁紙アプリ「Dolice」のプッシュ通知コピーライターです。
目的: ユーザーが既に好きなテーマの新作を 1 つだけ案内し、アプリを開いてもらう
絶対的な制約:
- title: 18〜26 文字
- body: 40〜65 文字、敬体(です/ます)で書く
- {top_themes} のうち 1 つを必ず固有名詞として含める
- 「!」「絵文字」「全部大文字」を使わない
- 効果や品質を保証する語(「絶対」「最高」「必見」など)を使わない
- analytics_label: notif_{segment}_v2_{date}
入力:
- segment: {segment}
- top_themes: {top_themes}
- last_active_days_ago: {last_active_days_ago}
- recent_action_summary: {recent_action_summary}
7 日間の開封率は 3.4% に改善しました。改善の主因は「自由度を増やしたこと」ではなく「制約を増やしたこと」です。「親しみやすく」という曖昧な指示は Gemini に過剰な選択肢を与え、出力の振れ幅が大きくなります。具体的な禁止事項を積み重ねるほど、出力はアプリの世界観に近い帯に収束します。
私が十数回のイテレーションから取り出した一般原則は次の通りです。プロンプトは「やってほしいこと」を増やすより「やってほしくないこと」を増やすほうが質が上がる。 「Do X」は願望ですが、「Do not Y」は積み上がる制約です。前者より後者に時間を使うのが、個人開発でも応用できる定石だと考えています。
維持コスト — 週に何分かかるか
パーソナライズは「導入したら終わり」ではありません。毎週の私の運用ルーチンを参考までに共有します。
毎日(自動) : 配信ジョブが走り、BigQuery エクスポートが翌朝に揃う
月曜朝(手動 15 分) : 上記 3 つの SQL を順に実行し、セグメント開封率と無効トークン率を眺める
2 週間ごと(手動 30 分) : 最も成績が悪いセグメントのプロンプトを修正、または兄弟セグメントに統合
四半期ごと(手動 2 時間) : プロンプトのバージョンを刷新、90 日以上前の notifications ドキュメントをアーカイブ、目標値を再設定
合計で週 1 時間程度です。これなら他の機能開発と並行して、副業や本業のかたわらでも回せる運用負荷だと思います。
次にやってほしい1つのこと
ここまで読んでくださってありがとうございます。本番に乗せる前に、まず手動で 5 件だけパーソナライズ通知を作って、自分の感覚で「これは開きたくなるか?」を確かめてみてください。Gemini が出してきた文面を、自分のアプリの世界観に照らして眉をひそめずに送れるかどうか。その感覚が育ってから自動化すると、配信のたびに「自分が監督した通知」を出している感覚が残り、運用が長続きします。
私自身も Cloud Functions のリトライ設計を組むときに何度か読み返した一冊です。
関連する深掘り記事として、Cloud Tasks でジョブキューを設計する Gemini API × Cloud Tasks 非同期 AI ジョブキュー本番ガイド と、レート制限の現実的な対処法を整理した Gemini API のレート制限・クォータ管理 本番ガイド を一緒に読むと、本記事のアーキテクチャをそのまま運用に持ち込みやすくなります。
私自身もまだプッシュ通知のパーソナライズ運用を改善し続けている途中です。アプリごとにユーザー像が違うので、ここに書いた構成も完成形ではありません。皆さんのアプリで動かしてみた結果が、また新しい設計のヒントになれば嬉しいです。