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.
- 全クライアントは 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/eventsはTelemetryAckを返す。UI コンポーネント構造は返さない(描画はネイティブ実装)。 - Mobile: views (e.g.
/v1/mobile/views/home-feed) returnsViewEnvelope, actions (e.g./v1/mobile/actions/sessions/start) returnsActionEnvelope,/v1/mobile/telemetry/eventsreturnsTelemetryAck. 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
- セキュリティ境界: Service Role Key / FCM 秘密 / X API Secret / Resend token の露出面を BFF に閉じ込める。クライアントはシークレットを持たない。
- Single security boundary: Service Role Key / FCM secret / X API secret / Resend token exposure is confined to the BFF. Clients simply never hold secrets.
- DB を直接触らせない: BFF が aggregate してから返す。RLS は二重防御として残す(穴が開いても DB レベルで止まる)。
- Clients never touch the DB: the BFF aggregates first. RLS stays as defence-in-depth (a bug at the BFF still hits a DB-level stop).
- 契約変更を BFF で吸収: 画面/テーブル変更を BFF で束ね、クライアントの契約は安定に保つ。
- Absorb contract change server-side: schema/screen changes are hidden by the BFF, keeping client contracts stable.
- Edge runtime でレイテンシ最小: Cloudflare Workers で低レイテンシ + キャッシュ + Cron + R2 が 1 プラットフォームに揃う。
- Low latency at the edge: Cloudflare Workers give us low-latency routing, caching, cron, and R2 in one platform.
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() は clientAppVersion
が ENVELOPE_SLIM_MIN_VERSION (現在 "1.0.0") 以上のとき、
validation / states を omit して payload を縮める(client は
meta.view_spec_ref から /v1/mobile/views/boot の ui_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.ts。lib/view-envelope.ts
は薄い再エクスポートファサード。states.error は ERROR_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 bytes
— validation / 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.
- 主役 (mismatch 検知): 各 view レスポンスに
meta.expected_ui_versionを載せ、クライアントがローカルキャッシュの version と突き合わせる。ズレたら背景で/v1/mobile/views/bootを再取得して L0/L1 を更新。平常時はゼロ往復。 - 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/bootin the background. Steady state = zero extra round-trips. - 保険 (24h ETag probe): フォアグラウンド復帰 + 前回
/v1/mobile/views/bootから 24h+ 経過していたら、If-None-Matchで 304 probe。差分があれば本体取得。アプリが長期スリープしていても確実に整合。 - Safety net (24h ETag probe): on foreground resume, if the last
/v1/mobile/views/boothit is 24h+ old, do anIf-None-Match304 probe. Pull the body only on a real diff. Keeps long-suspended apps consistent. - 緊急 (push-bust): silent FCM/APNs で
{"type":"ui_bust"}を配信して即時に boot 再取得をトリガ。MVP ではスキップ可(mismatch だけで実用十分)。 - 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
- rule の instance(例:
max_length=500 → 300)はアプリ更新なしで/v1/mobile/views/boot経由で差替可能 - A rule instance (e.g.
max_length=500 → 300) ships via/v1/mobile/views/bootwith no app update required - rule の 種別(例: 新しい validator type を追加)は Flutter 側に validator 実装が必要、Flutter 先行リリース必須
- A new rule type (a new validator) requires the matching Flutter implementation — ship the Flutter side first
- 運用: 汎用 type(
required/min_length/max_length/regex/numeric_range)を最初に Flutter に揃え、以後は instance のみで戦う - Operationally: front-load the generic types (
required/min_length/max_length/regex/numeric_range) into Flutter and then iterate purely on instances
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" }
}
navigationは必須。同画面再描画はNAVIGATION_REFRESH_CURRENT({target:"none", strategy:"replace"})。navigationis required. Re-rendering the current screen usesNAVIGATION_REFRESH_CURRENT({target:"none", strategy:"replace"}).- Idempotency:
routes-manifestのidempotent: trueでIdempotency-Keyヘッダを必須化。meta.mutation_idはサーバー採番の ack key。 - Idempotency: set
idempotent: trueinroutes-manifestto require theIdempotency-Keyheader.meta.mutation_idis a server-issued ack key.
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 | 主な path | Key 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 |
Flutter | Flutter |
| 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.jp | parky.co.jp / dev.parky.co.jp |
| 管理者 | Admin | docs/admin/openapi.json |
docs/admin/openapi.json |
/v1/admin/* |
/v1/admin/* |
admin.parky.co.jp | admin.parky.co.jp |
| オーナー | Owner | docs/owner/openapi.json |
docs/owner/openapi.json |
/v1/owner/* |
/v1/owner/* |
owner.parky.co.jp | owner.parky.co.jp |
| マーケティング | Marketing | docs/marketing/openapi.json |
docs/marketing/openapi.json |
/v1/marketing/* |
/v1/marketing/* |
marketing.parky.co.jp | marketing.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_behavior | ✅ states / 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 flag | Feature flag | ✅ ホワイトリスト経由で配信 | ✅ Via whitelist | boolean で分岐 | Branch on boolean |
| オフライン fallback | Offline fallback | ルール配信 | Rule distribution | ✅ ルールに従って描画 | ✅ Render per rule |
9. 契約バージョニング9. Contract versioning
X-App-Versionヘッダはすべての/v1/*で必須。セマンティックバージョン(MAJOR.MINOR.PATCH)。X-App-Versionis required on every/v1/*call. Semantic version (MAJOR.MINOR.PATCH).- BFF は
meta.min_app_versionと照合し、古ければfallback_behavior.on_version_mismatchで挙動指示:force_update/degrade/ignore。 - The BFF compares against
meta.min_app_versionand directs behaviour viafallback_behavior.on_version_mismatch:force_update/degrade/ignore. - 互換不可変更時は HTTP 426 Upgrade Required を返し、クライアントは強制アップデートモーダルを表示する。
- On incompatible changes the BFF returns HTTP 426 Upgrade Required and the client shows a force-update modal.
- OpenAPI の
info.versionはapi/package.jsonと同期させる(contracts ワークフローで lock)。 - OpenAPI
info.versionstays in sync withapi/package.json(locked by the contracts workflow). sunset_dateが設定された View はその日以降停止。クライアントは次回起動時にアップデート導線を提示する。- Views carrying
sunset_datestop being served after that date. Clients should surface an update prompt on next launch.