API アーキテクチャ方針 — BFF 一本化 + SDUI Level 3 API architecture — BFF-first + Server-Driven UI Level 3

2026-04-23 に確定した Parky の API アーキテクチャ方針のまとめ。 全クライアント(モバイル / Web / 管理者・オーナー・マーケティングの各ポータル)は Cloudflare Workers 上の BFF (/v1/*) だけを叩く。 モバイルは SDUI Level 3 に固定し、 OpenAPI 契約(OAS)はチャネル別に完全分離する。

The Parky API architecture locked on 2026-04-23. Every client (mobile / web / admin / owner / marketing portals) talks only to the Cloudflare Workers BFF (/v1/*). Mobile is pinned at SDUI Level 3, 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.
  • モバイルは SDUI Level 3 固定。/v1/mobile/views/*ViewEnvelope/v1/mobile/actions/*ActionEnvelope/v1/mobile/telemetry/*TelemetryAck を返す。UI コンポーネント構造(L4/L5)は返さない。
  • Mobile is pinned to SDUI Level 3: /v1/mobile/views/* returns ViewEnvelope, /v1/mobile/actions/* returns ActionEnvelope, /v1/mobile/telemetry/* returns TelemetryAck. No UI component trees (L4/L5).
  • 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[/mobile/views
mobile/actions
mobile/telemetry/] WEBSUR[/public resource API
/v1/parking-lots etc./] ADMSUR[/admin/*/] OWNSUR[/owner/*/] MKTSUR[/marketing/*/] end subgraph Backend["Backend"] SB[(Supabase DB
+ RLS)] FCM[FCM / Resend / X API] end MOB -- ViewEnvelope/Action/Telemetry --> MOBSUR WEB --> WEBSUR ADM --> ADMSUR OWN --> OWNSUR MKT --> MKTSUR MOBSUR --> SB WEBSUR --> SB ADMSUR --> SB OWNSUR --> SB MKTSUR --> SB MOBSUR --> FCM ADMSUR --> FCM MKTSUR --> FCM MOB -. Realtime (Supabase client) .-> SB

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. Why BFF-first

3. SDUI レベル定義と L3 採用理由3. SDUI level definitions and why L3

Level サーバーが返すものWhat the server returns Parky 採否Parky adoption
L1 生の JSON(スキーマ任せ) Raw JSON, schema-by-convention —(旧世代) — (legacy)
L2 正規化されたドメインデータ(REST / GraphQL) Normalised domain data (REST / GraphQL) Web アプリはほぼ L2 相当 Web app is effectively L2
L3 画面単位 aggregate(ViewEnvelope。データ + 遷移指示 + バリデーション + UI Config。UI は各プラットフォーム実装 Per-screen aggregate (ViewEnvelope): data + navigation + validation + UI config. UI widgets remain platform-native モバイルで採用 Adopted for mobile
L4 コンポーネントツリー JSON(例: Airbnb GraphQL Driven UI) Component-tree JSON (e.g. Airbnb GraphQL Driven UI) ❌ 不採用 ❌ Rejected
L5 DSL + runtime(例: Airbnb Ghost Platform / Server Driven Widgets) DSL + runtime (e.g. Airbnb Ghost Platform, Server Driven Widgets) ❌ 不採用 ❌ Rejected

なぜ L3: サーバー主導で振る舞い(ルール・文言・遷移・フラグ)を動的に変えたい。 ただし UI の見た目は Flutter Material / React の強みを殺したくない。L4/L5 だと自前のレンダリングエンジンを持つ羽目になり、 アニメーション・ジェスチャー・Mapbox のようなネイティブコンポーネントと噛み合わない。L3 はその中間の「運用の機動力と描画の自由度が両立する」落としどころ。

Why L3: we want the server to drive behaviour (rules, copy, navigation, flags) without losing Flutter Material / React's native-widget advantage. L4/L5 forces us to maintain a custom render engine that fights animations, gestures, and Mapbox. L3 is the midpoint where operational agility coexists with native rendering freedom.

4. ViewEnvelope の構造4. ViewEnvelope structure

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

Every /v1/mobile/views/* endpoint returns this envelope. data, meta, and fallback_behavior are required; the rest are optional depending on the screen.

{
  "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": "...", "cache_key": "...", "min_app_version": "1.4.0", "sunset_date": null, "realtime": {...} }
}
フィールド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

実装ヘルパーは lib/view-envelope.tsbuildViewEnvelope() / buildErrorStatesFromCodes() / resolveClientFeatureFlags()states.errorERROR_CATALOG + ERROR_MESSAGE_KEYS から自動派生させること(手書き禁止)。

The implementation lives in lib/view-envelope.ts: buildViewEnvelope(), buildErrorStatesFromCodes(), resolveClientFeatureFlags(). Never hand-write states.error; always derive it from ERROR_CATALOG + ERROR_MESSAGE_KEYS.

5. ActionEnvelope の構造5. ActionEnvelope structure

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

Mutation endpoint (/v1/mobile/actions/*) 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,actions,telemetry}/* /v1/mobile/{views,actions,telemetry}/* 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 を叩かない/v1/mobile/{views,actions,telemetry}/* の 3 surface だけで完結させる。 旧 /v1/mobile/boot / /v1/mobile/home-feed / /v1/mobile/lots/<id> 等の thin wrapper は Sunset ヘッダ付きで 2026-07-31 まで並走、その後 410 Gone。

Mobile does not hit resource APIs. It is closed under the 3 surfaces /v1/mobile/{views,actions,telemetry}/*. Legacy paths (/v1/mobile/boot, /v1/mobile/home-feed, /v1/mobile/lots/<id>, …) run alongside with Sunset headers until 2026-07-31, then return 410 Gone.

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 ❌ L4/L5 採用せず❌ No L4/L5
画面遷移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