# Viață la Țară — Backend Build Plan (Node.js + Express + MongoDB)

A phased, gated build plan for **Claude Code (CLI)** to generate a professional backend.

**Auth model (locked):** native email/password with **backend-issued JWTs** as the primary
system; **Firebase** is used only for **Google Sign-In, Apple Sign-In, and push (FCM)**.
**Language (locked):** TypeScript.

This plan implements the data contract in `backend_api_spec.md` — the `{ data, error }` envelope,
roles `client | business | admin`, pagination, shared enums, the error-code table, and the
layered architecture **route → controller → service → repository → model** with
**per-endpoint Zod validation** — but **overrides the spec's auth mechanism**: clients send the
**backend's** access token (`Authorization: Bearer <accessToken>`), not a Firebase ID token.

---

## How to use this plan with Claude Code

**Gate protocol.** Build one phase at a time, then **stop and wait for confirmation**. After each
phase, print: (a) what was created, (b) how to verify it, (c) what's next.

**Kickoff prompt** (paste into `claude` at the repo root):

```
Read PLAN.md. We are building the Viața la Țară backend per backend_api_spec.md, but with the
auth model defined in PLAN.md (native JWT primary; Firebase only for Google/Apple/FCM). Work
ONE PHASE AT A TIME. Do not start a phase until I type "confirm". Begin with Phase 0: print the
locked decisions + prerequisites and wait. After each phase, summarize, give verification steps,
and STOP.
```

**Per-phase continue:** `confirm — proceed to Phase N`.

---

## Phase 0 — Prerequisites  ⛔ confirm before any code

### 0.1 Locked decisions

| # | Decision | Value |
|---|---|---|
| D1 | Auth | **Native email/password + backend JWT (access + rotating refresh).** Firebase only for Google/Apple sign-in + FCM. |
| D2 | Language | **TypeScript** |
| D3 | Package manager | npm |
| D4 | Node | 20 LTS (or 22 LTS) |
| D5 | Realtime | STOMP-over-WS (to match the Flutter `@stomp/stompjs` client) |
| D6 | **Facebook login** | **Deferred** (one-line toggle to enable later via Firebase Facebook provider through the same social-exchange endpoint). Flip if you want it now. |

### 0.2 Accounts / external setup

- **Firebase project** with **Google** + **Apple** providers enabled, **Cloud Messaging** on, and a
  **service-account JSON** (used for verifying Google/Apple ID tokens + sending FCM). *Not* used as
  the request-auth mechanism.
- **MongoDB** — local Docker for dev, Atlas for staging/prod.
- **JWT secrets** — strong random values for access + refresh signing (or an RSA keypair for RS256).
- *(Optional, only if enabling email verification / password reset)* an **email transport**
  (SMTP / SendGrid / Resend) and its credentials.

### 0.3 Local tooling

Node 20 LTS, npm, Docker + Docker Compose (local Mongo). Keep the Firebase service-account JSON and
all secrets **out of git** (paths/values in `.env`).

**➡️ Confirm prerequisites + the Facebook decision (D6), then `confirm` to start Phase 1.**

---

## Tech stack

| Concern | Choice |
|---|---|
| Runtime / framework | Node 20 LTS + Express 4.x (Express 5 optional) |
| Language | TypeScript |
| DB / ODM | MongoDB + Mongoose 8 |
| Validation | **Zod** — one schema per endpoint (body / params / query), enforced by middleware |
| Native auth | **jsonwebtoken** (access + refresh) + **argon2** (password hashing) |
| Social auth + push | **firebase-admin** — verify Google/Apple ID tokens, send FCM |
| Security | helmet, cors (allowlist), express-rate-limit, express-mongo-sanitize, hpp, compression, body-size limits |
| Logging | pino + pino-http (request IDs; no secrets) |
| Config | dotenv (or `node --env-file`) + typed config module |
| Realtime | `ws` + minimal STOMP frame layer |
| Tests | Vitest + Supertest |
| Tooling | ESLint + Prettier + tsx (dev) + tsc/tsup (build) |

---

## Target folder structure (feature-first + layered)

