通知設計 Notification design

Parky モバイルアプリの通知は、Push 通知(FCM)アプリ内通知(user_notifications テーブル)の 二層構造で設計されます。全ての通知は必ず user_notifications に記録されるため、ユーザーはアプリ内で再閲覧できます。

Notifications in the Parky mobile app are built as a two-layer architecture: push notifications (FCM) and in-app notifications (the user_notifications table). Every notification is always recorded in user_notifications, so users can revisit them inside the app.

8.1 通知方針 8.1 Notification principles

flowchart LR
  Src["通知トリガ
(DB/Cron/管理者)"] --> Ins[user_notifications INSERT] Ins --> Send["POST /v1/admin/user-notifications/{id}/send"] Send --> Pref{ユーザー
Prefs許可?} Pref -- NO --> End[終了] Pref -- YES --> Quiet{静音時間?} Quiet -- YES --> Queue[Cron遅延送信] Quiet -- NO --> Q["parky-fcm-dispatch
Queue (500/batch)"] Queue --> Q Q --> Cons["queue/fcm-dispatch.ts
consumer"] Cons --> Tok[user_push_tokens 取得] Tok --> FCM[FCM v1 送信] FCM --> iOS[iOS端末] FCM --> And[Android端末] Cons --> Stat[user_notifications
success_count 加算]

8.2 Push通知 8.2 Push notifications

配信経路 Delivery pipeline

  1. 何らかのトリガで user_notifications に INSERT
  2. Some trigger INSERTs a row into user_notifications.
  3. after_insert トリガが Workers queue parky-fcm-dispatch をキュー実行
  4. The after_insert trigger queues the Workers queue parky-fcm-dispatch.
  5. Cloudflare Workers が user_push_tokens を参照してアクティブなトークン一覧を取得
  6. The Cloudflare Workers reads user_push_tokens to get the list of active tokens.
  7. FCM v1 API で message.token を指定して1端末ずつ送信
  8. It sends to each device individually via FCM v1 API with message.token.
  9. 結果を user_notifications.status / delivered_at に反映
  10. Results are written back to user_notifications.status / delivered_at.

FCM ペイロード(iOS / Android 共通) FCM payload (shared iOS / Android)

{
  "message": {
    "token": "<fcm_token>",
    "notification": {
      "title": "料金が1,500円に到達しました",
      "body":  "現在駐車中の料金が閾値を超えました"
    },
    "data": {
      "notif_id": "uuid",
      "notif_type": "fee_alert",
      "deep_link": "parky://session/abcd-1234"
    },
    "android": {
      "priority": "high",
      "notification": { "channel_id": "fee_alerts" }
    },
    "apns": {
      "headers": { "apns-priority": "10" },
      "payload": { "aps": { "sound": "default", "interruption-level": "time-sensitive" } }
    }
  }
}

クライアント側受信処理 Client-side receive handling

8.3 アプリ内通知 8.3 In-app notifications

受信箱(画面 #17) Inbox (screen #17)

Realtime 同期 Realtime sync

複数端末間で未読バッジ・既読状態を即時同期するため、user_notifications を Realtime で購読します。

To keep unread badges and read state in sync across multiple devices in real time, subscribe to user_notifications via Realtime.

supabase
  .channel('notif:' + userId)
  .on('postgres_changes', {
    event: '*', schema: 'public',
    table: 'user_notifications',
    filter: `user_id=eq.${userId}`
  }, handler)
  .subscribe();

バッジカウント Badge count

8.4 通知トリガー一覧 8.4 Notification trigger list

# 通知種別Type notif_type トリガTrigger 緊急度Priority Push デフォルトON/OFFDefault on/off
1料金閾値アラートFee threshold alertfee_alert現在料金がユーザー設定閾値を超過Current fee exceeds the user-configured thresholdHighON
2最大料金到達Max fee reachedfee_cap_reached最大料金に到達Max fee has been reachedHighON
3予定時刻接近Exit time approachingexit_reminder設定予定出庫時刻の10分前10 minutes before the planned exit timeMediumON
4長時間駐車Long parkinglong_parking24時間経過(1日1回)24 hours elapsed (once per day)MediumON
5自動終了予告Auto-end warningauto_end_soon70時間経過(72h自動終了の2時間前)70 hours elapsed (2 hours before the 72h auto-end)HighON
6セッション自動終了Session auto-endedsession_auto_ended72時間経過で自動終了Auto-ended after 72 hoursHighON
7レビュー承認Review approvedreview_approvedモデレーション承認Moderation approvedLowON
8レビュー却下Review rejectedreview_rejectedモデレーション却下Moderation rejectedLowON
9バッジ獲得Badge earnedbadge_earnedバッジ条件達成Badge criteria metLowON
10レベルアップLevel uplevel_upレベル閾値超過Level threshold crossedLowON
11誤情報報告対応Error report resolvederror_report_resolved管理者が resolved に更新Admin updated the report to resolvedLowON
12サポート返信Support replysupport_reply管理者が返信Admin repliedMediumON
13プラン更新Plan renewedsubscription_renewedIAP更新成功IAP renewal succeededLowON
14プラン失効Plan expiredsubscription_expiredグレース期間超過Grace period exceededMediumON
15新着記事New articlenews新着記事公開A new article is publishedLowOFF
16プロモーションPromotionpromoキャンペーン配信Campaign deliveryLowOFF
17管理者からのお知らせAdmin announcementsystem_announcement管理者ポータルから送信Sent from the admin portalMediumON
18保存駐車場の変更Saved lot updatedsaved_lot_updatedお気に入り駐車場の料金/営業時間変更A favorite lot changed its fees or hoursLowOFF

8.5 通知スケジューリング 8.5 Notification scheduling

即時送信 Immediate send

fee_alert, fee_cap_reached, session_auto_ended 等の駐車中イベントは、発生即時にトリガされ Cloudflare Workers で送信されます。

In-parking events such as fee_alert, fee_cap_reached, and session_auto_ended are triggered immediately on occurrence and sent via the Cloudflare Workers.

遅延送信 Delayed send

静音時間ハンドリング Quiet hours handling

重複抑制 Deduplication

配信失敗時の処理 Failure handling

注意:Caveat: 通知のペイロードに金額や住所等の個人情報を含めず、通知本文は「料金が閾値に達しました」のような抽象的な文言にします。詳細は deep_link 先の画面で表示します(ロック画面プライバシー保護)。 Do not include monetary amounts, addresses, or other personal information in the notification payload. Notification bodies should use abstract wording such as "your fee reached the threshold." Details are shown on the destination screen of the deep_link (lock-screen privacy protection).