# 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/<f>/hooks        binds a service or repository to React
        ↓
  features/<f>/services     domain rules: validation, interpretation, defaults
        ↓
  features/<f>/repositories TRANSPORT ONLY — the single place that knows a URL
        ↓
  app/api/<f>/route.ts      HTTP boundary: authorises, parses, responds
        ↓
  features/<f>/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/<feature>/
    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 <Component>` ejects the source for that one component
rather than opening a stylesheet the whole app can be edited through.
