# Architecture

## Stack

| Layer | Choice | Why (template ADR) |
| --- | --- | --- |
| Language | TypeScript, Node 24 LTS | 0001 |
| Framework | TanStack Start (React, file routes, server functions), Nitro node-server output | 0002 |
| API access | `@ezytail/ezyflow-sdk` (generated from pinned API specs) | 0006 |
| Shared behaviour | `@ezytail/ezyflow-start` (config, token cache, login glue, delivery verification, inbox, worker, MCP server) | 0011 |
| Database | Postgres 17, schema by Drizzle | 0005, 0009 |
| App-user auth | Better Auth + pluggable OIDC (Authentik in prod, mock issuer locally) | 0004, 0009 |
| Webhooks | Postgres inbox + pg-boss worker | 0008 |
| UI | Tailwind 4, shadcn/radix, lucide, sonner; TanStack Query / Table / Form + Zod | conventions |
| i18n | paraglide, `messages/{fr,en}.json` | conventions |
| Tests | Vitest + Testing Library + MSW (no network), Playwright e2e on the compose stack | conventions |
| Packaging | multi-stage `Dockerfile` (node:24-slim, non-root), `docker-compose.yml`, optional Helm chart | conventions |
| Package manager | pnpm 11, `@ezytail` from GitHub Packages | 0007, 0012 |

## Shape of the system

```
 Browser (App user: viewer | admin)
        │  HTTPS, session cookie (Better Auth, OIDC login)
        ▼
 ┌──────────────────────────── EzyOrder (one container) ───────────────────────────┐
 │  routes/  (screens: /login /orders /orders/$key /procurements /push …)          │
 │     │ createServerFn — role re-derived from session on every call               │
 │  server/  (orders.ts, procurements.ts, push.ts, deliveries.ts, problem.ts)      │
 │     │                                                                            │
 │     ├── sdk client ──────────────► EzyFlow Integrator API  (one activity)        │
 │     │   (Runtime: token cache,       • canonical.pushEvent  (+ dry_run)          │
 │     │    trace-id logging)           • projection reads (list/get/timeline…)     │
 │     │                                • delivery API (query, replay, test send)   │
 │     │                                                                            │
 │  routes/api/ezyflow/deliveries ◄──── signed POST (delivery) from EzyFlow         │
 │     │ Runtime verifies HMAC → inbox row (dedup by message id) → pg-boss job      │
 │     ▼                                                                            │
 │  worker → server/handlers.ts  registry.on("<event.type>", handler)               │
 │                                                                                  │
 │  /health (liveness)   /ready (readiness)                                         │
 └──────────────────────────────────┬───────────────────────────────────────────────┘
                                    │
                               Postgres  (Runtime inbox + login tables, pgboss schema,
                                          EzyOrder-only tables via Drizzle)
```

## Two identities (ADR 0004)

- **API client** — machine identity, `EZYFLOW_CLIENT_ID` / `EZYFLOW_CLIENT_SECRET`, created by an
  activity administrator on the portal's *API Tokens* page. Server side only. Exactly one.
- **App user** — a person, via OIDC (`OIDC_*`). Never known to the EzyFlow API.
  A route guard decides navigation only; every server function re-checks the role
  (`requireAppUser`, `requireAdmin` from the Runtime), and every write toward the API requires `admin`.

## Directory layout (scaffolded App)

```
ezyorder/
├── AGENTS.md                 # Integrator prose + generated Brief (markers)
├── CLAUDE.md                 # "@AGENTS.md"
├── .ai/                      # this folder — product & project context
├── .claude/agents/           # Claude Code subagents (ezyorder-engineer)
├── .mcp.json, .cursor/, .vscode/   # generated MCP registration
├── docs/ezyflow/             # generated Primer — never hand-edit
├── messages/{fr,en}.json     # UI catalogs
├── src/
│   ├── routes/               # file routes (screens) + routes/api/** (HTTP endpoints)
│   ├── server/               # server-only modules (guarded) and server functions
│   │   ├── ezyflow.ts        # config, logger, db, token cache, sdk client, pg-boss
│   │   ├── handlers.ts       # delivery handlers registry
│   │   ├── orders.ts         # order projection reads
│   │   ├── push.ts           # order push: Zod input → body builder → rehearse / push
│   │   ├── problem.ts        # API Problem → UI answer
│   │   └── schema.ts         # Drizzle schema (EzyOrder-only tables)
│   ├── components/           # app shell, data table, problem panel, ui/ (shadcn)
│   └── lib/                  # auth, formatting, utils
├── scripts/                  # migrate, agents, deliver, env-local, mock-api
├── tests/                    # Playwright e2e
├── Dockerfile, docker-compose.yml
└── k8s-charts/app/           # only with deployment-helm
```

## Data ownership

- **EzyFlow is the source of truth** for orders, procurements, products, stock. EzyOrder reads
  projections; it does not mirror them.
- **Postgres in EzyOrder** holds: the Runtime's inbox and login tables (migrated by
  `pnpm db:migrate`), the `pgboss` schema, and only EzyOrder-specific data (e.g. drafts).
- A push, once accepted, **is** the event. EzyOrder may forget it.

## Runtime endpoints

| Path | Purpose |
| --- | --- |
| `/health` | liveness |
| `/ready` | readiness (DB and config) |
| `/api/auth/*` | Better Auth / OIDC |
| `/api/ezyflow/deliveries` | delivery receiver (webhooks add-on); register `<origin>/api/ezyflow/deliveries` in the portal |
