Files
jrshikoku/docs/background-rikka-backend-push-plan.md

22 KiB
Raw Permalink Blame History

バックグラウンドりっかちゃん通知・Live Activity バックエンド連携計画(素案)

ステータス: 相談用ドラフト 作成日: 2026-07-19 目的: n8n・Expo Push・APNs・ActivityKitを利用し、アプリが前面にいない状態でも列車追従アナウンスとDynamic Island更新を成立させる。

1. 結論

既存の通知システムは大部分を再利用できる。

  • 通知対象の登録、購読リスト管理、列車位置の監視、送信判定はn8nを継続利用する。
  • 通常の表示通知は、まず既存のExpo Push経路を利用する。
  • Dynamic IslandLive Activity)のリモート更新だけは、ActivityKit専用Push Tokenを使い、n8nまたは専用APIからAPNsへ直接送信する。
  • 端末で動的生成したりっかちゃん音声をExpo Push経由で再生できるかは、先に実機PoCで確認する。
  • Expo経由で動的通知音が安定しない場合は、通常通知もAPNs直接送信へ切り替える。

想定する最終構成は次のとおり。

アプリ
  ├─ ExpoPushToken ───────────────┐
  ├─ ActivityKit Push Token ─────┤
  ├─ 追従列車・運行日・有効期限 ─┤
  └─ 端末に準備済みの音声一覧 ──┤
                                    ↓
                                  n8n / DB
                                    ↑
                            列車走行位置情報API
                                    │
                         次駅変化・到着接近を判定
                                    │
                  ┌─────────────────┴─────────────────┐
                  ↓                                   ↓
        Expo Push Service                     APNs直接送信
        通常の表示・音声通知                  Live Activity更新
                  ↓                                   ↓
        「次は、○○です。」                    Dynamic Island更新

2. 実現したいユーザー体験

2.1 列車追従モード

  1. ユーザーがアプリで列車を選び、列車追従を開始する。
  2. 追従開始時に、経路上の駅について「次は、○○です。」の音声を端末内へ準備する。
  3. アプリは追従条件とPush Tokenをバックエンドへ登録する。
  4. アプリがバックグラウンドまたは終了状態でも、バックエンドが列車走行位置を監視する。
  5. 次駅が変化したとき、表示通知とりっかちゃん音声を配信する。
  6. 開始済みのLive Activityが存在する場合、Dynamic Islandも同じ状態へ更新する。
  7. 終着、日付変更、ユーザー操作、有効期限切れのいずれかで追従を終了する。

2.2 駅固定モード

駅固定モードも同じ購読基盤へ載せられるが、最初のリリースでは列車追従モードを優先する。

将来は以下のイベントを対象にできる。

  • 対象駅への列車接近
  • 発車時刻または発車検知
  • 一定以上の遅延発生
  • 番線、行先、運休等の重要な変更

3. 「アプリを起動していない状態」の定義

状態によって実現方法が異なるため、仕様上は明確に区別する。

アプリ状態 表示・音声Push通知 開始済みLive Activityの更新 新規Live Activity開始
フォアグラウンド 可能 端末内更新・Push更新とも可能 可能
バックグラウンド 可能 APNs Pushで可能 別途検討
ユーザーがアプリを終了 原則可能 Activityが存続中ならAPNs Pushで可能 初期版では対象外
端末再起動後 通知許可等に依存 既存Activityの状態に依存 初期版では対象外

初期版の「アプリを起動していない」は、追従登録とLive Activity開始を一度アプリ上で行った後、アプリがバックグラウンドまたは終了状態になった場合を指す。

バックエンドが表示通知を送る方式では、通知到着時にアプリのJavaScriptを起動する必要はない。そのため、本機能だけを理由にiOSの UIBackgroundModesaudioremote-notification を追加しない。

4. 現在の実装と流用範囲

4.1 既に存在するもの

  • expo-notifications によるExpo Push Token取得
  • n8nを介した通知対象リストとExpo Push一斉送信の運用
  • 列車追従・駅固定のLive Activityネイティブモジュール
  • NSSupportsLiveActivitiesNSSupportsLiveActivitiesFrequentUpdates
  • Voicepeak APIによる駅名音声のWAV生成
  • WAVをiOSの Library/Sounds へ保存するネイティブ処理
  • 駅名から決定的な通知音ファイル名を作る処理
  • 端末位置方式と列車走行位置方式の設定切替

