# ShipRex CMS/API — Architecture & Mental Model

> Read this once before your first feature. It is the map every PRP assumes you
> hold in your head. Pair it with [feature-development-guide.md](feature-development-guide.md)
> (how to build) and [database-guide.md](database-guide.md) (how to change data).

---

## 1. What this app is

A **single Node.js (Express) application** that is the backend for the ShipRex
marketing website. It has **two completely separate surfaces** that share the
same models and database:

| Surface | Who uses it | Auth realm | Transport |
|---------|-------------|-----------|-----------|
| **Public API** (`/api/v1/*`) | the marketing website (and future integrators) | **API key** (`x-api-key` / `Bearer`) with **scopes** | JSON in / JSON out |
| **Admin portal** (`/admin/*`) | the ShipRex team | **session cookie** (MySQL-backed), bcrypt login | server-rendered **EJS** (HTML forms) |

> 🔒 **The cardinal rule:** the admin portal is a *different auth realm* and must
> **never** be reachable with an API key, and no admin route may be exposed under
> `/api`. Integrators talk only to `/api/v1`. They never see admin internals.

The app is built to run on **cPanel / Phusion Passenger** with **MySQL**. The
startup file is `app.js` at the repo root (Passenger requirement); all real code
is under `src/`.

---

## 2. Request lifecycle (both surfaces)

```
HTTP → app.js → src/server.js → src/app.js (Express)
  ├─ helmet, compression, morgan
  ├─ CORS (only on /api, origin-allowlisted)
  ├─ body parsers, cookie-parser
  ├─ express-session (MySQL store)        ← admin realm
  ├─ EJS + express-ejs-layouts            ← admin views
  ├─ static /assets → public/
  ├─ webHelpers: methodOverride, flash, locals
  ├─ apiLimiter on /api
  └─ routes (src/routes/index.js)
        ├─ /health, /  (root → /admin)
        ├─ /api/v1/*   → src/routes/api/v1/*   [apiKeyAuth + validate + controller]
        └─ /admin/*    → src/routes/admin/*    [adminAuth + controller → EJS view]
  └─ notFound → errorHandler   (JSON for /api, HTML for admin)
```

- **Public API** path: `route → apiKeyAuth(scope) → [rateLimit] → validate(zodSchema) → controller → service → model → apiResponse.ok()`.
- **Admin** path: `route → requireAdmin → controller → service → model → res.render('view', data)`.

---

## 3. The module pattern (where feature logic lives)

Each feature is a **module** under `src/modules/<feature>/`. A module owns its
**service** (business logic + data access) and its **controllers** (thin HTTP
adapters). The admin-facing controllers live in `src/admin/` and call the same
services.

```
src/
├─ models/                     # Sequelize models + associations (src/models/index.js loader)
├─ modules/<feature>/
│   ├─ <feature>.service.js     # ⭐ business logic + Sequelize queries (the brain)
│   ├─ <feature>.api.controller.js   # public JSON controller (uses apiResponse)
│   └─ (provider/client files)  # e.g. ai.provider.js, brevo.client.js
├─ admin/<feature>.controller.js     # admin EJS controller (res.render)
├─ routes/
│   ├─ api/v1/<feature>.routes.js     # public routes (apiKeyAuth + validate)
│   └─ admin/index.js                 # admin routes (requireAdmin)
├─ views/<feature>/*.ejs              # admin screens
├─ middleware/                        # cross-cutting (auth, validate, errors, rateLimit)
└─ utils/                             # apiResponse, asyncHandler, helpers
```

**Golden separation:**
- **Services** never touch `req`/`res`. They take plain args, return plain data,
  and throw `ApiError` (from `utils/apiResponse.js`) on domain failures.
- **Controllers** are thin: parse the request, call a service, format the
  response (`apiResponse.ok` for JSON, `res.render` for admin), nothing more.
- **Models** are schema only (+ tiny instance/class helpers like password hashing).

The same service powers both surfaces. Example: `kb.service.js` is used by both
`kb.api.controller.js` (public reads) and `src/admin/kb.controller.js` (full CRUD).

---

## 4. Reused building blocks (do NOT re-implement these)

