# Architecture How this codebase is organised, and the rules that keep it that way. Read this before adding a feature; it should take about five minutes. ## The layers Data flows in one direction, and each layer is allowed to know only the one below it: ``` component / page renders state, owns no rules ↓ features//hooks binds a service or repository to React ↓ features//services domain rules: validation, interpretation, defaults ↓ features//repositories TRANSPORT ONLY — the single place that knows a URL ↓ app/api//route.ts HTTP boundary: authorises, parses, responds ↓ features//mock/*.mock fixtures, server-side only ``` **Connecting a real backend touches the repository layer and nothing else.** Point `NEXT_PUBLIC_API_BASE` at a host, or rewrite the four lines in a repository, and every component above it is untouched. That is the property the whole structure exists to protect, so: - **Never** `import` from a `mock/` module in a component, hook or page. Server Components read through a `*ServerRepository`; clients go over HTTP. - **Never** call `fetch` in a component. `shared/services/httpClient` is the only module that does. - A repository never interprets a response, and a service never builds a URL. ## Where things live ``` src/ app/ routing only — thin files, no logic (public)/login unauthenticated routes → PublicLayout → GuestGuard (workspace)/… authenticated routes → ProtectedLayout → AuthGuard api/ route handlers proxy.ts the server-side auth gate (Next 16 renamed middleware → proxy) features// components/ UI for this feature only hooks/ data access + view state services/ domain rules, framework-free, unit-testable repositories/ URLs and verbs types/ the wire contract for this feature mock/ fixtures (server-side) utils/ · config/ · guards/ · providers/ as needed shared/ components/ brand · charts · patterns · primitives · scope · data · motion hooks/ useResource · useScope · useBreakpoint · usePersistentFlag layouts/ ProtectedLayout · PublicLayout · workspace shell providers/ app-wide state (workspace scope) services/ httpClient (transport) · apiRoute (handler helpers) types/ the response envelope mock/ seeded RNG shared by every fixture theme/ design tokens; edit loyalyTheme.ts, run `npm run theme:build` ``` A feature owns everything about itself. If two features need the same thing, it moves to `shared/` — it does not get imported across features, with one deliberate exception: `features/dashboard/types` holds the analytics primitives (`TimePoint`, `ActivityEvent`) that LYTs and Stores genuinely share, because duplicating them would let two modules disagree about the same wire shape. ## Authentication Three independent gates, in order of authority: 1. **`src/proxy.ts`** — runs before any protected route renders. No valid session cookie, no page. API paths get a 401 JSON; pages get a redirect to `/login?next=…`. This is the one that matters. 2. **`requireApiSession()`** — every data route handler calls it. Next's own docs warn that proxy coverage can be silently lost by a matcher change or a route move, so the check is repeated where the data is. 3. **`AuthGuard` / `GuestGuard`** — client-side. Covers what the server never sees: a session expiring in an open tab, a logout in another tab, a client-side navigation. **Not a security boundary.** The session is a signed httpOnly cookie (`features/auth/services/sessionToken`), so the browser cannot read or forge it. `SessionProvider` holds only what to draw, never what to permit — it asks `GET /api/auth/session` and believes the answer. Swapping the mock for a real identity provider means changing `verifyCredentials` in `features/auth/mock/users.mock.ts` and the three URLs in `authRepository`. Nothing else, including the login form, is aware. ## Conventions - **No `any`.** The codebase has zero, and `tsc --noEmit` is expected to pass before every commit. - **Components stay under ~300 lines.** When one grows past that, the split is usually behaviour-into-a-hook, not markup-into-more-markup. - **Names say what the thing is**: `DashboardKpiCard`, `StaffAttendanceTable`. Never `Card.tsx`, `utils.ts`, `NewFile.tsx`. - **Styling comes from the design system**, in this order: component props → token-backed Tailwind utilities → a theme override in `loyalyTheme.ts`. There are no per-feature CSS files; see the note below. ### On CSS The brief that produced this refactor asked for per-feature stylesheets (`dashboard/styles/kpi.css`, and so on) so that changing one module cannot affect another. That isolation is already total here, by construction rather than by convention: styling lives in component props and utility classes scoped to the element they are written on, so there is no selector in the codebase that *can* reach another feature. Adding stylesheets would introduce global selectors — the one mechanism that can leak — and they would have to fight StyleX's `@layer astryx-base` for the cascade. Shared visual decisions belong in `theme/loyalyTheme.ts`, which is the single place a change is meant to be system-wide. If a future component genuinely needs CSS that props and utilities cannot express, `astryx swizzle ` ejects the source for that one component rather than opening a stylesheet the whole app can be edited through.