Architecture du backend CRM
Phase 22.0–48. Statut : DÉPLOYÉ (VPS en ligne, 68+ endpoints, architecture modulaire). Dernière mise à jour : 2026-04-28 (Phase 48 — Décomposition architecture)
Vue d'ensemble
Navigateur (:18888)
│
├── Fichiers statiques ──► Nginx ──► Docker frontend (React + Nginx)
│
├── /api/crm/* ────► Nginx ──► Bun Master Bot (:19210)
│ ├── crmAuthMiddleware (HMAC-SHA256)
│ └── shared/routes/router.ts → 17 modules domaine
│
├── /api/auth/* ───► Nginx ──► Bun (:19210) → master-bot/routes/auth.ts
├── /api/cli/* ───► Nginx ──► Bun (:19210) → master-bot/routes/cli.ts
├── /api/internal/* ► Bun (:19210, loopback uniquement) → master-bot/routes/internal.ts
│
├── /api/sse/* ────► Nginx (non bufférisé) ──► Bun (:19210)
│
└── /ws/terminal/* ► Nginx (upgrade) ──► Bun (:19210) → master-bot/routes/websocket.ts
Structure des modules (Phase 48)
Serveur API (master-bot/api-server.ts, 196 lignes) — dispatcher léger :
master-bot/routes/auth.ts(562) — OAuth, login, register, 2FA, device codemaster-bot/routes/internal.ts(282) — chat/save, bridge-event, relay, téléchargements binairesmaster-bot/routes/cli.ts(293) — ARC CLI + intégration MCPmaster-bot/routes/websocket.ts(303) — terminal, event-stream, local-bridge
Routes CRM (shared/routes/router.ts, 512 lignes) — table de dispatch pour 17 modules domaine :
projects.ts(723),workers.ts(636),skills.ts(665),chat.ts(862)sage.ts(757),onboarding.ts(601),files.ts(381),wiki.ts(258)system.ts,pins.ts,learnings.ts,docs.ts,specs.ts,restart.ts,analytics.ts_utils.ts(343) — types partagés, sécurité des chemins, helpers auth
Flux d'authentification
Utilisateur ──► LoginOverlay (frontend)
│
├── Email/mot de passe → POST /api/auth/login
└── OAuth → /api/auth/{google,github} → callback
│
▼
Auth réussie → token HMAC-SHA256
│
├── payload = { sub: userId, iat, exp: +24h }
├── signature = HMAC-SHA256(base64url(payload), CRM_SECRET)
└── token = base64url(payload) + "." + signature
│
▼
localStorage('crm-token') → Tous les appels API : Authorization: Bearer <token>
│
▼
Token expiré → 401 → reconnexion requise
Fichiers clés
| Fichier | Rôle |
|---|---|
shared/auth.ts |
Génération/vérification de token, email/OAuth, réinitialisation mot de passe, vérification email, CORS, middleware |
shared/vault.ts |
Stockage de secrets AES-256-GCM (CRM_SECRET auto-créé) |
frontend/src/crm/components/LoginOverlay.jsx |
UI de connexion (email, OAuth, réinitialisation mot de passe, vérification email) |
Propriétés de sécurité
- CRM_SECRET : 32 octets hex aléatoire, chiffré par vault (AES-256-GCM)
- TTL du token : 24 heures, non renouvelable
- Signature : HMAC-SHA256, encodage base64url (sans padding)
- CORS : echo d'origine, limité aux headers Authorization + Content-Type
- Vérification email requise avant connexion pour les nouveaux comptes
- L'authentification basique nginx reste comme couche de défense en profondeur externe
Routes API CRM
Toutes les routes sous /api/crm/ requièrent Authorization: Bearer <token>.
GET /api/crm/projects
Liste tous les projets enregistrés avec statut de santé en direct. Inclut master comme pseudo-projet (DEBT-6).
| Champ | Source |
|---|---|
name, type, bot_username |
config/bot_registry.json (master : entrée virtuelle, type: "system") |
healthy |
Vérification santé HTTP (enfants : port child, master : /api/master/health) |
tmux_alive |
tmux has-session -t <name> (master : citadel-master) |
status |
Fichier heartbeat ou dérivé de health+tmux (master : healthy/degraded/down) |
Master est toujours ajouté en première entrée dans le tableau de réponse.
GET /api/crm/projects/:name
Fiche détaillée complète du projet.
| Champ | Source |
|---|---|
manifest |
Texte brut depuis <cwd>/MANIFEST.md |
heartbeat |
JSON depuis state/heartbeat_<name>.json |
skills[] |
Noms de fichiers <cwd>/skills/*.md (excluant _*.md) |
healthy, tmux_alive |
Même que l'endpoint liste |
GET /api/crm/projects/:name/logs
Tail des logs JSONL structurés.
| Paramètre | Défaut | Max |
|---|---|---|
lines |
100 | 500 |
category |
dialog |
system, dialog, error |
Source : /var/log/citadel/<name>/<category>-YYYY-MM-DD.log
Lit les fichiers de log les plus récents en premier, parse le JSONL, retourne en ordre chronologique.
GET /api/crm/projects/:name/files
Listing de répertoire avec protection contre le path traversal.
| Paramètre | Défaut | Description |
|---|---|---|
path |
/ |
Chemin relatif dans le cwd du projet |
Sécurité : resolve(base, requested) doit startsWith(resolve(base)). Violation → 403.
Entrées de réponse : { name, type: "file"|"directory", size?, modified }, triées répertoires en premier.
GET /api/crm/projects/:name/skills
Skills installés avec attribution de source.
| Source | Détection |
|---|---|
file |
<cwd>/skills/*.md (excluant _*.md) |
manifest |
MANIFEST.md → skills[] + library_skills[] (parse JSON) |
Dédupliqué : la source fichier a priorité sur le manifest.
POST /api/crm/projects/:name/restart (Phase 22.3)
Redémarre un child bot via le watchdog.
| Aspect | Détail |
|---|---|
| Méthode | POST uniquement (GET retourne 405) |
| Auth | Token HMAC-SHA256 (même que les autres routes CRM) |
| Cooldown | 30 secondes par projet (retourne 429 si trop fréquent) |
| Mécanisme | Appelle restartChild() depuis watchdog.ts |
| Réponse | { ok: true } ou { error: "..." } |
GET /api/crm/projects/:name/metrics (Phase 22.1)
Séries temporelles de métriques qualité pour les graphiques sparkline.
| Paramètre | Défaut | Description |
|---|---|---|
days |
7 | Nombre de jours à agréger |
Source : /var/log/citadel/<name>/quality-events.log → décomptes succès/échecs par jour.
WS /ws/terminal/:name (Phase 22.3)
Streaming de contenu de pane tmux via WebSocket.
| Aspect | Détail |
|---|---|
| Auth | Paramètre query ?token= (vérifié via verifyToken().valid) |
| Mode | Lecture seule par défaut ; ?mode=interactive active l'envoi de touches |
| Polling | tmux capture-pane -p -e -t {session} -S -80 toutes les 200ms |
| Delta | Envoie uniquement si le contenu change par rapport au précédent |
Sécurité : Protection contre le path traversal (Phase 22.3, DEBT-7)
Tous les endpoints valident les noms de projet avant traitement :
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
export function isValidProjectName(name: string): boolean {
return name.length > 0 && name.length <= 64 && SAFE_NAME_RE.test(name);
}
Appliqué à 3 points d'entrée : routeCrmRequest(), routeSseRequest(), handler d'upgrade WebSocket dans bot.ts.
Noms invalides → 403 Forbidden.
Intégrité des données : Écritures atomiques (Phase 22.3, DEBT-2)
Les écritures de registry dans onboarding.ts utilisent le pattern atomique :
async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
const tmp = `${filePath}.tmp.${process.pid}`;
await Bun.write(tmp, JSON.stringify(data, null, 2));
Bun.spawn(["mv", tmp, filePath]); // renommage atomique POSIX
}
Fichiers clés
| Fichier | Rôle | Lignes |
|---|---|---|
shared/crm-routes.ts |
55+ handlers + routeur routeCrmRequest() + isValidProjectName() |
~4800 |
shared/sage.ts |
Sage Worker : runSageAnalysis(), runBenchmark(), juge LLM, tests A/B |
~550 |
shared/db.ts |
SQLite SSOT : userQueries, projectQueries, skillQueries, benchmarkQueries, chatQueries | ~600 |
shared/auth.ts |
Génération token, vérification, CORS, middleware | ~100 |
master-bot/bot.ts |
Bun.serve : routeur HTTP + handler terminal WebSocket | ~1160 |
master-bot/onboarding.ts |
atomicWriteJson() pour les écritures de registry |
~850 |
Proxy Nginx
Fichier : infra/nginx/citadel-crm.conf — remplace la config legacy citadel-os.
Trois upstreams :
| Upstream | Port | Service |
|---|---|---|
crm_backend |
19210 | Bun Master Bot (CRM API, santé) |
phaser_frontend |
18889 | Docker Phaser office (legacy) |
mcp_bridge |
19200 | Docker MCP state-bridge (legacy) |
| Location | Backend | Auth | Config spéciale |
|---|---|---|---|
/ |
phaser_frontend |
auth basique | Docker Phaser frontend |
/api/crm/ |
crm_backend |
token (sans basique) | Timeout 30s, keepalive |
/api/master/health |
crm_backend |
aucune | Vérification santé publique |
/api/sse/ |
crm_backend |
token (sans basique) | proxy_buffering off, timeout 1h (Phase 22.1) |
/ws/ |
crm_backend |
token (sans basique) | Upgrade WebSocket, timeout 1h (Phase 22.1) |
/api/ |
mcp_bridge |
aucune (sans basique) | API d'état legacy + SSE |
/health |
mcp_bridge |
aucune | Santé bridge legacy |
Tous les emplacements transmettent X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
Chemins bloqués : /.* (dotfiles), /config/, /state/, /scripts/.
Note : La priorité des locations nginx assure que /api/crm/ et /api/sse/ correspondent avant le catch-all /api/ (MCP legacy).
Intégration dans le Master Bot
master-bot/bot.ts Bun.serve (:19210) :
async fetch(req) {
/api/master/health → public, sans auth
/api/crm/* → preflight CORS → crmAuthMiddleware → routeCrmRequest()
* → 404
}
Le routeur dispatche vers les fonctions handler de shared/crm-routes.ts. Les headers CORS sont injectés sur chaque réponse CRM (y compris les erreurs 401).
Résumé des endpoints (toutes phases)
| Phase | Fonctionnalité | Endpoint | Statut |
|---|---|---|---|
| 22.0 | Liste projets | GET /api/crm/projects |
DONE |
| 22.0 | Détail projet | GET /api/crm/projects/:name |
DONE |
| 22.0 | Tail de logs | GET /api/crm/projects/:name/logs |
DONE |
| 22.0 | Listing fichiers | GET /api/crm/projects/:name/files |
DONE |
| 22.0 | Listing skills | GET /api/crm/projects/:name/skills |
DONE |
| 22.1 | Streaming SSE logs | GET /api/sse/logs/:name |
DONE |
| 22.1 | Historique métriques | GET /api/crm/projects/:name/metrics |
DONE |
| 22.3 | Redémarrage projet | POST /api/crm/projects/:name/restart |
DONE |
| 22.3 | Terminal WebSocket | WS /ws/terminal/:name |
DONE |
| 24.5 | CRUD specs | GET/POST /api/crm/projects/:name/specs |
DONE |
| 24.5 | Rôle actif | GET/POST /api/crm/projects/:name/active-role |
DONE |
| 25 | Bundle skills | GET /api/crm/projects/:name/skills-bundle |
DONE |
| 25 | Sync learnings | GET/POST /api/crm/projects/:name/learnings |
DONE |
| 26 | CRUD workers | GET/POST /api/crm/projects/:name/workers |
DONE |
| 32 | Sauvegarde wiki | PUT /api/crm/projects/:name/wiki/save |
DONE |
| 32 | Sauvegarde/suppression skills | PUT/DELETE /api/crm/projects/:name/skills/* |
DONE |
| 33 | Paramètres compte | GET/PUT /api/crm/account/settings |
DONE |
| 33 | Créer projet | POST /api/crm/projects/create |
DONE |
| 34 | CRUD tickets | POST/GET/PUT /api/mcp/issues/:project |
DONE |
| 34 | Wiki MCP | PUT /api/mcp/wiki/:project |
DONE |
| 34 | Sync roadmap | GET/PUT /api/mcp/roadmap/:project |
DONE |
| 34 | Init CLI | GET /api/cli/init/:project/:mode |
DONE |
| 35 | Ingestion log terminal | POST /api/crm/projects/:name/terminal/log |
DONE |
| 36 | Chat Cloud PM | POST /api/crm/projects/:name/chat |
DONE |
| 36.6 | Génération skill | POST /api/crm/projects/:name/skills/generate |
DONE |
| 36.7 | Listing notebooks | GET /api/crm/projects/:name/notebooks |
DONE |
| 40.14 | Roadmap (contenu) | GET /api/mcp/roadmap/:project (+ champ content) | DONE |
Total : 46+ endpoints (REST + SSE + WebSocket + CLI/MCP)
NotebookLM Bridge (Phase 36.3)
Service Python FastAPI séparé sur :19213 (localhost uniquement). Pas partie de l'API CRM Bun.
| Endpoint | Méthode | Objectif |
|---|---|---|
/query |
POST | Recherche sémantique via Google NotebookLM |
/sync |
POST | Mettre en file une source pour sync async (fire-and-forget) |
/notebooks/init |
POST | Créer un notebook pour un nouveau projet |
/health |
GET | Statut auth, nombre de notebooks, stats de file d'attente |
Le CRM Bun déclenche des appels fire-and-forget vers le bridge lors de :
handleUpdateIssue(fermeture/ouverture de ticket) →/synchandleMcpWikiUpdate(sauvegarde wiki) →/synchandleCreateProject/handleOnboardingSetup→/notebooks/initexecuteAskNotebooklm→/query(timeout 15s, fallback local)
Dette technique
Voir docs/backlog/technical-debt.md pour le registre complet. Éléments clés :
| ID | Titre | Statut |
|---|---|---|
| DEBT-2 | Écritures JSON atomiques | RÉSOLU (Phase 22.3) |
| DEBT-3 | Token Docker intégré → auth session | RÉSOLU (Phase 31) |
| DEBT-6 | Angle mort CRM master bot | RÉSOLU (Phase 22.3) |
| DEBT-7 | Défense contre path traversal | RÉSOLU (Phase 22.3) |
Maintenu par Rick (Orchestrateur). Mis à jour après chaque phase terminée.