```
src/
  core/
    config/            # typed env loading & validation
    db/                # mongoose connection + graceful shutdown
    auth/              # jwt sign/verify, token TTLs, refresh rotation/reuse-detection
    firebase/          # firebase-admin init, verify social ID token, fcm send
    middleware/        # authRequired, authOptional, requireRole, requireOwnership, validate(zod), errorHandler, notFound, rateLimiters
    errors/            # AppError subclasses mapped to the spec's error codes
    http/              # response envelope helpers (ok, created, paginated, fail)
    logger/            # pino + pino-http
    security/          # helmet/cors/sanitize/hpp/compression assembly
  modules/
    auth/              # register, login, refresh, logout, social-exchange (+ optional verify/reset)
    users/             # /users/me
    devices/           # FCM push tokens
    categories/
    products/          # public read
    producers/         # public read
    reviews/
    favorites/
    cart/
    bookingPlaces/     # public read
    myLocations/       # business: own booking places
    partner/           # partner application
    shop/              # business: own producer profile + own products
    chat/              # threads + messages (REST)
    admin/             # products/producers/applications/bookingPlaces moderation
    realtime/          # STOMP-over-WS server
  app.ts
  server.ts
test/
.env.example
docker-compose.yml
Dockerfile
```

Each `modules/<feature>/`:
`feature.routes.ts → feature.controller.ts → feature.service.ts → feature.repository.ts`,
plus `feature.model.ts` (Mongoose), `feature.schema.ts` (Zod), `feature.mapper.ts`
(`_id → id`, ISO dates, strip internals).

---

## Phase 1 — Scaffolding & tooling  ⛔ gate

**Goal:** an empty-but-runnable, lint-clean TypeScript project.
**Build:** `package.json` scripts (`dev`, `build`, `start`, `lint`, `format`, `test`, `typecheck`);
tsconfig, ESLint + Prettier, `.nvmrc`, `engines`; `.gitignore` (env, service-account JSON,
`node_modules`, `dist`); `.env.example`; `docker-compose.yml` (Mongo); `Dockerfile`; a trivial
`GET /health` returning the envelope.
**Acceptance:** `npm run dev` boots; `/health` → `{ "data": { "status": "ok" }, "error": null }`;
`lint` + `typecheck` pass.

---

## Phase 2 — Core infrastructure  ⛔ gate

**Goal:** the shared spine.
**Build:**
- **Config:** load + validate env with Zod (`PORT`, `MONGO_URI`, `CORS_ORIGINS`,
  `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `ACCESS_TTL`, `REFRESH_TTL`,
  `FIREBASE_SERVICE_ACCOUNT_PATH`, `NODE_ENV`, rate-limit knobs). Fail fast if missing.
- **DB:** Mongoose connect with retry + graceful shutdown.
- **Envelope helpers:** `ok`, `created`, `paginated(items,total,page,size)`,
  `fail(code,message,details?)` — exact spec shapes.
- **Error layer:** `AppError` subclasses → spec codes (`BAD_REQUEST`, `VALIDATION_ERROR`,
  `UNAUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `UNPROCESSABLE`, `INTERNAL_ERROR`);
  central handler that **never leaks stack traces in prod**.
- **`validate(schema)` middleware:** Zod over `{ body, params, query }` → `422 VALIDATION_ERROR`
  with `details[] = { field, message }`.
- **Security assembly:** helmet, CORS allowlist, `express-mongo-sanitize`, `hpp`, compression,
  body-size limits, `trust proxy`.
- **Rate limiters:** global + a strict limiter reserved for auth endpoints.
- **Logger:** pino + pino-http with request IDs.

**Acceptance:** unknown route → `404` envelope; thrown `AppError` → correct status + envelope; bad
payload on a sample validated route → `422` with `details[]`.

---

## Phase 3 — Auth & authorization (native JWT + Firebase social)  ⛔ gate

**Goal:** the backend owns identity. After any login path the app uses **backend JWTs** for all
requests. The backend **never trusts a client-supplied id or role** — both come from the verified
access token (and role is re-checked from the DB on sensitive ops).

**Build — native email/password:**
- `POST /api/auth/register` — email + password; **argon2id** hash; create `User` (role `client`);
  return access + refresh tokens. `409` if email exists.
- `POST /api/auth/login` — verify with `argon2.verify`; issue tokens; **rate-limited + lockout**
  after N failures.
- `POST /api/auth/refresh` — validate the presented refresh token against its **hashed** copy in
  DB, **rotate** (issue new pair, invalidate old). **Reuse detection:** if a revoked token is
  replayed, revoke the entire session family and force re-login.
