API レスポンス構造 API response structure

全クライアント(モバイル / Web / 管理者・オーナー・マーケティングの各ポータル)は Cloudflare Workers 上の BFF (/v1/*) だけを叩く。 モバイルの /v1/mobile/* は画面単位のレスポンスエンベロープを返し、 OpenAPI 契約(OAS)はチャネル別に分離されている。

Every client (mobile / web / admin / owner / marketing portals) talks only to the Cloudflare Workers BFF (/v1/*). Mobile's /v1/mobile/* endpoints return per-screen response envelopes, and the OpenAPI contracts (OAS) are split per channel.

3 行で言うと TL;DR
  • 全クライアントは Cloudflare Workers BFF (/v1/*) のみ。Supabase DB / Edge Functions の直叩きは禁止
  • Every client hits only the Cloudflare Workers BFF (/v1/*). Direct access to Supabase DB / Edge Functions is forbidden.
  • モバイルの views 系 (例: /v1/mobile/views/home-feed) は ViewEnvelope、actions 系 (例: /v1/mobile/actions/sessions/start) は ActionEnvelope/v1/mobile/telemetry/eventsTelemetryAck を返す。UI コンポーネント構造は返さない(描画はネイティブ実装)。
  • Mobile: views (e.g. /v1/mobile/views/home-feed) returns ViewEnvelope, actions (e.g. /v1/mobile/actions/sessions/start) returns ActionEnvelope, /v1/mobile/telemetry/events returns TelemetryAck. No UI component trees (widgets stay native).
  • OAS はチャネル別に 5 ファイル(管理者 / オーナー / マーケティング / モバイル / Web アプリ)。
  • OAS is split into 5 per-channel files (admin / owner / marketing / mobile / web app).

1. システム全景 (flow)1. System flow

flowchart LR
  subgraph Clients["クライアント / Clients"]
    MOB[Mobile
Flutter] WEB[Web App
parky.co.jp] ADM[Admin Portal] OWN[Owner Portal] MKT[Marketing Portal] end subgraph Edge["Cloudflare Workers BFF (/v1/*)"] MOBSUR[/v1/mobile/views/home-feed
/v1/mobile/actions/sessions/start
/v1/mobile/telemetry/events] WEBSUR[/v1/parking-lots etc.
public resource API] ADMSUR[/v1/admin/*] OWNSUR[/v1/owner/*
/v1/owner-public/*] MKTSUR[/v1/marketing/*] HOOK[/v1/webhooks/*
stripe / apple-iap / google-play] end subgraph Backend["Backend"] SB[(Supabase DB
5 schema + RLS)] BFF_RPC[(bff_only schema RPC)] FCM[FCM / Resend / X API / Stripe] end MOB -- ViewEnvelope/Action/Telemetry --> MOBSUR WEB --> WEBSUR ADM --> ADMSUR OWN --> OWNSUR MKT --> MKTSUR Stripe[(Stripe / Apple / Google)] --> HOOK MOBSUR --> SB MOBSUR --> BFF_RPC WEBSUR --> SB ADMSUR --> SB OWNSUR --> SB MKTSUR --> SB HOOK --> SB MOBSUR --> FCM ADMSUR --> FCM MKTSUR --> FCM MOB -. Realtime (Supabase client) .-> SB

BFF は 5 channel(admin / owner / marketing / mobile / public)を route manifest で 宣言している。owner-public(パスワード設定リンク等の認証前 owner 接面)と webhooks (外部からの署名付 callback)は public Worker にマウントされる別 BFF 実装ディレクトリ。 BFF-only by default: RPC は原則 bff_only schema 配置で、client から supabase.rpc() 直叩きは ESLint + CI grep で block(ADR 0012)。

The BFF declares 5 channels (admin / owner / marketing / mobile / public) in the route manifest. owner-public (pre-auth owner surfaces such as password-setup links) and webhooks (signed external callbacks) are separate BFF implementation directories that are mounted on the public Worker. BFF-only by default: RPCs live in the bff_only schema; direct supabase.rpc() from clients is blocked by ESLint + CI grep gates (ADR 0012).

1.1 BFF channel × ディレクトリ対応1.1 Channel ↔ implementation directory map

flowchart LR
  subgraph Routes["api/src/routes/*-routes.ts (manifest)"]
    R1[admin-routes.ts]
    R2[marketing-routes.ts]
    R3[mobile-routes.ts]
    R4[owner-routes.ts]
    R5[public-routes.ts]
  end
  subgraph BFF["api/src/bff/* (Layer-First)"]
    B1[bff/admin/]
    B2[bff/marketing/]
    B3[bff/mobile/
views / actions / telemetry] B4[bff/owner/] B5[bff/owner-public/
password-setup] B6[bff/web/
parking-lots / articles / me / ...] B7[bff/webhooks/
stripe / apple-iap / google-play] end subgraph Core["api/src/core/* (capability)"] C[orchestration / domain rules] end subgraph Data["api/src/data/*.data.ts (1 file = 1 table cluster)"] D[postgres.js + Row→domain 変換] end R1 --> B1 R2 --> B2 R3 --> B3 R4 --> B4 R5 --> B5 R5 --> B6 R5 --> B7 B1 & B2 & B3 & B4 & B5 & B6 & B7 --> C C --> D D --> SBDB[(Supabase Postgres)]

route manifest 上の category は 5 値(public / admin / marketing / owner / mobile)。bff/web/ / bff/owner-public/ / bff/webhooks/ はいずれも category: 'public' で manifest 登録される。

Route manifest category has 5 values (public / admin / marketing / owner / mobile). bff/web/, bff/owner-public/, and bff/webhooks/ are all registered as category: 'public' in the manifest.

Realtime 購読のみ、ROI の都合でモバイル Flutter から Supabase client 直接接続を許容する (BFF 経由 WebSocket 中継はコストに見合わない)。それ以外の読み書きはすべて BFF を経由する。

Realtime subscriptions are the single exception: mobile Flutter may open them directly via the Supabase client (proxying WebSockets through the BFF is not ROI-positive). Every other read/write goes through the BFF.

2. BFF の役割2. What the BFF does

3. モバイル 3 surface の役割3. Mobile's three surfaces

Surface 用途Purpose レスポンスResponse
views (例: /v1/mobile/views/home-feed) 画面を開いたときの初期データ取得 Initial payload when a screen opens ViewEnvelope ViewEnvelope
actions (例: /v1/mobile/actions/sessions/start) ボタン押下等のミューテーション Mutations triggered by user actions ActionEnvelope ActionEnvelope
/v1/mobile/telemetry/events fire-and-forget のイベント記録(geofence・位置・push 受信等) Fire-and-forget event logging (geofence / location / push receipts) TelemetryAck TelemetryAck

UI コンポーネント構造はサーバーから返さない。アニメーション・ジェスチャー・Mapbox 等のネイティブ描画を活かすため、 サーバーは振る舞い(ルール・文言・遷移・フラグ)と画面単位 aggregate のデータだけを返す。

The server never returns UI component trees. To keep animations, gestures, and native widgets (Mapbox etc.) intact, the server returns only behaviour (rules, copy, navigation, flags) plus the per-screen aggregate data.

4. ViewEnvelope の構造4. ViewEnvelope structure

views 系(例: /v1/mobile/views/home-feed)はすべて以下のエンベロープを返す。data / meta / fallback_behavior は必須、 その他は画面の性質に応じて任意。

Every views endpoint (e.g. /v1/mobile/views/home-feed) returns this envelope. data, meta, and fallback_behavior are required; the rest are optional depending on the screen.

// 実装: api/src/lib/view/meta.ts (ViewEnvelope<D> / viewEnvelopeSchema / buildViewEnvelope)
// バックフェード: api/src/lib/view-envelope.ts は ./view から re-export するだけの薄いファサード
{
  "data":              { /* screen-specific primary payload */ },
  "ui_config":         { /* messages / feature_flags / experiment_id / theme_hint */ },
  "navigation":        { "target": "...", "params": {...}, "strategy": "push|replace|pop_to_root" },
  "validation":        [ { "field": "...", "rule": "...", "param": ..., "message_code": "..." } ],
  "states":            { "error": [...], "empty": {...}, "skeleton": {...} },
  "fallback_behavior": { "on_network_error": "...", "on_auth_error": "...", "on_version_mismatch": "...", "cache_ttl_seconds": 60 },
  "meta":              {
    "server_time":         "2026-05-03T10:00:00.000Z",
    "cache_key":           "parking_lot_detail:abc123",
    "min_app_version":     "1.0.0",
    "sunset_date":         null,
    "realtime":            { /* RealtimeHint: channel / event_types / rls_precondition / fallback_poll_seconds */ },
    "expected_ui_version": "ui_2026_04_26_1547",   // SDUI cache layering 鮮度トークン(/boot 突合)
    "view_spec_ref":       "parking_lot_detail_v1" // /boot の ui_layer.view_specs[id] への参照
  }
}

