# 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 `<html>` and `<body>`. 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
`<html lang dir>` 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.**
- `<html lang dir>` — `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 `<html>` — 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<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'shell' });
  return { title: t('customer_app') };
}

export default function Page() {
  return <HomeScreen />;
}
```

```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 `<PageName>Screen.tsx`, so its existing
  relative imports keep working unchanged.
- `page.tsx` never renders `<title>` 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.