- `POST /api/auth/logout` — revoke the presented refresh token / session.
- *(Optional, needs email transport)* `verify-email`, `forgot-password`, `reset-password`.

**Build — Firebase social exchange:**
- `POST /api/auth/social` — body `{ idToken }` (Firebase ID token from Google/Apple sign-in on the
  client). Verify via `admin.auth().verifyIdToken()`; extract email/uid/provider; **find-or-create**
  the native `User` (link by email; store `firebaseUid`, `provider`); return backend access +
  refresh tokens — same shape as login. *(Facebook reuses this endpoint when D6 is enabled.)*

**Tokens:**
- **Access JWT:** short-lived (~15 min), HS256 (RS256 optional), claims `{ sub: userId, role }`.
- **Refresh token:** long-lived (~30 days), opaque random, stored **hashed** in a `RefreshToken`
  collection, rotating with reuse detection; returned in the body for the app's secure storage.

**Authorization middleware:**
- `authRequired` → verify **backend access token**, attach `req.user = { id, role }`.
- `authOptional` → hydrate `req.user` if a valid token is present (for "Optional auth" routes).
- `requireRole(...roles)`, `requireOwnership(loader)`.

**First vertical slice:** `GET /api/users/me` + `PUT /api/users/me` (proves
route→controller→service→repository→model→schema→mapper end-to-end).

**Acceptance:** register → login → call a protected route; expired access + valid refresh → new
pair; replaying an old refresh token → whole session revoked (`401`); a `client` hitting an admin
route → `403`; Google/Apple ID token via `/api/auth/social` → backend tokens for an existing or new
user.

---

## Phase 4 — Data models & mappers  ⛔ gate

**Goal:** all Mongoose schemas + mappers matching the spec's models and enums.
**Build:** `User` (now with `passwordHash?`, `firebaseUid?`, `provider`, `role`, `emailVerified`),
**`RefreshToken`** (`userId`, `tokenHash`, `sessionId`/family, `expiresAt`, `revokedAt`),
`Category`, `Product`, `Producer`, `BookingPlace`, `Review`, `Favorite`, `CartItem`, `ChatThread`,
`Message`, `PartnerApplication`, `ModerationNote`. Enforce all enums; indexes including **unique
`(userId, targetType, targetId)`** for favorites/reviews (powers the `409`s), text indexes for
search. Per-model mappers emit the exact API shape (`id`, ISO dates, no `_id`/`__v`/`passwordHash`).
Seed script for categories.
**Acceptance:** `npm run seed` populates categories; a unit test confirms each mapper matches the
spec JSON (and that `passwordHash` never serializes).

---

## Phase 5 — Public read endpoints  ⛔ gate

**Goal:** the unauthenticated / optional-auth read surface.
**Build:** `GET /api/categories`; `GET /api/products` + `/:id`; `GET /api/producers` + `/:id`;
`GET /api/reviews`; `GET /api/booking-places` + `/:id`. Search/filter params, pagination,
`approvedOnly` logic, and the visibility rule (non-approved items only to the owner or an admin via
`authOptional`). Full Zod validation on every query/param.
**Acceptance:** unauth list returns only approved items; owner sees own pending item; `size > 100`
is clamped/validated.

---

## Phase 6 — Authenticated user features  ⛔ gate

**Goal:** the logged-in consumer surface.
**Build:** `reviews POST` (rating 1–5, one-per-target → `409`); `favorites` GET/check/POST/DELETE
(`409` dup, `404` missing); `cart` GET/add/update/remove/clear (validate product approved+available,
embed product); `devices/push-token` register/unregister (FCM). Recompute rating/reviewCount on the
target after a new review.
**Acceptance:** duplicate review → `409`; cart add of unavailable product → `400`; favorite check
→ `{ favorited: bool }`.

---

## Phase 7 — Business features  ⛔ gate

**Goal:** producer/business self-service (role `business`/`admin`, ownership enforced).
**Build:** `my-locations` CRUD (create → `pending`, update → resets `pending`, ownership `403`);
`shop/producer` GET/PUT; `shop/products` GET/POST/PUT/DELETE (create → `pending`, significant edits
reset approval); `partner/application` GET + POST (`client` only, one active → `409`). New products
and places always start **PENDING**.
**Acceptance:** business editing another owner's product → `403`; new product persists `pending`;
second partner application → `409`.

---