P4 envelope slim: buildViewEnvelope()clientAppVersionENVELOPE_SLIM_MIN_VERSION (現在 "1.0.0") 以上のとき、 validation / states を omit して payload を縮める(client は meta.view_spec_ref から /v1/mobile/views/bootui_layer.view_specs を引いて resolve)。 fallback_behavior は schema 上必須のため P6 までは残置(後方互換のための安全網)。

P4 envelope slim: buildViewEnvelope() omits validation / states when clientAppVersion >= ENVELOPE_SLIM_MIN_VERSION (currently "1.0.0"); the client resolves them from /v1/mobile/views/boot's ui_layer.view_specs[meta.view_spec_ref]. fallback_behavior stays in the schema until P6 as a backward-compat safety net.

4.0 ViewEnvelope リクエスト/レスポンス sequence4.0 ViewEnvelope request/response sequence

sequenceDiagram
  autonumber
  participant C as Client (Flutter)
  participant E as BFF endpoint
/v1/mobile/views/lot-detail/{id} participant L as buildViewEnvelope()
lib/view/meta.ts participant V as ViewSpec catalog
(/boot ui_layer) participant DB as Supabase Postgres
(public/admin/marketing/analytics) C->>E: GET /v1/mobile/views/lot-detail/{id}
X-App-Version: 1.4.0 E->>DB: core/* + data/*.data.ts
(SELECT / RPC bff_only.*) DB-->>E: domain rows E->>L: buildViewEnvelope({ data, fallback_behavior, ui_config?, ... }) L->>L: shouldSlimEnvelope(clientAppVersion)
checkMinAppVersion(...) L-->>E: ViewEnvelope<D> E-->>C: 200 OK + JSON envelope C->>C: meta.expected_ui_version vs cache alt drift C-->>+V: GET /v1/mobile/views/boot (background) V-->>-C: ui_layer (L0/L1) refresh end
フィールドField 責務Responsibility Example 必須?Required?
data data 画面に表示する primary payload(画面固有型) Primary payload for the screen (screen-specific type) 駐車場詳細 → ParkingLotDetail Parking lot detail → ParkingLotDetail ✅ 必須✅ Required
ui_config ui_config 文言(messages)/ feature flag / A/B 実験 ID / theme hint Messages / feature flags / A/B experiment id / theme hint boot で client flag 一括配信 Boot endpoint ships client flags once 画面次第Per screen
navigation navigation 次画面遷移のヒント(deeplink / modal / pop) Next-step hint (deeplink / modal / pop) 予約成立後 → push parking_detail After booking → push parking_detail action 系のみ必須Required on actions
validation validation 入力系画面の制約(required / min_length / regex / numeric_range) Input constraints (required / min_length / regex / numeric_range) ナンバープレート = 8 桁 Plate number = 8 chars 入力画面のみInput screens only
states states loading / empty / error 時のメッセージと fallback 指針(ERROR_CATALOG から自動生成) Loading / empty / error messages and fallback direction (auto-generated from ERROR_CATALOG) 404 → empty illustration 404 → empty illustration 任意Optional
fallback_behavior fallback_behavior ネット不通 / 認証エラー / バージョン不整合時の既定動作 Defaults for network / auth / version-mismatch failures on_version_mismatch: force_update on_version_mismatch: force_update ✅ 必須✅ Required
meta meta ETag / cache-control / request_id / server_time / min_app_version / realtime ヒント ETag / cache-control / request_id / server_time / min_app_version / realtime hint min_app_version: "1.4.0" min_app_version: "1.4.0" ✅ 必須✅ Required

実装は 2026-04-28 の分割で lib/view/ 以下に責務別に再配置済み: view/meta.ts (envelope 組立 + version check), view/states.ts (ErrorCatalog 統合), view/fallback.ts (FALLBACK_BOOT/FEED/DETAIL/PERSONAL), view/navigation.ts / view/ui-config.ts / view/validation.ts / view/action.tslib/view-envelope.ts は薄い再エクスポートファサード。states.errorERROR_CATALOG + ERROR_MESSAGE_KEYS から自動派生させること(手書き禁止)。

The implementation was split (2026-04-28) into lib/view/: view/meta.ts (envelope builder + version check), view/states.ts (ErrorCatalog), view/fallback.ts, view/navigation.ts, view/ui-config.ts, view/validation.ts, view/action.ts. lib/view-envelope.ts is a thin façade that re-exports from ./view. Never hand-write states.error; always derive it from ERROR_CATALOG + ERROR_MESSAGE_KEYS.

4.1 キャッシュ階層 L0 / L1 / L2 と 3 段防御4.1 Cache layers L0 / L1 / L2 and the 3-stage freshness contract

ViewEnvelope のフィールドはライフサイクルが大きく異なる。毎リクエストで全部往復させると 90% は無駄になる (validation / states / fallback_behavior は endpoint 仕様で不変のため)。Parky では L0 / L1 / L2 に階層分離し、 L0/L1 は /v1/mobile/views/boot に集約して永続キャッシュ、L2 のみ毎リクエストで返す(2026-04-26 確定)。

ViewEnvelope fields have wildly different lifecycles. Round-tripping all of them per request wastes ~90% of bytesvalidation / states / fallback_behavior are essentially endpoint-specification constants. Parky separates them into L0 / L1 / L2: L0 + L1 are aggregated into /v1/mobile/views/boot and persisted client-side, only L2 ships per request (decided 2026-04-26).

階層Layer 中身Contains 配信経路Delivery クライアント保持Client retention
L0 構造L0 structure navigation_shell / screen registry / route definitions navigation_shell / screen registry / route definitions /v1/mobile/views/boot/v1/mobile/views/boot 永続キャッシュ(バージョン mismatch まで保持) Persistent cache, kept until a version mismatch
L1 ロジックL1 logic validation rules / states (error/empty/skeleton) / fallback_behavior / view_specs / ui_config 静的部分 validation rules / states / fallback_behavior / view_specs / static ui_config /v1/mobile/views/boot/v1/mobile/views/boot 永続キャッシュ(バージョン mismatch まで保持) Persistent cache, kept until a version mismatch
L2 状態L2 state data / badge / A/B flag / feature_flags / premium status data / badge / A/B flag / feature_flags / premium status 毎リクエスト(例: /v1/mobile/views/home-feed Per request (e.g. /v1/mobile/views/home-feed) 短 TTL or キャッシュなし。L0/L1 に絶対紛れさせない Short TTL or none. Never bleeds into L0/L1

鮮度契約 = 3 段防御(時間 TTL は持たない)Freshness contract = 3-stage defense (no time-based TTL)

定期 polling や n 分 TTL は採用しない。Parky の deploy 頻度なら 99% が無駄打ちになる。代わりに 変化検知(mismatch)を主役に置き、フォアグラウンド復帰と push を保険・緊急に回す

We deliberately avoid periodic polling and N-minute TTLs — at Parky's deploy cadence, 99% of refreshes would be wasted. Instead, change detection (mismatch) is the main mechanism; foreground resume and push are the safety net and emergency lever.

  1. 主役 (mismatch 検知): 各 view レスポンスに meta.expected_ui_version を載せ、クライアントがローカルキャッシュの version と突き合わせる。ズレたら背景で /v1/mobile/views/boot を再取得して L0/L1 を更新。平常時はゼロ往復。
  2. Primary (mismatch detection): every view response carries meta.expected_ui_version; the client compares it to its cached version and, on drift, refreshes /v1/mobile/views/boot in the background. Steady state = zero extra round-trips.
  3. 保険 (24h ETag probe): フォアグラウンド復帰 + 前回 /v1/mobile/views/boot から 24h+ 経過していたら、If-None-Match で 304 probe。差分があれば本体取得。アプリが長期スリープしていても確実に整合。
  4. Safety net (24h ETag probe): on foreground resume, if the last /v1/mobile/views/boot hit is 24h+ old, do an If-None-Match 304 probe. Pull the body only on a real diff. Keeps long-suspended apps consistent.
  5. 緊急 (push-bust): silent FCM/APNs で {"type":"ui_bust"} を配信して即時に boot 再取得をトリガ。MVP ではスキップ可(mismatch だけで実用十分)。
  6. Emergency (push-bust): a silent FCM/APNs payload {"type":"ui_bust"} triggers an immediate boot refresh. Optional in MVP — mismatch alone covers most cases.

rule type と rule instance の境界rule type vs rule instance

Splash 集約原則との整合: /v1/mobile/views/boot はバージョン情報・L0・L1 のみを返し、ホーム画面の本体データ(L2)は混ぜない。 ETag を効かせ、locale 切替時は別 endpoint へ切り出して boot 自体の再取得コストを最小化する(memory: feedback_parky_bootstrap_split_not_merge)。

Aligned with the Splash-aggregation rule: /v1/mobile/views/boot carries version info + L0 + L1 only. Home screen data (L2) lives elsewhere so ETag stays effective and locale switching does not cost a full boot refetch (memory: feedback_parky_bootstrap_split_not_merge).

5. ActionEnvelope の構造5. ActionEnvelope structure

actions 系 mutation endpoint(例: /v1/mobile/actions/sessions/start)が返すレスポンス契約。α 型: 成功時に「次に描画すべき View の data」を同梱することで、クライアントは再 fetch せずに即描画する。

Actions mutation endpoint (e.g. /v1/mobile/actions/sessions/start) response contract. α variant: the response already carries the next view's data so the client can re-render without an extra fetch.

{
  "result":      { /* 操作結果 / next-view data */ },
  "navigation":  { "target": "parking_detail|none", "params": {...}, "strategy": "push|replace|pop_to_root" },
  "toast":       { "kind": "success|error", "message_code": "..." },
  "meta":        { "request_id": "...", "server_time": "...", "mutation_id": "uuid" }
}

