通知設計 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
- 二層構造:Push を送る前に必ず
user_notificationsに INSERT。Push はあくまで配信手段 - Two-layer architecture: always INSERT into
user_notificationsbefore sending a push. Push is merely the delivery channel. - ユーザーコントロール:種別ごとに ON/OFF 切替可能(
app_users.notification_prefsJSONB) - User control: on/off can be toggled per type (
app_users.notification_prefsJSONB). - 静音時間:ユーザー設定の静音時間帯(例:22:00〜7:00)は非緊急通知を抑制
- Quiet hours: during the user's configured quiet hours (e.g. 22:00–07:00), non-urgent notifications are suppressed.
- マルチデバイス:1ユーザーが複数端末を持つ場合、全トークンに送信
- Multi-device: when a single user has multiple devices, send to all tokens.
- 既読同期:Realtime で
read_atを全端末同期 - Read-state sync:
read_atis synchronized across all devices via Realtime. - プライバシー:通知本文に個人情報や決済金額の詳細は含めない
- Privacy: notification bodies do not include personal information or payment amount details.
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
- 何らかのトリガで
user_notificationsに INSERT - Some trigger INSERTs a row into
user_notifications. after_insertトリガが Workers queueparky-fcm-dispatchをキュー実行- The
after_inserttrigger queues the Workers queueparky-fcm-dispatch. - Cloudflare Workers が
user_push_tokensを参照してアクティブなトークン一覧を取得 - The Cloudflare Workers reads
user_push_tokensto get the list of active tokens. - FCM v1 API で
message.tokenを指定して1端末ずつ送信 - It sends to each device individually via FCM v1 API with
message.token. - 結果を
user_notifications.status/delivered_atに反映 - 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
- フォアグラウンド:OS通知を抑制し、アプリ内バナー+振動で表示
- Foreground: suppress the OS notification and show an in-app banner with haptic feedback.
- バックグラウンド:OS通知表示。タップ時に
deep_linkで該当画面へ - Background: show the OS notification. On tap, navigate to the target screen via
deep_link. - 終了状態:タップ時に
initialNotificationからdeep_linkを取得して起動遷移 - Terminated state: on tap, read
deep_linkfrominitialNotificationand navigate after launch. - 受信時に
delivered_atを更新(ローカル即時 + サーバー PATCH) - On receipt, update
delivered_at(immediately locally plus a server PATCH).
8.3 アプリ内通知 8.3 In-app notifications
受信箱(画面 #17) Inbox (screen #17)
- タブ構成:全て / 未読 / システム / プロモーション
- Tabs: All / Unread / System / Promotions.
- 各行:アイコン、タイトル、本文、時刻、既読/未読バッジ
- Each row: icon, title, body, timestamp, read/unread badge.
- スワイプ操作:既読化 / 削除
- Swipe actions: mark as read / delete.
- 一括既読ボタン:RPC
mark_notifications_read - Mark-all-as-read button: RPC
mark_notifications_read.
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
- アプリアイコンバッジ:未読件数(iOS/Android対応)
- App icon badge: unread count (supported on both iOS and Android).
- タブバー通知タブ:未読件数の赤丸
- Tab bar notification tab: red dot showing the unread count.
- 購読レスポンスでローカルカウントを即更新
- Update the local count immediately from the subscription response.
8.4 通知トリガー一覧 8.4 Notification trigger list
| # | 通知種別Type | notif_type |
トリガTrigger | 緊急度Priority | Push | デフォルトON/OFFDefault on/off |
|---|---|---|---|---|---|---|
| 1 | 料金閾値アラートFee threshold alert | fee_alert | 現在料金がユーザー設定閾値を超過Current fee exceeds the user-configured threshold | 高High | ○ | ON |
| 2 | 最大料金到達Max fee reached | fee_cap_reached | 最大料金に到達Max fee has been reached | 高High | ○ | ON |
| 3 | 予定時刻接近Exit time approaching | exit_reminder | 設定予定出庫時刻の10分前10 minutes before the planned exit time | 中Medium | ○ | ON |
| 4 | 長時間駐車Long parking | long_parking | 24時間経過(1日1回)24 hours elapsed (once per day) | 中Medium | ○ | ON |
| 5 | 自動終了予告Auto-end warning | auto_end_soon | 70時間経過(72h自動終了の2時間前)70 hours elapsed (2 hours before the 72h auto-end) | 高High | ○ | ON |
| 6 | セッション自動終了Session auto-ended | session_auto_ended | 72時間経過で自動終了Auto-ended after 72 hours | 高High | ○ | ON |
| 7 | レビュー承認Review approved | review_approved | モデレーション承認Moderation approved | 低Low | ○ | ON |
| 8 | レビュー却下Review rejected | review_rejected | モデレーション却下Moderation rejected | 低Low | ○ | ON |
| 9 | バッジ獲得Badge earned | badge_earned | バッジ条件達成Badge criteria met | 低Low | ○ | ON |
| 10 | レベルアップLevel up | level_up | レベル閾値超過Level threshold crossed | 低Low | ○ | ON |
| 11 | 誤情報報告対応Error report resolved | error_report_resolved | 管理者が resolved に更新Admin updated the report to resolved | 低Low | ○ | ON |
| 12 | サポート返信Support reply | support_reply | 管理者が返信Admin replied | 中Medium | ○ | ON |
| 13 | プラン更新Plan renewed | subscription_renewed | IAP更新成功IAP renewal succeeded | 低Low | ○ | ON |
| 14 | プラン失効Plan expired | subscription_expired | グレース期間超過Grace period exceeded | 中Medium | ○ | ON |
| 15 | 新着記事New article | news | 新着記事公開A new article is published | 低Low | ○ | OFF |
| 16 | プロモーションPromotion | promo | キャンペーン配信Campaign delivery | 低Low | ○ | OFF |
| 17 | 管理者からのお知らせAdmin announcement | system_announcement | 管理者ポータルから送信Sent from the admin portal | 中Medium | ○ | ON |
| 18 | 保存駐車場の変更Saved lot updated | saved_lot_updated | お気に入り駐車場の料金/営業時間変更A favorite lot changed its fees or hours | 低Low | ○ | OFF |
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
exit_reminder:ユーザーが設定した予定出庫時刻から逆算して10分前に発火。DB 側はpg_cronで毎分チェックexit_reminder: fires 10 minutes before the user's planned exit time (reverse-calculated). On the DB side,pg_cronchecks every minute.auto_end_soon:駐車開始から70時間後に1回限定発火auto_end_soon: fires once only, 70 hours after parking start.long_parking:24時間×n 経過時に1日1回発火long_parking: fires once per day each time 24h × n has elapsed.
静音時間ハンドリング Quiet hours handling
- ユーザー設定(例:22:00〜7:00)の間は、緊急度「低」「中」はキューに積み静音解除後に配信
- During the user's configured window (e.g. 22:00–07:00), Low and Medium priority notifications are queued up and delivered after quiet hours end.
- 緊急度「高」(料金アラート等、駐車行動に直結するもの)は静音時間でも送信
- High priority notifications (fee alerts and anything directly tied to parking action) are sent even during quiet hours.
- キューは
user_notifications.status = 'queued'で表現、Cron が定期消化 - The queue is represented by
user_notifications.status = 'queued', drained periodically by a cron job.
重複抑制 Deduplication
- 同一種別・同一セッションに対する
fee_alertは60分間再送しない - Do not resend
fee_alertfor the same type and same session for 60 minutes. long_parkingは直近24時間内に送信済みなら再送しない- Do not resend
long_parkingif one has already been sent within the last 24 hours. - トリガ関数内で過去ログを参照してチェック
- The trigger function checks against past logs.
配信失敗時の処理 Failure handling
- FCM エラー
NOT_REGISTERED→ トークンをuser_push_tokensから削除 - FCM error
NOT_REGISTERED→ remove the token fromuser_push_tokens. - 一時的エラー → 指数バックオフで3回リトライ(30s / 2min / 10min)
- Transient errors → retry three times with exponential backoff (30s / 2min / 10min).
- 最終失敗は
user_notifications.status = 'failed'に更新、管理者ポータルで監視 - Final failure updates
user_notifications.status = 'failed'and is monitored from the admin portal.