# PRD: Fix App Version Fetching with Server Prefetch + Hydrator
## Problem
- The app shows a missing translation key for `common.toasts.update.description.` because `appState.version.update_type` is empty.
- Root cause: The app never fetches or populates `appState.version` with backend data.
## Current State (validated)
- Backend API and internal route exist and work: `/app/api/version/route.ts` calls `fetchFromApi('/version')`.
- Client util exists but is unused: `utils/fetchLatestVersion.tsx`.
- Global state shape: `utils/appState.tsx` initializes `version` as `{ version: '0.0', update_type: '', updated_at: '' }`.
- `UpdateToastClient` reads `appState.version` but never triggers a fetch and only checks on mount.
## Goals
- Populate `appState.version` reliably on app load without extra client latency.
- Show the update toast exactly when appropriate (within time window and not already seen).
- Avoid redundant network requests and race conditions.
## Solution Overview
- Server-prefetch the version in the localized layout (`app/[locale]/layout.tsx`).
- Hydrate global state on the client via a tiny `VersionHydrator` client component.
- Make `UpdateToastClient` reactive to version changes using Valtio snapshots.
- Ensure `update_type` maps to valid i18n keys; add a small fallback mapping.
## Implementation Plan
1) Server Prefetch in `app/[locale]/layout.tsx`
- Fetch version data on the server and pass to the client for hydration.
- Use either of:
- Direct helper: `const version = await fetchFromApi('/version')` (from `app/lib/api-utils.ts`), or
- Next fetch: `const res = await fetch('/api/version', { cache: 'no-store' }); const version = await res.json();`
- Do this inside the default exported async layout function.
- Handle errors gracefully (wrap in try/catch and set `version = null`).
2) Add `VersionHydrator` (client)
- File: `app/components/VersionHydrator.tsx`
- Behavior: On mount and when `version` prop changes, set `appState.version`.
Example:
- 'use client'
- import { useEffect } from 'react'
- import { appState } from '~/utils/appState'
- export default function VersionHydrator({ version }: { version: AppUpdate | null }) {
useEffect(() => {
if (version && version.updated_at) {
appState.version = version
}
}, [version])
return null
}
3) Wire Hydrator in Layout
- In `app/[locale]/layout.tsx`, render `` alongside `` and ``.
- Order suggestion (not strict): Hydrator before UpdateToastClient.
4) Make `UpdateToastClient` reactive
- Import `useSnapshot` from `valtio` and observe `appState.version`.
- Change the `useEffect` dependency to run when `version?.updated_at` becomes available.
- This ensures the toast can open even if hydration happens after mount.
Sketch:
- import { useSnapshot } from 'valtio'
- const { version } = useSnapshot(appState)
- useEffect(() => {
if (version && version.updated_at) {
const cookie = getToastCookie(version.updated_at)
const now = new Date()
const updatedAt = new Date(version.updated_at)
const validUntil = add(updatedAt, { days: 7 })
if (now < validUntil && !cookie.seen) setUpdateToastOpen(true)
}
}, [version?.updated_at])
5) Validate i18n key mapping for `update_type`
- Current keys: `common.toasts.update.description.content` and `...feature`.
- Ensure API returns only these (or map unknown types to a default):
- const typeKey = ['content', 'feature'].includes(version.update_type) ? version.update_type : 'content'
- appState.version.update_type = typeKey
6) Optional: Pages Router compatibility (temporary)
- If the Pages Router `pages/_app.tsx` still needs server-unavailable behavior, add a small client hydrator there too or remove coupling to `appState.version`.
## Files to Update
- `app/[locale]/layout.tsx` (server prefetch + render Hydrator)
- `app/components/VersionHydrator.tsx` (new)
- `app/components/UpdateToastClient.tsx` (valtio snapshot + effect dependency)
- Optional: Add a small mapping for `update_type` before setting state
## Testing
- With API up: `appState.version` is populated on first render; toast opens if within 7 days and unseen.
- With API down: No crash; state remains initial; toast doesn’t open.
- Verify both locales; i18n keys resolve correctly for `update_type`.
- Confirm no duplicate requests across navigations.
## Risks & Mitigations
- Race between hydrator and toast effect: mitigated by making toast reactive to `version?.updated_at`.
- Caching behavior: using `no-store` avoids stale data; can switch to `revalidate` if desired.
- API type mismatch: handle unknown `update_type` with a default mapping.
## Rollout Steps
- Implement server prefetch and hydrator.
- Update `UpdateToastClient` reactivity.
- Ship behind no feature flags (safe, read-only state).
- Observe logs and confirm behavior in both locales.