6. TelemetryAck の構造6. TelemetryAck structure

画面に紐付かない fire-and-forget 記録系(geofence 進入 / 位置テレメトリ / push 受信記録)のレスポンス契約。UI に影響しないので軽量 ack のみ。

Response contract for fire-and-forget telemetry endpoints (geofence entries / location telemetry / push receipt logs). UI-independent, so we ship only a lightweight ack.

{
  "ack":  true,
  "meta": { "request_id": "...", "received_at": "...", "event_id": "uuid" }
}

クライアントは ack を待たず処理継続してよい。Idempotency-Key は必須(重複記録防止)。

The client may proceed without awaiting the ack. Idempotency-Key is mandatory to prevent duplicate records.

7. チャネル別 OAS と API surface 境界7. Per-channel OAS and API surface boundaries

OpenAPI はチャネル別に完全分離して 5 ファイルで運用する。1 本の巨大 OAS は誰のためにもならない。

OAS is fully split per channel into 5 files. One giant OAS serves nobody.

チャネルChannel OAS ファイルOAS file 主な pathKey paths クライアントClient
モバイルMobile docs/mobile-app/openapi.json docs/mobile-app/openapi.json /v1/mobile/views/home-feed 等 / /v1/mobile/actions/sessions/start 等 / /v1/mobile/telemetry/events /v1/mobile/views/home-feed etc. / /v1/mobile/actions/sessions/start etc. / /v1/mobile/telemetry/events FlutterFlutter
Web アプリWeb app docs/web-app/openapi.json docs/web-app/openapi.json /v1/parking-lots, /v1/articles 等の公開 resource API /v1/parking-lots, /v1/articles, public resource endpoints parky.co.jp / dev.parky.co.jpparky.co.jp / dev.parky.co.jp
管理者Admin docs/admin/openapi.json docs/admin/openapi.json /v1/admin/* /v1/admin/* admin.parky.co.jpadmin.parky.co.jp
オーナーOwner docs/owner/openapi.json docs/owner/openapi.json /v1/owner/* /v1/owner/* owner.parky.co.jpowner.parky.co.jp
マーケティングMarketing docs/marketing/openapi.json docs/marketing/openapi.json /v1/marketing/* /v1/marketing/* marketing.parky.co.jpmarketing.parky.co.jp

モバイルは resource API を叩かないviews / actions / telemetry の 3 surface(例: /v1/mobile/views/home-feed/v1/mobile/actions/sessions/start/v1/mobile/telemetry/events)だけで完結させる。

Mobile does not hit resource APIs. It is closed under the 3 surfaces views / actions / telemetry (e.g. /v1/mobile/views/home-feed, /v1/mobile/actions/sessions/start, /v1/mobile/telemetry/events).

8. 責任分界点8. Responsibility boundaries

責任Responsibility BFF(サーバー)BFF (server) クライアントClient
データ shape(JSON 契約)Data shape (JSON contract) 受信のみReceive only
UI スタイル(色・余白・タイポ)UI style (colour / spacing / typography)
UI コンポーネント構造UI component structure
画面遷移Screen navigation ✅ navigation で指示✅ Instructed via navigation 遵守(go_router 実装)Follow (go_router)
バリデーションルールValidation rules ✅ validation で配信(最終判定も BFF)✅ Shipped via validation (final check on BFF) 即時表示のみRealtime hint only
状態遷移 (loading/empty/error)States (loading / empty / error) ✅ states / fallback_behaviorstates / fallback_behavior レンダーRender
ビジネスルール計算(料金・予約可否)Business rule compute (fee / bookable)
ローカル永続化(キャッシュ・フォーム)Local persistence (cache / form draft)
認証トークン管理Auth token management 検証のみVerification only ✅(Supabase Auth SDK)✅ (Supabase Auth SDK)
アニメーション / ジェスチャーAnimations / gestures
テレメトリ送信Telemetry submission endpoint 提供Endpoint provision ✅ イベント発火✅ Event firing
Push 通知送信Push delivery ✅ FCM / Resend / X API✅ FCM / Resend / X API 表示のみDisplay only
Realtime 購読Realtime subscription meta.realtime でヒント提供Hint via meta.realtime ✅ Supabase client で接続✅ Supabase client
Deeplink / Push 初期ルーティングDeeplink / push initial routing
文言(i18n リソース)Display strings (i18n) message_code を配信Ship message_code ✅ ローカルリソース優先✅ Local resource first
Feature flagFeature flag ✅ ホワイトリスト経由で配信✅ Via whitelist boolean で分岐Branch on boolean
オフライン fallbackOffline fallback ルール配信Rule distribution ✅ ルールに従って描画✅ Render per rule

9. 契約バージョニング9. Contract versioning

10. 関連ドキュメント10. Related docs