## Phase 8 — Admin moderation  ⛔ gate

**Goal:** the full admin workflow (role `admin`).
**Build:** for products, producers, applications, booking-places — `pending` lists, get-by-id,
`approve` / `reject` (reason) / `request-info` (note), and section approvals
(`PATCH .../sections/:section`, `approve | requestChanges`). Application **approve** must:
set `status: approved`, **create a Producer** from the application, and **set `User.role = business`
in the DB** (native role — no Firebase claim involved), then send the FCM push
`{ data: { action: "REFRESH_TOKEN" } }` so the app calls `/api/auth/refresh` and gets an access
token carrying the new role. Producer `verify` → `verified: true`. Append `ModerationNote`s.
**Acceptance:** approving an application creates a Producer + flips `User.role` to `business`
(verified by a refreshed access token); section PATCH updates only that section.

---

## Phase 9 — Realtime chat  ⛔ gate (hardest)

**Goal:** REST chat + STOMP-over-WS, authed by the **backend access token**.
**Build:** REST `chat/threads` (list, get-or-create `200/201`), `threads/:id/messages` (GET
paginated newest-first, POST), `threads/:id/read` — participant-gated (`403`). WS `/ws`: verify the
backend access token on `CONNECT`; support `SUBSCRIBE /user/queue/thread/<id>` and
`/user/queue/threads`; handle `SEND /app/chat.send` → persist, push to participants, update thread +
unread counts. Minimal STOMP frame layer over `ws`.
**Acceptance:** two authed clients exchange a message live; non-participant subscribe/post → `403`;
unread count increments and clears on `read`.

---

## Phase 10 — Hardening, tests, docs, deploy  ⛔ gate

**Goal:** production readiness.
**Build:** Supertest integration tests for critical flows (register/login/refresh rotation +
reuse-revocation, validation `422`s, ownership `403`s, conflict `409`s, admin approval role-flip);
`npm audit` clean; tighten CORS/CSP/rate limits for prod; structured logging without PII; graceful
shutdown (http + ws + mongo); README with run/deploy + `.env` reference; multi-stage Dockerfile;
optional CI.
**Acceptance:** `npm test` green; prod build runs in Docker against Atlas; one endpoint per module
smoke-passes.

---

## Consolidated security checklist (applied across phases)

- Verify the **backend access token** on every protected route; never trust client-sent id/role.
- Passwords hashed with **argon2id**; never logged or returned.
- **Refresh tokens** stored hashed, short access TTL, rotation + **reuse detection** (revoke session
  family on replay); logout revokes.
- Brute-force protection: rate limiting + account lockout on `login`.
- Per-endpoint Zod validation → `422` with `details[]`.
- `express-mongo-sanitize` + Mongoose schemas block NoSQL/operator injection; no raw input in query
  keys.
- helmet headers, CORS allowlist (no `*` with credentials), `hpp`, body-size limits.
- Secrets only in env/secret manager (`JWT_*`, Firebase JSON); never committed; `.env.example` only.
- No stack traces/secrets in responses or logs; generic `500 INTERNAL_ERROR` to clients.
- `trust proxy` + HTTPS at the edge; enforce HTTPS in prod.
- Pin deps (lockfile), `npm audit` in CI, keep `jsonwebtoken`/`argon2`/`firebase-admin` current;
  plan for JWT secret rotation.

---

## How this connects to the Flutter app

**Unchanged (connects as-is):** the `{ data, error }` envelope, status/error codes, enums, field
names, pagination shape, and the `id` (not `_id`) convention — so your existing model
deserialization and data screens need **no changes**. Images still go client → Firebase Storage; the
backend only stores/returns URLs.

**Requires app-side auth wiring changes** (because auth moved from Firebase tokens to backend JWTs):
- Email/password login now calls `POST /api/auth/register` / `POST /api/auth/login` and stores the
  returned **backend** access + refresh tokens (secure storage).
- Google/Apple: still sign in via Firebase on the client, then **exchange** the Firebase ID token at
  `POST /api/auth/social` for backend tokens; use those thereafter.
- The HTTP interceptor sends `Authorization: Bearer <backendAccessToken>` and, on `401`, calls
  `POST /api/auth/refresh` (rotating refresh) before retrying.
- After a role change, the FCM `REFRESH_TOKEN` push tells the app to call `/api/auth/refresh` so its
  new access token carries the updated role.
