モバイルで特別扱いが必要な 4 つの連携を、シーケンス図つきで。 The four integrations that need special handling on mobile — with sequence diagrams.
通知・ファイルアップロード・地図・位置情報の 4 連携は、BFF の通常エンドポイントと比べて 「クライアント側の手続きが増える」という特徴があります。ここでは各連携の端から端までのフローを整理します。
These four integrations — push, uploads, maps, and location — require more client-side choreography than ordinary BFF calls. This page lays out the end-to-end flow for each.
1. FCM(プッシュ通知) 1. FCM (push notifications)
1-1. トークン登録フロー 1-1. Token registration flow
sequenceDiagram participant App as Flutter App participant OS as iOS/Android participant FCM as FCM participant BFF as Parky BFF App->>OS: request notification permission OS-->>App: granted / denied App->>FCM: getToken() FCM-->>App: fcm_token App->>BFF: PUT /v1/me/push-tokens
{ token, device_type, app_version } BFF-->>App: 200 OK Note over App,FCM: Token can rotate — listen to onTokenRefresh FCM-->>App: onTokenRefresh(new_token) App->>BFF: PUT /v1/me/push-tokens (upsert)
- 起動時:権限を持っていれば
getToken()→PUT /v1/me/push-tokensで upsert - On launch: if permission is granted,
getToken()→PUT /v1/me/push-tokensupsert - ログアウト時:
DELETE /v1/me/push-tokens/{token}で失効させる - On logout: invalidate via
DELETE /v1/me/push-tokens/{token} - トークン rotation:
onTokenRefreshで受けたら再 upsert - Token rotation: on
onTokenRefresh, upsert again
1-2. 受信と画面遷移 1-2. Receive and route
sequenceDiagram
participant BFF as Parky BFF
participant Queue as parky-fcm-dispatch
participant FCM as FCM v1
participant OS as iOS/Android
participant App as Flutter App
BFF->>Queue: enqueue (user_ids, notif_id)
Queue->>FCM: send batch
FCM->>OS: push { notif_type, notif_id, deep_link? }
OS->>App: onMessage / onBackgroundMessage
App->>BFF: GET /v1/me/notifications/{id}
BFF-->>App: 200 OK { notification }
App->>App: navigate by deep_link
- ペイロード:
notif_type(コード値)、notif_id、deep_link(任意)、title、body - Payload:
notif_type(code),notif_id, optionaldeep_link,title,body - Notification Channel(Android):
parky.critical、parky.general、parky.marketingの 3 つ。ユーザーが OS 設定でカテゴリ単位にオン/オフ可能 - Notification Channels (Android):
parky.critical,parky.general,parky.marketing. User-controllable per channel in OS settings - 通知種別の詳細:18 種類(詳細は mobile-app/notifications.html)
- Notification types: 18 kinds (see mobile-app/notifications.html)
- 静音時間:ユーザー設定(
quiet_hours)をサーバー側で尊重。モバイル側での時間判定は不要 - Quiet hours: Server-side respects user settings (
quiet_hours). Mobile does not need to gate delivery by time
2. R2(ファイルアップロード) 2. R2 (file upload)
駐車メモ写真とレビュー写真は Cloudflare R2 に保存します。
モバイルは BFF から presigned PUT URL を取得 → R2 に直接 PUT
→ 返却された asset_id を関連レコードに紐付けという 3 ステップで実行します。
Parking-memo photos and review photos go to Cloudflare R2. The mobile app
gets a presigned PUT URL from the BFF, PUTs the bytes directly to R2,
and then links the returned asset_id to a parent record — three steps.
sequenceDiagram participant App as Flutter App participant BFF as Parky BFF participant R2 as Cloudflare R2 participant PG as PostgreSQL App->>BFF: POST /v1/storage/upload-url
{ file_name, mime_type, file_size, category } BFF->>PG: INSERT assets (pending) PG-->>BFF: asset_id BFF-->>App: 200 OK
{ asset_id, upload_url, s3_key, public_url, expires_in } App->>R2: PUT upload_url
Content-Type: <exact MIME>
Body: <bytes> R2-->>App: 200 OK App->>BFF: POST /v1/parking-sessions/{id}/memo
{ memo_photo_asset_id: asset_id, ... } BFF->>PG: UPDATE parking_sessions SET memo_photo_asset_id = ... BFF->>PG: UPDATE assets SET status = 'active' BFF-->>App: 200 OK
注意点 Key points
- Content-Type 完全一致:PUT 時のヘッダは
upload-url発行時に指定したmime_typeと 完全一致させる必要があります(不一致だと 403) - Exact Content-Type match: The PUT request's header must exactly match the
mime_typeused when requesting the URL (otherwise 403) - ファイルサイズ上限:カテゴリごとに BFF が制限(写真
review_photoは 10MB、memo_photoは 5MB) - Max file size: Per-category limit enforced by BFF (
review_photo10MB,memo_photo5MB) - URL 有効期限:通常 5 分(
expires_inで返却)。これ以内に PUT を完了すること - URL expiry: Usually 5 minutes (returned as
expires_in). Complete the PUT within the window - HEIC / HEIF:iOS ユーザーの写真ライブラリから直接読む場合は、PUT 前に JPEG / PNG に変換すること(
image_pickerのimageQuality設定) - HEIC / HEIF: Convert to JPEG/PNG before PUT when reading from an iOS photo library (use
image_pickerwithimageQuality) - PUT の認証不要:presigned URL 自体に署名が載っているので
Authorizationヘッダは不要(むしろ付けると 403) - No auth on PUT: The URL is signed; do not add an
Authorizationheader (it will 403)
3. Mapbox(地図表示) 3. Mapbox (maps)
Parky は地図に Mapbox を採用しています。Flutter では
mapbox_maps_flutter パッケージを使います(Mapbox 公式)。
アクセストークンは Parky 運営から別途提供します(Android / iOS 両方)。
Parky uses Mapbox for maps. On Flutter we use mapbox_maps_flutter
(the official package). Access tokens (for Android and iOS) are provided separately by Parky.
- スタイル:カスタムスタイル URL を Parky が用意(ブランドカラーに合わせた配色)
- Style: Custom style URL provided by Parky (brand-aligned colors)
- 駐車場マーカー:PostGIS の
nearbyRPC 結果をそのままマーカー化。12 件超える場合はクラスタリング(アイコン付きクラスタ) - Lot markers: Results from the PostGIS
nearbyRPC become markers. Over 12 results → clustering (with icon) - 料金ラベル:ズームレベル 15 以上でマーカーの横に料金バッジを表示
- Fee labels: At zoom ≥ 15, show a fee badge next to each marker
- 駐車中マーカー:自分の駐車セッションは特別アイコンで強調表示(他のマーカーより上のレイヤ)
- Active session marker: The user's own active session uses a special icon (above other markers)
- オフライン:地図タイルのオフラインキャッシュは v1 ではスコープ外(ネット接続前提)
- Offline: Offline tile caching is out of scope for v1 (network required)
4. ジオフェンス(位置情報) 4. Geofence (location)
位置情報は以下の 3 用途で使います。プラットフォームネイティブのジオフェンス
(iOS CLLocationManager.startMonitoring / Android GeofencingClient)を利用し、
常時ポーリングは行わないこと(バッテリー消費と審査リスクのため)。
Location is used for three cases. Use the platform-native geofencing APIs
(iOS CLLocationManager.startMonitoring / Android GeofencingClient) —
do not continuously poll (battery drain and store-review risk).
| 用途Use case | 必要な権限Required permission | 実装Implementation | |||
|---|---|---|---|---|---|
| ホーム画面の現在地センター、周辺検索 | Center map on current location, nearby search | When in use(使用中のみ) | When in use only | geolocator.getCurrentPosition()、取得したら即座にトラッキング停止 | geolocator.getCurrentPosition(), stop tracking immediately after |
| 駐車場到着時の「駐車開始」促進通知(任意機能) | Prompt to start a session when the user arrives (optional feature) | Always(バックグラウンド)— 任意許可、拒否時は機能オフ | Always (background) — opt-in; feature disabled if denied | ネイティブジオフェンス、半径 100m、駐車場座標を登録 | Native geofence at 100m radius around the parking lot |
| スポンサー近接検知(v1 オプション) | Sponsor proximity (optional in v1) | Always(上記と同じセット) | Always (same permission as above) | ネイティブジオフェンス、area_sponsors.radius_m を使用 | Native geofence using area_sponsors.radius_m |
権限要求フロー Permission request flow
- When in use → Always のアップグレードは OS が推奨するタイミング(駐車セッションを 1 回以上成立させた後など)で行う。いきなり Always を求めない
- Upgrade When-in-use → Always only after an OS-recommended trigger (e.g. after the user's first completed session). Do not ask for Always up front
- 拒否された場合は機能側を grace-degrade する(プッシュ通知ベースで代替など)
- If denied, gracefully degrade the feature (fall back to push-based prompts)
- iOS は
NSLocationAlwaysAndWhenInUseUsageDescriptionにユーザーが理解できる説明を記載(App Store 審査の必須項目) - On iOS, set
NSLocationAlwaysAndWhenInUseUsageDescriptionto a user-understandable explanation (required by App Store review) - Android 10+ は
ACCESS_BACKGROUND_LOCATIONを段階的に求める(まず ACCESS_FINE → 時間を置いて ACCESS_BACKGROUND) - Android 10+: request
ACCESS_BACKGROUND_LOCATIONin stages (ACCESS_FINE first, then ACCESS_BACKGROUND after a usage gap)
Realtime(Supabase Realtime) Realtime (Supabase Realtime)
user_notifications 行フィルタ購読を追加する方針です。
Realtime is not used in v1 — all in-app updates use pull-based fetching.
When we add "live notification updates" or "broadcast messages" later, we'll subscribe to
user_notifications row-filtered streams via Supabase Realtime.