4.2 現在の制限

  • Live Activityは pushType: nil で開始され、ActivityKit Push Tokenを取得していない。
  • 列車走行位置方式の次駅判定は、アプリのJavaScriptが動作して情報更新を受けている間しか安定して動かない。
  • 動的生成した Library/Sounds 内のWAVを、Expo Push Serviceがカスタム通知音として確実にAPNsへ転送するか未検証。
  • 通知登録に使うID、購読の有効期限、重複送信防止、Push Receipt処理の正式なデータモデルが未整備。

5. 配信経路の設計

5.1 通常通知

第一候補は既存経路を維持する。

n8n → Expo Push Service → APNs → iPhone

送信例:

{
  "to": "ExponentPushToken[...]",
  "title": "列車追従・りっかちゃん",
  "body": "次は、坂出です。",
  "sound": "rikka-next-1a2b3c.wav",
  "priority": "high",
  "ttl": 60,
  "data": {
    "schemaVersion": 1,
    "type": "train-follow-announcement",
    "subscriptionId": "sub_xxx",
    "trainId": "123D",
    "serviceDate": "2026-07-19",
    "nextStation": "坂出",
    "eventId": "123D:2026-07-19:next:坂出"
  }
}

同じ駅名は全端末で同じファイル名にする。実際の音声内容は各端末がユーザーのVoicepeak設定で生成するため、サーバーは話者別の音声データを保持しなくてよい。

ただし、Pushを有効化する前に端末側で対象音声の保存完了を確認する。音声未準備の駅については、次のいずれかを仕様として選択する。

  1. 通常の通知音へフォールバックする。
  2. 音声なしで表示通知だけ送る。
  3. 追従開始を失敗としてユーザーへ再試行を案内する。

初期案は 1の通常通知音フォールバック とする。

5.2 Live Activity / Dynamic Island

ActivityKitのリモート更新はExpo Push Tokenではなく、Activity単位のPush Tokenを使用する。

n8nまたはPush送信用API → APNs HTTP/2 → ActivityKit → Dynamic Island

必要なAPNsヘッダー:

apns-push-type: liveactivity
apns-topic: <Bundle ID>.push-type.liveactivity
apns-priority: 5 または 10

更新例:

{
  "aps": {
    "timestamp": 1784453400,
    "event": "update",
    "stale-date": 1784453520,
    "content-state": {
      "currentStation": "丸亀~宇多津",
      "nextStation": "宇多津",
      "delayMinutes": 3,
      "scheduledArrival": "18:42",
      "updatedAt": 1784453400
    }
  }
}

Live Activity終了時は event: "end" を送信し、最終表示内容とdismissal方針を定める。

5.3 Expo経由の動的音声が利用できない場合

通常通知もAPNs直接送信へ変更する。

n8nまたはPush送信用API
  ├─ APNs alert push       → 表示通知・Library/Sounds内の音声
  └─ APNs liveactivity push → Dynamic Island

Appleの通知仕様では、通知音はアプリバンドルまたはアプリコンテナの Library/Sounds 内から指定できる。一方、Expoの公式なカスタムサウンド手順はビルド時設定を前提としているため、この分岐はPoC結果で決定する。

6. アプリからバックエンドへ登録する項目

6.1 追従購読

{
  "schemaVersion": 1,
  "subscriptionId": "sub_xxx",
  "installationId": "install_xxx",
  "mode": "trainFollow",
  "trainId": "123D",
  "serviceDate": "2026-07-19",
  "routeId": "yosan",
  "destination": "高松",
  "expoPushToken": "ExponentPushToken[...]",
  "activity": {
    "activityId": "activity_xxx",
    "pushToken": "activitykit_token_hex",
    "environment": "production"
  },
  "preparedSounds": {
    "宇多津": "rikka-next-xxxx.wav",
    "坂出": "rikka-next-yyyy.wav"
  },
  "createdAt": "2026-07-19T18:00:00Z",
  "expiresAt": "2026-07-19T23:30:00Z"
}

