起動フロー(Splash / Bootstrap) Bootstrap flow (Splash)
GET /v1/mobile/views/boot(api/src/bff/mobile/legacy/boot.ts)。
routes-manifest 上で stability: 'deprecated', sunsetDate: '2026-07-31' がマーク済みで、
後継の View 集約・ui_layer 同梱方式に移行中(BOOT_SPEC_REF)。Sunset 後は 410 Gone を返す予定。
移行詳細は mobile-bff-design / mobile-bff-impl を参照。
Current endpoint: GET /v1/mobile/views/boot (api/src/bff/mobile/legacy/boot.ts).
Marked stability: 'deprecated' with sunsetDate: '2026-07-31' in routes-manifest;
will return 410 Gone after sunset. See mobile-bff-design /
mobile-bff-impl for the migration plan.
ETag / 304 ベースのキャッシュが強く効くため、単体 endpoint として分離する方が
コスト/可搬性の両面で有利。boot 側には「bundle の version だけ」を同梱し、
client はそれで再取得を短絡できる。
Splash needs two classes of data: code master + app config + user profile and the i18n dictionary.
Parky keeps them in two separate endpoints that the client fetches in parallel instead of a single merged aggregate.
The i18n bundle benefits strongly from ETag / 304 caching, so isolating it pays off.
The boot response carries only the bundle's version, letting the client short-circuit the bundle fetch when unchanged.
1. 設計方針 1. Design principles
1.1 なぜ集約 endpoint に混ぜないか 1.1 Why not merge into one endpoint
| 観点Aspect | 単一集約(NG)Single merged (NO) | 分離+並列(採用)Split & parallel (adopted) |
|---|---|---|
| キャッシュ粒度Cache granularity | 辞書が変わるたびに全体 invalidateAny dictionary change invalidates the whole payload | bundle だけ個別 invalidate、code master は無変更Only bundle is invalidated; code master stays hot |
| レスポンス量Payload size | 毎起動 bundle 本体(数十〜数百 KB)を流すShips the whole bundle (tens to hundreds of KB) on every launch | 変更無しなら 304 Not Modified(実質 0 byte)Returns 304 Not Modified when unchanged (effectively 0 bytes) |
| locale 切替Locale switch | boot 全体を再取得することになるForces a full boot re-fetch | bundle endpoint を別 lang で叩くだけJust hit the bundle endpoint for the new lang |
| レイテンシLatency | 1 リクエスト(サーバ側で総和)1 request (sum on the server) | 並列 2 本なので実質 max(boot, bundle)、304 時は boot と同等2 parallel requests so effectively max(boot, bundle); on 304 it matches boot |
1.2 静的 UI 文言 vs 動的コンテンツ 1.2 Static UI strings vs. dynamic content
「画面内に表示される動的文言」はサーバ側で解決して ViewEnvelope.data に直書きする。
一方で、ボタンラベル・フォームバリデーションメッセージ・汎用エラー文言など 静的 UI 文言 は
画面に依存せず、全画面で再利用されるため、i18n_messages テーブル由来の bundle を client 側にキャッシュして使う。
"Dynamic text that appears inside a screen" is resolved server-side and written directly into ViewEnvelope.data.
Static UI strings — button labels, form validation messages, generic error copy —
are screen-agnostic and reused across the app, so they are served from the i18n_messages table as a bundle
that the client caches locally.
| 分類Category | 例Examples | 解決方法Resolution |
|---|---|---|
| 静的 UI 文言Static UI strings | 「保存」「キャンセル」「ログイン」「メールアドレスを入力してください」"Save", "Cancel", "Sign in", "Enter your email" | i18n bundle → ローカルキャッシュi18n bundle → local cache |
| 動的コンテンツDynamic content | 駐車場名、セッション履歴、プレミアム特典ラベルLot names, session history, premium benefit labels | ViewEnvelope.data に直書き(サーバ解決)Written directly into ViewEnvelope.data (server-resolved) |
| 列挙値ラベルEnum labels | ステータス / カテゴリ / 車種Status / category / vehicle type | codes マスタ → /v1/mobile/views/boot で同梱codes master → bundled in /v1/mobile/views/boot |
2. Splash での並列 fetch フロー 2. Parallel fetch flow on Splash
2.1 ステップ詳細 2.1 Step-by-step
- ローカルキャッシュ読み込み:SharedPreferences / Hive から直近の
i18n_version・i18n_messages・codesを復元。オフライン起動の安全網にもなる。 - Read local cache: restore last-known
i18n_version,i18n_messages, andcodesfrom SharedPreferences / Hive. Also acts as a safety net for offline launches. - 2 本の並列 fetch:
Future.wait([boot, bundle])。bundle 側はIf-None-Match: "<cached_version>"を必ず付与する。 - Issue 2 parallel fetches:
Future.wait([boot, bundle]). The bundle call must sendIf-None-Match: "<cached_version>". - reconcile:
boot.data.i18n.versionと、bundle 応答から得た version を突き合わせる。異なる場合は server 側で短時間に更新があったケースなので、boot側の version を正(最新)とみなし、次回起動時に再取得する。 - Reconcile: compare
boot.data.i18n.versionwith the version returned by the bundle call. A mismatch means the server was updated between the two calls; treatboot's value as authoritative and re-fetch on next launch. - キャッシュ書き込み:bundle が 304 なら touch のみ(TTL 更新)、200 なら新 messages + version を保存。
- Write cache: on 304 just bump the TTL; on 200 persist the new
messagesandversion. - ルーティング:
boot.navigation.targetに従って/main//force_update//onboardingへ遷移。 - Route: follow
boot.navigation.targetto/main//force_update//onboarding.
boot を先に await し、boot.data.i18n.version が手元キャッシュと完全一致した場合に限り
bundle endpoint 呼び出し自体をスキップできる。この最適化は「1 リクエスト削減」だが、ETag / 304 が
既に効いているため効果は 1 往復の RTT 削減のみ。実装コストが安ければ入れてよい。
You may optionally await boot first and skip the bundle call entirely when
boot.data.i18n.version exactly matches the cached version. The gain is one round-trip;
since ETag / 304 already minimises payload cost, adopt this only if the implementation is cheap.
3. エンドポイント仕様 3. Endpoint specs
3.1 GET /v1/mobile/views/boot(集約)
3.1 GET /v1/mobile/views/boot (aggregate)
ViewEnvelope 形式で me / codes / app_config / i18n を返す。
data.i18n.version は i18n bundle の変更検知キー で、bundle 本体は同梱しない。
Returns me / codes / app_config / i18n inside a ViewEnvelope.
data.i18n.version is the cache invalidation key for the i18n bundle; the bundle itself is not included.
{
"data": {
"me": { "user_id": "<uuid>", "email": null, "app_user": { ... } },
"codes": [ { "category_id": "vehicle_type", "code": "sedan", "display_label": "セダン", "sort_order": 1 }, ... ],
"app_config": { "mapbox_token": "...", "feature_X_enabled": true },
"i18n": {
"lang": "ja",
"version": "d41d8cd98f00b204e9800998ecf8427e"
}
},
"ui_config": { "feature_flags": { ... } },
"navigation": { "target": "home", "strategy": "replace" },
"fallback_behavior": { ... },
"meta": {
"server_time": "2026-04-24T10:00:00Z",
"cache_key": null,
"min_app_version": "1.0.0",
"sunset_date": null
}
}
認証は Bearer JWT が必須。X-App-Version ヘッダを付けると min_app_version と比較して
navigation.target: "force_update" が返ることがある。
Requires a Bearer JWT. If the optional X-App-Version header is below min_app_version
the server may return navigation.target: "force_update".
3.2 GET /v1/mobile/i18n/bundle(辞書本体)
3.2 GET /v1/mobile/i18n/bundle (dictionary body)
認証不要(login 前の welcome 画面からも叩けるように)。?lang=ja|en(未指定時 ja)。
If-None-Match で ETag を送ると、変更なしなら 304 Not Modified。
Unauthenticated (callable from the pre-login welcome screen). ?lang=ja|en (default ja).
Send If-None-Match with the cached ETag; the server returns 304 Not Modified when unchanged.
GET /v1/mobile/i18n/bundle?lang=ja
If-None-Match: "d41d8cd98f00b204e9800998ecf8427e"
--- unchanged ---
HTTP/1.1 304 Not Modified
ETag: "d41d8cd98f00b204e9800998ecf8427e"
--- changed ---
HTTP/1.1 200 OK
ETag: "<new_version>"
Cache-Control: max-age=0, must-revalidate
{
"lang": "ja",
"version": "<new_version>",
"updatedAt": "2026-04-24T10:00:00Z",
"messages": {
"common.save": "保存",
"common.cancel": "キャンセル",
"auth.welcome_message": "Parky へようこそ",
...
}
}
3.3 リクエスト相関表 3.3 Request correlation
| 契機Trigger | /v1/mobile/views/boot |
/v1/mobile/i18n/bundle |
備考Notes |
|---|---|---|---|
| 初回起動(キャッシュ無)First launch (no cache) | 200 | 200 | 両方 payload を受信Both return full payload |
| 通常起動(辞書変更なし)Warm launch (no dict change) | 200 | 304 | bundle は ETag ヒットBundle hits ETag |
| 辞書更新後の起動Launch after dict update | 200 | 200 | 新 version に差し替えSwap to the new version |
| locale 切替(画面から)Locale switch (in-app) | — | 200 | bundle のみ別 lang で再取得Re-fetch bundle for new lang only |
| オフライン起動Offline launch | fail | fail | ローカルキャッシュで起動、リトライは navigation.fallback_behavior に従うBoot from cache; retry per navigation.fallback_behavior |
4. クライアントキャッシュ戦略 4. Client cache strategy
| データData | 保存場所Storage | TTLTTL | 無効化トリガInvalidation |
|---|---|---|---|
i18n_messages + i18n_version |
SharedPreferences / Hive | 無期限(サーバ version で管理)Unbounded (server version governs) | boot が返す version と不一致 / locale 切替Mismatch with boot's version / locale switch |
codes |
SharedPreferences / Hive | セッション内(app_configs.codes_version が将来追加されたら version 比較へ)Session (may migrate to version-compare once app_configs.codes_version ships) |
Splash の boot レスポンスで毎回上書きOverwritten on every boot response |
app_config |
SharedPreferences | セッション内Session | Splash の boot レスポンスで毎回上書きOverwritten on every boot response |
me |
メモリ(provider / riverpod)In-memory (provider / Riverpod) | アプリ生存中App lifetime | Supabase session 破棄時 / sign-outSupabase session cleared / sign-out |
version="empty":
i18n_messages テーブルが未投入(row 0 件)の lang に対しては server が
version: "empty" を返す。この場合 client はキャッシュを空で保持し、
UI 文言は hard-coded フォールバックを使う(ほぼ dev 環境のみの挙動)。
When the i18n_messages table has no rows for the requested lang the server returns
version: "empty". The client then keeps its cache empty and uses hard-coded fallbacks.
This normally surfaces only in dev.
5. 辞書更新の伝播 5. Propagation of dictionary updates
管理者が Admin Portal(/i18n-messages)で辞書を編集すると、
i18n_messages.updated_at が変化し、md5(string_agg(key || ':' || value || ':' || epoch))
で計算される bundle version も変わる。次に client が Splash に到達したタイミングで、
boot.data.i18n.version が手元キャッシュと不一致になり、自動的に bundle を再取得する。
When an admin edits entries via Admin Portal (/i18n-messages),
i18n_messages.updated_at changes and the bundle version
(md5(string_agg(key || ':' || value || ':' || epoch))) rolls over.
On the next Splash, boot.data.i18n.version diverges from the cached one and the client auto-fetches the new bundle.
つまり 「管理者が編集すれば次回起動で勝手に追従する」 がデフォルト挙動。強制配信(push)は現状不要。 緊急反映が必要な場合は、アプリ側 foreground 復帰時にも Splash と同じ fetch を走らせる実装を検討する(将来拡張)。
So the default behaviour is "admin edits will pick up on the next launch automatically"; no push delivery needed for now. If urgent propagation is needed, a future enhancement can re-run the same fetch when the app returns to foreground.
6. エラー・エッジケース 6. Errors & edge cases
| ケースCase | 挙動Behaviour |
|---|---|
| boot 失敗(5xx / network)+ bundle 成功boot fails (5xx / network) + bundle succeeds | fallback_behavior に従い「前回 codes キャッシュで起動 → 後ろで retry」Per fallback_behavior: boot from cached codes, retry in the background |
| boot 成功 + bundle 失敗boot OK + bundle fails | 手元の i18n キャッシュで起動、次回 Splash で再取得。UI 文言はキャッシュに無ければ hard-coded fallbackUse cached i18n; retry on next Splash. Fall back to hard-coded strings if the cache is empty |
未サポート lang (?lang=zh-Hant)Unsupported lang (?lang=zh-Hant) |
server は ja にフォールバック(resolveLang())Server falls back to ja via resolveLang() |
| boot と bundle の version 不一致Mismatch between boot and bundle versions | レース(編集直後)。client は boot を正とし、bundle キャッシュを stale flag 付きで保持、次起動で再取得Race (just edited). Client trusts boot, keeps the bundle cache flagged stale, and re-fetches next launch |
| JWT 失効JWT expired | boot が 401。refresh 後リトライ。bundle は認証不要なので影響なしboot returns 401; refresh token and retry. Bundle is unauthenticated, unaffected |
7. 管理フロー(参考) 7. Admin flow (reference)
辞書の CRUD は Admin Portal 側で提供される。bundle 配信フローとは別系統。
Dictionary CRUD lives in the Admin Portal and is decoupled from bundle delivery.
| 操作Operation | エンドポイントEndpoint | UIUI |
|---|---|---|
| 一覧List | GET /v1/admin/i18n-messages |
web/portal/admin/src/pages/I18nMessagesPage.tsx |
| 作成Create | POST /v1/admin/i18n-messages |
〃 |
| 更新Update | PATCH /v1/admin/i18n-messages/{id} |
〃 |
| 削除Delete | DELETE /v1/admin/i18n-messages/{id} |
〃 |
7.5 Splash 後のルーティング判定 7.5 Splash routing decision
SplashController は boot 結果と ローカル Supabase session + OnboardingPrefs.hasSeen() を組み合わせて
次の画面を決める。BFF の navigation.target は強いヒントだが、auth 判定は端末側 session を正とする
(BFF 不調で「サインイン済みユーザーが毎回サインインに飛ぶ」事故を防ぐため)。
SplashController combines the boot response with the local Supabase session and
OnboardingPrefs.hasSeen() to decide the next screen. The BFF's navigation.target is a strong hint,
but auth is judged from the on-device session (so a BFF outage cannot bounce signed-in users back to sign-in).
session?} Local -- expired --> Refresh[refreshSession] Refresh -- ok --> Auth[authenticated] Refresh -- fail --> Anon[anonymous] Local -- valid --> Auth Local -- none --> Anon Auth --> ParBoot[GET /v1/mobile/views/boot] Anon --> ParBoot ParBoot --> Reconcile{boot.navigation
.target?} Reconcile -- force_update --> FU[/force-update/] Reconcile -- home --> Main[/main/] Reconcile -- onboarding --> OB[/onboarding/] Reconcile -- sign_in --> SI[/sign-in/] Reconcile -- location_permission --> LP[/permissions/location/] Reconcile -- empty / network error --> LocalFallback{authenticated
+ onboarding seen?} LocalFallback -- yes --> Main LocalFallback -- no, onboarding seen --> SI LocalFallback -- no, never seen --> OB
Onboarding 既読フラグの遡及付与:
ログイン履歴がある端末では OnboardingPrefs.markSeen() を遡って true にする
(ログアウト後の再起動でオンボーディングが再表示されるのを防止)。
Retroactive onboarding-seen flag:
For devices that have ever signed in, OnboardingPrefs.markSeen() is back-filled to true
so the onboarding tour does not re-appear after a sign-out + relaunch.
8. 関連ファイル 8. Related files
api/src/bff/mobile/legacy/boot.ts— boot endpoint 本体(ViewEnvelope 整形、deprecated 2026-07-31)boot endpoint handler (ViewEnvelope shaping; deprecated 2026-07-31)mobileapp/prototype/flutter/lib/features/splash/application/splash_controller.dart— Flutter 側 Splash オーケストレータ(local session 判定 + boot fetch + onboarding 遡及付与)Flutter splash orchestrator (local-session check, boot fetch, retroactive onboarding-seen)api/src/core/mobile-boot/index.ts—fetchBootData(並列 SQL + i18n summary 合流)fetchBootData(parallel SQL + i18n summary)api/src/core/i18n/get-bundle.ts— bundle 取得 use case + ETag 判定bundle use case + ETag matchingapi/src/data/i18n-messages.data.ts— SQL(bundle 本体 / summary / admin CRUD)SQL (bundle body / summary / admin CRUD)api/src/bff/mobile/views/i18n-bundle.ts— bundle endpoint(If-None-Match→ 304 / 200)bundle endpoint (If-None-Match→ 304 / 200)api/src/bff/admin/i18n-messages.ts— admin CRUD endpointadmin CRUD endpointinfra/supabase/migrations/20260423050000_i18n_messages.sql— テーブル定義Table definition