| Concern | Use this | File |
|---------|----------|------|
| Async route → error funnel | `asyncHandler(fn)` | `src/utils/asyncHandler.js` |
| JSON success/error envelope | `ok / created / fail / ApiError` | `src/utils/apiResponse.js` |
| Input validation | `validate(zodSchema, 'body'\|'query')` | `src/middleware/validate.js` |
| Public API auth + scope | `apiKeyAuth('kb'\|'chat'\|'contact'\|'tickets')` | `src/middleware/apiKeyAuth.js` |
| Admin session guard / roles | `requireAdmin`, `requireRole`, `redirectIfAuthed` | `src/middleware/adminAuth.js` |
| Rate limiting | `apiLimiter`, `strictLimiter`, `loginLimiter` | `src/middleware/rateLimit.js` |
| Central errors (JSON vs HTML) | `notFound`, `errorHandler` | `src/middleware/errorHandler.js` |
| Flash + method-override + locals | `webHelpers` | `src/middleware/webHelpers.js` |
| Slug / markdown→text / ids | `slugify`, `markdownToText`, `publicId`, `ticketReference` | `src/utils/helpers.js` |
| Transactional email (logged) | `sendAndLog(...)` | `src/modules/email/email.service.js` |
| Brevo REST | `brevo.sendEmail / upsertContact` | `src/modules/email/brevo.client.js` |
| Config (never read `process.env` directly) | `config` | `src/config/index.js` |
| DB instance | `sequelize` | `src/config/database.js` |
| Logger | `logger.{error,warn,info,debug}` | `src/config/logger.js` |
| Admin layout / partials | `layouts/admin.ejs`, `partials/flash.ejs`, `partials/pagination.ejs` | `src/views/` |
| Admin styles | `public/css/admin.css` (CSS variables, `.card/.table-wrap/.btn/.badge`) | `public/css/admin.css` |

---

## 5. The JSON response contract (public API)

Every `/api/v1` response uses a **stable envelope** so integrators can rely on it:

```jsonc
// success
{ "ok": true, "data": <payload>, "meta": { "page": 1, "limit": 20, "total": 42, "pages": 3 } }
// error
{ "ok": false, "error": { "code": "validation_error", "message": "…", "details": [ … ] } }
```

- Success: `apiResponse.ok(res, data, meta?)` / `created(res, data)` (201).
- Failure: throw `new ApiError(status, code, message, details?)`; the central
  `errorHandler` formats it. **Never** hand-roll error JSON in a controller.
- The **admin** surface does not use this envelope — it renders EJS and uses
  flash messages.

The canonical, machine-readable description of every endpoint is the **Postman
collection**: [`docs/postman/ShipRex_API.postman_collection.json`](postman/ShipRex_API.postman_collection.json).
It is the contract — when you add or change an endpoint, you update it in the
same PRP (see [postman/README.md](postman/README.md)).

---

## 6. Scopes & permissions

- **Public API:** each `ApiClient` row has a `scopes` string (CSV of
  `kb,chat,contact,tickets`). `apiKeyAuth('<scope>')` enforces it. A new module
  that exposes public endpoints adds a **new scope** and documents it.
- **Admin:** `AdminUser.role` ∈ `superadmin | admin | agent`. Gate sensitive
  admin routes with `requireRole('superadmin')`. The default `requireAdmin` only
  checks that *someone* is logged in.

---

## 7. The data layer in one paragraph

Models live in `src/models/*.js` (one file each, `(sequelize, DataTypes) => Model`),
are registered + associated in `src/models/index.js`, and are **mirrored by a
migration** in `migrations/`. Columns are `snake_case` (`underscored: true`),
charset `utf8mb4`. The model is the runtime truth; the **migration is the
deploy-time truth** — they must always agree. Full rules in
[database-guide.md](database-guide.md).

---

## 8. The five product modules (current state)

| # | Module | Public API | Admin | Scope |
|---|--------|-----------|-------|-------|
| 1 | Knowledge Base | `GET /kb/*` | full CRUD | `kb` |
| 2 | Marketing Chatbot | `POST /chat/message` | conversations stored | `chat` |
| 3 | Email / Contact (Brevo) | `POST /contact` | leads pipeline + email logs | `contact` |
| 4 | Support Tickets | `POST/GET /tickets/*` | tickets + replies | `tickets` |
| 5 | Client login flow | *reserved* | *reserved* | *tbd* |

When you build a new feature, it becomes the next module and follows this exact
shape. Use **Knowledge Base** as the canonical reference module — it has the
fullest example of service + public controller + admin controller + views +
model + migration + scope.