6.2 必須項目

項目 用途
subscriptionId 購読の更新・停止・冪等性確保
installationId Expo TokenをユーザーIDとして扱わないための端末識別子
mode 列車追従・駅固定等の判別
trainId 監視対象列車
serviceDate 同じ列車番号の翌日混同防止
expoPushToken 通常通知の送信先
activity.pushToken Live Activity更新の送信先
preparedSounds 端末に存在する通知音名の確認
expiresAt 孤立した購読の自動削除

activity.pushToken は更新される可能性があるため、アプリは pushTokenUpdates を監視し、変化のたびにバックエンドへ再登録する。

7. バックエンドの状態モデル

購読とは別に、列車ごとの監視状態を保持する。

{
  "trainKey": "2026-07-19:123D",
  "lastPosition": "丸亀~宇多津",
  "currentStation": "丸亀",
  "nextStation": "宇多津",
  "delayMinutes": 3,
  "lastSourceUpdatedAt": "2026-07-19T18:39:30Z",
  "lastProcessedAt": "2026-07-19T18:39:31Z",
  "lastEventId": "123D:2026-07-19:next:宇多津"
}

7.1 重複防止

通知判定は単なるポーリング回数ではなく、決定的な eventId で管理する。

<serviceDate>:<trainId>:<eventType>:<stationId>

同一購読・同一 eventId の送信は一度だけとし、n8nの再実行やAPIの一時エラーによる重複アナウンスを防ぐ。

7.2 データ鮮度

列車位置情報が一定時間更新されていない場合、次駅変化を確定しない。

初期案:

  • 位置情報の取得周期: 1530秒
  • 情報の許容鮮度: 90秒
  • 同じ変化を2回連続で観測した場合に確定。ただし情報源がイベント時刻や連番を持つ場合は再検討する。
  • 終着または有効期限超過で監視終了

8. n8nワークフロー案

Workflow A: 購読登録・更新

  1. アプリから署名付きリクエストを受け取る。
  2. スキーマ、通知許可、運行日、有効期限を検証する。
  3. subscriptionId をキーにupsertする。
  4. 同じ端末・同じ列車の古い購読を無効化する。
  5. 登録結果とサーバー時刻を返す。

Workflow B: 列車位置監視

  1. 有効な購読を列車単位で集約する。
  2. 同じ列車の位置情報は一度だけ取得する。
  3. 前回状態と比較して、次駅・現在位置・遅延の変化を算出する。
  4. データ鮮度と連続観測条件を検証する。
  5. 新しい eventId をイベントキューへ登録する。

Workflow C: 通常通知送信

  1. イベントに該当する購読者を抽出する。
  2. preparedSounds[nextStation] の有無を確認する。
  3. 最大100件単位でExpo Pushへ送る。
  4. Expo Push Ticketを保存する。
  5. 後続処理でPush Receiptを取得する。
  6. DeviceNotRegistered のExpo Tokenを無効化する。
  7. 429・5xxは指数バックオフ付きで再試行する。

Workflow D: Live Activity更新

  1. ActivityKit Push Tokenを持つ購読だけ抽出する。
  2. APNs JWTを生成または再利用する。
  3. liveactivity 用ヘッダーと content-state を送る。
  4. APNs応答を保存する。
  5. 無効・期限切れTokenを無効化する。
  6. 通常更新は優先度5、次駅変化等の主要イベントは必要に応じて10とする。

Workflow E: 購読終了・清掃

以下の条件で購読を終了する。

  • ユーザーが追従停止を操作
  • 別列車へ追従対象を変更
  • 列車が終着
  • expiresAt 超過
  • Push Tokenが無効
  • 数時間にわたり列車データを取得できない

9. API素案

POST /v1/tracking-subscriptions

追従開始。subscriptionId を指定した再送は冪等に処理する。

PATCH /v1/tracking-subscriptions/{subscriptionId}

ActivityKit Push Token、Expo Push Token、準備済み音声一覧などを更新する。

DELETE /v1/tracking-subscriptions/{subscriptionId}

追従停止。既存Live Activityをバックエンド側から終了させる必要がある場合は、削除前に event: end を送る。

