起動フロー(Splash / Bootstrap) Bootstrap flow (Splash)

2026-04 ステータス: 2026-04 status: 現行 endpoint は GET /v1/mobile/views/bootapi/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.
前提: Premise: Splash で必要なデータは 「コードマスタ・アプリ設定・ユーザー情報」と「i18n 辞書」の 2 系統。 Parky はこれを 1 本の集約 endpoint にマージせず、2 本を並列に叩く 設計をとる。 i18n bundle は 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

sequenceDiagram participant App as Flutter App (Splash) participant Cache as Local Cache participant BFF as /v1/mobile/views/boot participant I18n as /v1/mobile/i18n/bundle App->>Cache: read(i18n_version, i18n_messages, codes, codes_version) par parallel fetch App->>BFF: GET /v1/mobile/views/boot (Bearer JWT) BFF-->>App: 200 { data: { me, codes, codes_summary: {version}, app_config, i18n: {lang, version} } } and App->>I18n: GET /v1/mobile/i18n/bundle?lang=ja\nIf-None-Match: "<cached_version>" alt version unchanged I18n-->>App: 304 Not Modified else version changed or no cache I18n-->>App: 200 { lang, version, messages } end end App->>App: reconcile(boot.i18n.version vs response) App->>App: if cached codes_version == boot.codes_summary.version: keep cache, drop payload codes App->>Cache: write(codes, codes_version, app_config, i18n_messages, i18n_version) App->>App: route to next screen (/main or /onboarding)

2.1 ステップ詳細 2.1 Step-by-step

  1. ローカルキャッシュ読み込み:SharedPreferences / Hive から直近の i18n_versioni18n_messagescodes を復元。オフライン起動の安全網にもなる。
  2. Read local cache: restore last-known i18n_version, i18n_messages, and codes from SharedPreferences / Hive. Also acts as a safety net for offline launches.
  3. 2 本の並列 fetchFuture.wait([boot, bundle])。bundle 側は If-None-Match: "<cached_version>" を必ず付与する。
  4. Issue 2 parallel fetches: Future.wait([boot, bundle]). The bundle call must send If-None-Match: "<cached_version>".
  5. reconcileboot.data.i18n.version と、bundle 応答から得た version を突き合わせる。異なる場合は server 側で短時間に更新があったケースなので、boot 側の version を正(最新)とみなし、次回起動時に再取得する。
  6. Reconcile: compare boot.data.i18n.version with the version returned by the bundle call. A mismatch means the server was updated between the two calls; treat boot's value as authoritative and re-fetch on next launch.
  7. キャッシュ書き込み:bundle が 304 なら touch のみ(TTL 更新)、200 なら新 messages + version を保存。
  8. Write cache: on 304 just bump the TTL; on 200 persist the new messages and version.
  9. ルーティングboot.navigation.target に従って /main / /force_update / /onboarding へ遷移。
  10. Route: follow boot.navigation.target to /main / /force_update / /onboarding.
最適化オプション(任意): Optional optimization: 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" の扱い: Handling 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.

flowchart LR Admin[Admin Portal\n/i18n-messages] -->|PATCH| DB[(public.i18n_messages)] DB -->|triggers version hash change| BFF[/v1/mobile/views/boot/] DB -->|ETag update| Bundle[/v1/mobile/i18n/bundle/] App[Flutter Splash] -->|next launch| BFF BFF -->|boot.data.i18n.version changed| App App -->|If-None-Match mismatch| Bundle Bundle -->|200 with new payload| App

つまり 「管理者が編集すれば次回起動で勝手に追従する」 がデフォルト挙動。強制配信(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).

flowchart TD Start([Splash launch]) --> Hydrate[i18n cache hydrate] Hydrate --> Local{local Supabase
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