# Architecture — Backend

# Architecture — Backend

> _Dernière mise à jour : 2026-05-10_

## Couches

```
src/
├── routes/           # Contrats HTTP (Hono), validation Zod, auth
├── handlers/         # Logique métier pure (Phase 4 MVC)
├── services/         # Domaine RAG / Qdrant / TEI / Claude / cards / méta / OCR
├── repositories/     # Data access Drizzle — SEUL endroit qui importe drizzle-orm
├── middleware/       # auth.ts (sessions, roles), security.ts (CORS, headers)
├── lib/              # logger, schemas Zod partagés, utils, with-timeout
├── cron/             # ingest-scheduler, meta-sync, forum-sync
├── config.ts         # Validation Zod env vars (boot-time)
├── schema.ts         # Tables Drizzle (users, games, questions)
├── db.ts             # Connexion SQLite + migrations
├── types.ts          # Types globaux (SessionUser, AppEnv)
└── index.ts          # App Hono, montage routes, CORS, init
```

## Règles d'archi

1. **`routes/`** ne fait que parser/valider l'entrée (Zod), appeler un handler ou un service, renvoyer la réponse Hono.
2. **`handlers/`** ne connaît pas Hono. Reçoit des données validées + dépendances, retourne un `Result` discriminé. Pour les erreurs attendues : `type DeleteGameResult = { ok: true } | { ok: false; status: 404; error: string }`.
3. **`services/`** : logique métier, pas de DB directe — passe par les repos.
4. **`repositories/`** : SEUL endroit qui importe `db`, `drizzle-orm` ou les tables du schéma. Nommer les fonctions par intention métier (`getByBggId`, `setIngestStatus`).
5. **`config.ts`** : seul endroit qui peut lire `process.env.X`. Tous les autres fichiers font `import { config }`.
6. **`logger.ts`** : seul endroit où `console.*` est autorisé. Convention : préfixer le scope dans le message (`logger.info('[meta-sync] ...')`).
7. **Aucun fichier > 350 lignes**. Si un fichier enfle, le découper : `types.ts` pour les interfaces, un fichier par responsabilité, `index.ts` comme barrel.
8. **Imports services** : toujours via le barrel (`from '../services/rag/index.js'`), jamais un sous-module direct.

## Routes (vue d'ensemble)

| Préfixe | Module |
|---|---|
| `/api/auth/*` | `routes/auth.ts` — register, login, logout, change password, reset |
| `/api/games/*` | `routes/games.ts` — CRUD jeux, PDF, ingest, page-image |
| `/api/ask/*` | `routes/ask.ts` — RAG retrieve + stream + feedback |
| `/api/cards/*` | `routes/cards.ts` — autocomplete, image proxy |
| `/api/decks/parse` | `routes/decks.ts` — import deck FAB |
| `/api/bgg/*` | `routes/bgg.ts` — search, hot, expansions |
| `/api/lorcana-symbols/*` | `routes/lorcana-symbols.ts` |
| `/api/admin/*` | `routes/admin.ts` — health, users, feedback, sync cards |
| `/api/health` | `routes/health.ts` — pour healthcheck Docker |
| `/api/confirm-user/:token` | `routes/auth.ts` — lien email confirmation |

Tableau exhaustif des 34 endpoints : voir `architecture/api-rest.md`.

## Middleware

- **`auth.ts`** :
  - `requireAuth` : 401 si pas de session
  - `requireConfirmed` : 403 si role='pending'
  - `requireAdmin` : 403 si role≠'admin'
  - `requireCanAddGames` : 403 si user.canAddGames=false (admins bypass)
- **`security.ts`** : CORS whitelist (origines exactes + CIDR IPv4 LAN), headers sécurité de base. La majorité des headers (HSTS, CSP) sont gérés par NPM en amont.

## Services principaux

```
services/
├── qdrant.ts            # Client Qdrant (search, upsert, scroll, health)
├── tei.ts               # Client TEI bge-m3
├── reranker.ts          # Client TEI reranker
├── claude-ssh.ts        # SSH Claude Code avec streaming JSON
├── claude-local.ts      # Mode dev local (bypass SSH)
├── claude-quota.ts      # Détection ClaudeQuotaError + parse resetAt
├── validate-model.ts    # Regex stricte modèles Claude (anti-injection shell)
├── bm25.ts              # BM25 sparse retrieval (Qdrant natif)
├── email.ts             # SMTP (notif admin, password reset)
├── bgg.ts + bgg-forums.ts  # API BoardGameGeek + scrape forums Rules
├── hierarchy.ts         # LLM hiérarchie chapter/section
├── conflict-detect.ts   # Détecte conflits extension/base via similarité + LLM
├── contextual-cache.ts  # Cache JSON contextes générés
├── contextual-llm.ts    # LLM contextuel par chunk (Contextual Retrieval B)
├── cards-cache.ts       # Cache mémoire cartes + recherche par nom
├── cards-sync.ts        # Sync collections Qdrant vs sources locales
├── query-expand.ts      # Expansion query (HyDE, synonymes)
├── pdf-images.ts        # Rendu PDF→PNG via pdftoppm (300 DPI)
│
├── chunking/            # Pipeline chunking (chunker, contextual, pdf-extract, types)
├── ocr/                 # Phase 1 : tesseract auto (decideOCR, ocrPages, cache)
├── qdrant/              # Wrappers bas niveau (client, collections, points, payload)
├── ingest/              # Machine à états (queue, coordinator, stages)
├── rag/                 # Pipeline RAG (retrieve, answer, classify, decompose, deckbuilding)
├── cards/sources/       # Normalisations par TCG (magic, lorcana, fab, riftbound, tm, ark-nova) + registry
└── meta/                # Méta-game (17lands, mtggoldfish, mtgtop8, mobalytics, fabtcg, riftboundstats, ingest)
```

## Patterns importants

- **`withTimeout(promise, ms, label)`** : wrap toute promesse externe (HyDE, decompose, embeddings) pour ne jamais bloquer un boot ou une réponse RAG.
- **Pool SSH parallèle** (10 workers) : pattern standard pour Contextual Retrieval B et conflict detection. Chaque worker check `quotaError` avant de poll la queue.
- **EventPusher** : signature des stages d'ingestion `(game, ..., push: EventPusher) => Promise<...>`. Les stages ne connaissent rien de l'IngestJob.
- **Heartbeat SSE** : tout endpoint streamSSE qui peut rester silencieux >30s ouvre un `setInterval` qui émet `{ type: 'heartbeat' }` toutes les 8s.