POST /v1/tracking-subscriptions/{subscriptionId}/heartbeat

初期版では必須にしない。将来、端末状態や購読継続確認が必要になった場合だけ追加する。

10. アプリ側の変更計画

10.1 ActivityKit対応

  • Activity.request(..., pushType: nil)pushType: .token へ変更する。
  • activity.pushTokenUpdates を監視する。
  • Activity IDとPush TokenをJSへ通知するExpo Module APIを追加する。
  • Token変更、Activity終了、追従解除をバックエンドへ反映する。
  • Live Activityの ContentState とAPNs content-state の型を完全一致させる。

10.2 追従登録

  • 音声生成が完了した駅とファイル名を収集する。
  • Expo Push Token、ActivityKit Push Token、列車情報を購読APIへ登録する。
  • 登録中、登録済み、部分成功、失敗をUIで区別する。
  • 追従停止時は購読削除を送る。通信失敗時はローカルに停止要求を保持して再試行する。

10.3 通知処理

  • 通知タップ時に対象列車画面へ遷移する。
  • schemaVersiontype を検証し、未知のPayloadを安全に無視する。
  • フォアグラウンド受信時にも二重音声再生しない。
  • 端末内通知音が欠損している場合の挙動を実機確認する。

11. APNs認証情報と運用

APNs直接送信には最低限、以下が必要になる。

  • Apple DeveloperのAPNs Auth Key.p8
  • Key ID
  • Team ID
  • Bundle ID
  • development / production環境の識別

.p8 と署名用秘密情報はアプリ、Git、n8nワークフロー定義へ直接埋め込まない。n8n Credentialsまたは専用のSecrets管理へ保存する。

n8nからAPNs HTTP/2とJWT処理を安定して扱えない場合、APNs送信だけを小さなCloudflare Worker、Lambda、Cloud Run等へ分離し、n8nはその内部APIを呼ぶ。

12. セキュリティ・プライバシー

  • Expo Push Tokenを認証済みユーザーIDとして扱わない。
  • installationId と購読用の署名または短期トークンを導入する。
  • 登録APIをレート制限する。
  • 他人の subscriptionId を推測して更新・削除できないようにする。
  • Push Tokenをログ本文へそのまま出さず、必要なら末尾数文字のみ記録する。
  • 保存する情報は追従に必要な列車、運行日、通知Tokenに限定する。
  • 有効期限超過後は速やかに削除する。

13. 障害時の挙動

障害 推奨挙動
列車位置API停止 古い位置から通知せず、Live Activityをstale表示にする
Expo Push一時障害 指数バックオフで再試行。ただし次駅通知のTTL超過後は破棄
APNs一時障害 短時間再試行し、次の状態更新で上書き可能にする
音声未準備 通常音または音なしの表示通知へフォールバック
Activity Token無効 Dynamic Island更新のみ停止し、通常通知は継続
Expo Token無効 通常通知を停止し、購読を無効化または端末再登録待ちにする
重複イベント eventId と購読単位の送信履歴で抑止
位置情報が逆行・飛躍 連続観測と路線順序検証で確定を保留

14. 段階的な実装計画

Phase 0: 技術PoC

  • 実機で端末の Library/Sounds に動的WAVを保存する。
  • n8nからExpo Pushの sound にそのファイル名を指定する。
  • アプリが前面、背面、終了状態の3条件で音声再生を確認する。
  • マナーモード、集中モード、Bluetooth、他アプリ音声再生中も確認する。
  • Expo Push TicketとReceiptを保存・確認する。
  • ActivityKit Push Tokenを取得する最小実装を作る。
  • APNsから1回だけLive Activity更新を送る。

判定ゲート:

  • 動的WAVがExpo経由で安定再生する → 通常通知はExpoを継続。
  • 動的WAVが無音、デフォルト音、環境依存になる → 通常通知もAPNs直送へ変更。

Phase 1: 列車追従MVP

  • 列車追従購読APIを作成する。
  • 列車単位の共有ポーリングと次駅変化判定を作る。
  • eventId による重複防止を実装する。
  • 「次は、○○です。」通常Push通知を実装する。
  • 終着・停止・期限切れ処理を実装する。
  • n8n上で送信状況を確認できるログを用意する。

