# Client structure The route tree, the server/client boundary, and the page pattern every screen follows. > Last verified: 2026-07-30 against commit `d3ec723`. --- ## 1. `src/` at a glance | Folder | Holds | | --- | --- | | `app/` | The App Router tree. Everything under `[locale]/` | | `components/` | Shared UI — `common/` primitives plus one folder per domain composite family | | `constants/` | App-wide named constants (`routes.ts`, `roles.ts`, `headers.ts`, `policy.ts`) | | `context/` | React context providers — `auth/` (AuthContext + reducer) | | `hooks/` | Cross-cutting hooks (`auth.ts`, `capabilities.ts`, `layout.ts`, `useAdminListState.ts`) | | `i18n/` | next-intl wiring: `routing.ts`, `request.ts`, `navigation.ts` | | `layout/` | The one mobile app shell: `AppFrame`, `MobileShell`, the four actor layouts, chrome components | | `lib/` | Infrastructure: `api/` (fetch), `auth/` (token/session), `cookies/`, `query/`, `toast/` | | `services/` | 22 domain services, one folder each — the data layer | | `theme/` | Palette, tokens, typography, the pre-built themes | | `utils/` | Money, dates, numbers, CSV, text helpers | Plus `messages/{en,fa}.json`, `middleware.ts`, and `next.config.mjs` (which only wires the next-intl plugin and `reactStrictMode`) outside `src/`. This is a **pattern map, not a file listing**. `git ls-files client/src` enumerates files for free and never goes stale; what follows is the shape those files have to fit. --- ## 2. The one absolute rule: no layout above `[locale]` **`src/app/[locale]/layout.tsx` IS the root layout.** It renders `` and ``. There is no `src/app/layout.tsx`, and adding one — or any layout above the `[locale]` segment — breaks both locales. **Why**, because this is worth understanding rather than obeying: a layout above `[locale]` is *shared* between `/fa` and `/en`. Next.js statically caches it at build time with `defaultLocale` (`fa`) and never re-renders it on a client-side locale switch, because the segment it is keyed on doesn't change. Its `lang`, `dir`, messages, providers, and fonts therefore **freeze on `fa`/`rtl` for every route, including `/en`**. The `[locale]` layout is the lowest boundary keyed on the locale param, so it is the only place `` can reliably track the active locale. ### What the root layout owns - The locale, sourced from the **URL param** (`params.locale`), validated against `routing.locales` with a fallback to `defaultLocale`. **No header reads.** - `` — `dir` from `getDirection(locale)` — plus `data-mui-color-scheme` from `getThemeMode()`. - The per-locale font class (Mikhak on `fa` only — see [theme.md](theme.md)). - `setRequestLocale(locale)`, so server components deeper in the tree can call `getLocale()` / `getTranslations()` reliably. **Never remove it** — without it, deeper RSCs always see `defaultLocale`. - `getMessages({ locale })` with the locale passed **explicitly**, so `getRequestConfig` receives it via `Promise.resolve(locale)` rather than through the `React.cache` read — which avoids a cache-ordering race. **Never call `getMessages()` bare.** - The providers: `NextIntlClientProvider`, `AuthProvider` (seeded with `getServerAuthState()`), `ThemeProvider`, `NotistackProvider` + `ToastBridge`. - `generateStaticParams`, so Next can enumerate locale routes at build time. - `generateMetadata` — the `'%s | بالین‌یار'` / `'%s | Balinyaar'` title template, the default title and description, and `metadataBase: new URL(SITE_URL)` so child pages' relative OG/canonical URLs resolve absolute. `SITE_URL` comes from `src/config.ts`, never a hard-coded origin. **Never add `notFound()` to the `[locale]` layout.** Unknown locales are handled by middleware; a hard 404 there breaks the fallback. ### The two files that legitimately sit above `[locale]` | File | Why it's allowed | | --- | --- | | `app/global-error.tsx` | It *replaces* the root layout on a root-level crash, so it renders its own `` — which means it **cannot use next-intl**. It is the one sanctioned static-string exception: keep it minimal and bilingual (fa + en) | | `app/robots.ts`, `app/sitemap.ts` | Route handlers, not layouts. They enumerate the public surface across both locales | --- ## 3. The server/client boundary | Never import | From | | --- | --- | | `next/headers` | a client component | | `next-intl/server` | a client component | | `@/lib/cookies/server` | a client component | | `@/lib/cookies/client` | an RSC | The build fails on the first three. The fourth fails at runtime, quietly, which is worse. Route-group layouts (`(private-routes)/layout.tsx`, `(public-routes)/layout.tsx`) are `'use client'` — they only wrap a layout component and need no server capabilities. Never mix `clientFetch` and `serverFetch` in the same file; keep `clientApi.ts` and `serverApi.ts` separate. Next enforces the environment boundary at build time. --- ## 4. The route tree, by shape Everything lives under `src/app/[locale]/`. Route groups add no URL segment. ``` [locale]/ ├── layout.tsx error.tsx not-found.tsx [...rest]/page.tsx ├── (private-routes)/ layout.tsx mounts useSessionRoleSync │ ├── _chrome/ shared loading skeleton (private, not a route) │ ├── select-role/ first-use role picker, own FocusedLayout │ ├── (customer)/ the family app — no URL segment │ ├── (customer-focused)/ chrome-free counterpart, same URL space (onboarding) │ ├── nurse/ the nurse app │ ├── admin/ the backoffice │ └── partner/ the partner-centre portal — a SEPARATE authz scope └── (public-routes)/ login · terms · privacy · welcome ``` | Convention | Meaning | | --- | --- | | `(parenthesised)` | A route group. Adds no URL segment; exists to attach a layout and a `RoleGuard` | | `_`-prefixed folder | Private, **not a route** — `_chrome/`, `admin/_hub/` | | `[...rest]/page.tsx` | The catch-all. Calls `notFound()` so any unmatched path under a locale renders `not-found.tsx` — next-intl's recommended 404 pattern | | `loading.tsx` | A route-group skeleton shaped like that group's content area. The `MobileShell` chrome is already rendered by the enclosing layout, so a skeleton shapes the content only | Each private group's `layout.tsx` is `'use client'` and wraps `RoleGuard` → that actor's layout: `(customer)` → `CustomerLayout`, `nurse` → `NurseLayout`, `admin` → `AdminLayout`, `partner` → `PartnerLayout`. `(customer-focused)` and `select-role` wrap `FocusedLayout` instead, for flows a user must not be able to tab away from mid-setup. The **partner portal is a separate authorization scope**: a centre admin is not a Balinyaar admin. Its `RoleGuard` passes no `expected` role (it isn't an `AppRole`) and each page resolves the caller's *own* centre via `useMyPartnerCenter`. See [auth.md](auth.md). **When you add, remove, or rename a route group, a provider, or a top-level `src/` folder, update the "Project structure" section in [client/CLAUDE.md](../../../client/CLAUDE.md) and §1 above in the same change.** --- ## 5. The page pattern The root layout owns a title *template*; a route supplies the `%s`. So a route that wants its own tab title splits in two: ```tsx // page.tsx — a thin RSC. No 'use client'. import type { Metadata } from 'next'; import { getTranslations } from 'next-intl/server'; import HomeScreen from './HomeScreen'; export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'shell' }); return { title: t('customer_app') }; } export default function Page() { return ; } ``` ```tsx // HomeScreen.tsx — 'use client'. All the logic and JSX. ``` Rules that fall out of it: - The screen component is **co-located** with `page.tsx` and named `Screen.tsx`, so its existing relative imports keep working unchanged. - `page.tsx` never renders `` and never touches `document.title`. Assigning `document.title` in a render body throws `ReferenceError: document is not defined` during build-time prerendering. - A static `metadata` export is fine when the title needs no translation lookup. - Page bodies stay **composition + content**. Reusable visuals move to `src/components/`; page-only, never-reused composition can stay in the page. Adoption is partial by design: the landing pages (customer home, `/login`, `/search`, `/bookings`, `/nurse`, `/admin`, `/partner`) plus `/welcome` use it. The rest still render directly and gain it when the page is next substantially touched. `metadataBase` makes the pattern extend to OG: `/welcome` sets `alternates.canonical` + `openGraph` in its `generateMetadata` and supplies `og:image` from a co-located `opengraph-image.tsx` (`next/og`'s `ImageResponse`). --- ## 6. This is not a static export The app relies on server components, middleware, and server-side cookies. `next.config.mjs` wires the next-intl plugin and `reactStrictMode` and nothing else — don't add `output: 'export'`, and don't assume a page can be prerendered without its request context.