Phase 2: Dynamic Island連携

  • Live Activityを pushType: .token で開始する。
  • Token更新を購読APIへ同期する。
  • APNs Live Activity更新処理を実装する。
  • 次駅、現在位置、遅延、到着予定を同期する。
  • stale、終了、Token無効時の処理を実装する。

Phase 3: 駅固定モードと運用品質

  • 駅固定購読を同じデータモデルへ追加する。
  • 遅延・接近・発車イベントを追加する。
  • 監視、メトリクス、失敗通知を整備する。
  • 負荷試験とAPI障害試験を実施する。
  • 不要Tokenと期限切れ購読の自動清掃を実装する。

15. テスト項目

15.1 通知音

  • 対象WAVあり / なし
  • アプリ前面 / 背面 / 終了
  • 通常モード / マナーモード / 集中モード
  • 端末スピーカー / Bluetooth / CarPlay相当環境
  • 音楽や動画再生中
  • 通知許可あり / サウンドのみ拒否 / 通知拒否

iOSのユーザー設定や集中モードをアプリ側から回避する設計にはしない。

15.2 列車位置判定

  • 通常走行
  • 長時間停車
  • 遅延
  • 位置情報欠落
  • 位置の逆行または瞬間的な誤値
  • 途中駅通過
  • 列車番号重複と日付跨ぎ
  • 併結、分割、列車番号変更がデータ上存在する場合

15.3 PushとLive Activity

  • Token更新
  • 無効Token
  • 二重送信
  • 順不同到着
  • 古いPayload到着
  • APNs 410等のエラー
  • Activityがユーザーによって終了された場合
  • 追従停止直後に送信イベントが競合した場合

16. App Review上の説明方針

本機能はバックグラウンドで継続的にオーディオ再生するものではない。列車位置判定はサーバー側で行い、利用者が明示的に登録した追従条件に対して、通常のユーザー通知として短い音声を再生する。

そのため、初期方針では UIBackgroundModes = audio を要求しない。Live ActivityはActivityKitとAPNsの正規手段で更新する。

Review Notesでは以下を簡潔に説明する。

  • ユーザーが列車追従を明示的に開始・停止できること
  • 通知と音声を設定画面で無効化できること
  • 音声は通知到着時だけ短時間再生されること
  • バックグラウンドで常時音声処理を行わないこと
  • Dynamic IslandはActivityKit Pushで更新すること

17. 未決事項

別の設計レビューでは、特に以下を確認したい。

  1. Expo Push経由で Library/Sounds の動的WAV指定が実運用上保証できるか。
  2. n8nからAPNs HTTP/2へ直接送るか、署名・再試行を担当する小規模Push APIを分離するか。
  3. 列車位置変化を「1回で確定」するか「2回連続観測で確定」するか。
  4. 次駅アナウンスの発火点を、区間進入時、前駅発車時、次駅接近時のどこに置くか。
  5. 音声準備に一部失敗した状態で追従開始を許可するか。
  6. 駅固定モードをMVPへ含めるか、列車追従の安定後に追加するか。
  7. APNs認証情報をn8nで保持するか、専用Push APIでのみ保持するか。
  8. Live Activityの開始を常にアプリ操作必須とするか、将来Push-to-startを検討するか。
  9. Voicepeak設定変更後、同じファイル名の音声をいつ再生成するか。
  10. 列車走行位置情報APIの利用条件、更新間隔、障害時保証をどこまで前提にできるか。

18. 推奨する初期判断

  • MVPは列車追従モードだけに絞る。
  • n8nの既存購読リストとExpo一斉送信を維持する。
  • Phase 0の実機PoCを最優先し、動的音声の配信経路を先に確定する。
  • Dynamic IslandはExpo経由を試さず、最初からAPNs直接送信とする。
  • APNs送信処理は将来の再利用性と秘密鍵管理を考え、可能ならn8n外の小規模APIへ分離する。
  • アプリが閉じていても通知は成立させるが、初期版では追従開始そのものはアプリ操作を必須とする。
  • UIBackgroundModes = audio は復活させない。

19. 参考資料