Backend hexagonal
Services, schémas d'auth, modèle de données et découpage hexagonal du backend.
La plateforme déploie 4 conteneurs + un reverse-proxy, orchestrés par docker-compose.
flowchart TB
subgraph local["Repo client (poste dev)"]
cfg[".mcp.json<br/>url + Bearer token projet"]
end
nav["Navigateur"]
subgraph vps["VPS · docker-compose"]
caddy["caddy (ingress, TLS auto)"]
mcp["mcp · Node/TS<br/>adaptateur MCP HTTP"]
backend["backend · NestJS<br/>resolver · validate · CRUD · auth · catalogue"]
frontend["frontend · React/Vite (nginx)"]
pg[("postgres")]
end
cfg -->|MCP Streamable HTTP| caddy
nav -->|HTTPS| caddy
caddy --> mcp
caddy --> frontend
caddy -->|/api| backend
mcp -->|HTTP interne| backend
frontend -->|/api| backend
backend --> pg
| Service | Tech | Rôle | Exposition |
|---|---|---|---|
mcp | Node/TS, @modelcontextprotocol/sdk, Streamable HTTP | Adaptateur de protocole. Valide le bearer, traduit chaque outil en appel HTTP backend. Aucune logique métier. | publique |
backend | NestJS | Cœur. Resolver, validation, CRUD config, auth (Better Auth), catalogue. Source unique de logique. | interne / /api |
frontend | React + Vite (nginx) | Dashboard. Tape l'API backend. | publique |
postgres | PostgreSQL 16 | Config, catalogue, tokens, révisions. | interne |
caddy | Caddy 2 | Ingress + TLS automatique. | 80/443 |
Le conteneur mcp ne contient aucune logique : mcp et frontend consomment les mêmes
endpoints backend → cohérence par construction, zéro duplication.
Le point dur : serveur MCP distant
Un serveur MCP distant n'a aucun accès au filesystem local. Donc :
- la sélection « quels talents pour ce projet » vit en base, pas dans un fichier local ;
- le repo client ne porte qu'un token projet (dans
.mcp.json) qui sert d'ancre d'identité + auth :token → projet → organisation (si approuvée) → talents.
Deux schémas d'authentification
| Surface | Auth | Pour |
|---|---|---|
MCP (conteneur mcp) | Token projet (bearer, hashé en base) | identifie le projet ; réservé au .mcp.json |
| Web (dashboard) | Session Better Auth (cookie httpOnly révocable) | identifie l'utilisateur et son organisation active |
L'authentification humaine est déléguée à Better Auth 1.6.23, monté sur /api/auth/* :
e-mail/mot de passe, Google OAuth, et SSO OIDC/SAML par organisation (un fournisseur
d'identité dédié par tenant, routage par domaine e-mail). Plugins Better Auth : organization
(multitenant + cycle d'approbation Grace), admin (rôle plateforme), multiSession (jusqu'à 10),
sso. L'API web est isolée sous /web/* (gardée par la session, scopée par l'organisation
active) ; les endpoints MCP sont à la racine (gardés par le token projet).
Multitenant : projets, membres, talents et tokens appartiennent à une organisation, plus à
un utilisateur. Chaque organisation porte un statut (pending → approved/rejected, approved
⇄ suspended) et seule une organisation approved ouvre l'accès MCP — sinon 403 +
audit mcp.access_denied.
Découpage hexagonal
Chaque contexte du backend suit une découpe stricte en quatre couches :
| Couche | Contenu | Dépend de |
|---|---|---|
domain | modèles, logique pure, ports (interfaces) | rien d'externe |
application | use cases ; n'accède à l'extérieur que par des ports | domain |
infrastructure | adaptateurs (Drizzle, Better Auth, modèle d'embeddings…) implémentant les ports | domain |
interface | contrôleurs HTTP, thin | application |
La composition se fait à la racine (Nest modules, useFactory). Le domaine ne dépend de
rien ; les adaptateurs implémentent les ports ; le use case orchestre. C'est exactement la
règle que les talents enforceable valident en CI — voir Validation.
Modèle de données
Tables détenues par Better Auth (identité & multitenant) :
user id, name, email, email_verified, image, role(admin|user), banned, ... (remplace l'ancienne table users)
session id, user_id, token, expires_at, active_organization_id, ... (sessions révocables, contexte d'org par session)
account id, user_id, provider_id(google|credential|sso-…), account_id, password (identités fédérées + credentials)
organization id, name, slug, status(pending|approved|rejected|suspended), status_* (le tenant + cycle d'approbation Grace)
member id, user_id, organization_id, role(owner|admin|member) (appartenance)
invitation id, email, organization_id, role, status, expires_at (invitations, 72 h)
sso_provider id, provider_id, issuer, domain, oidc_config|saml_config, organization_id (IdP par organisation)
verification id, identifier, value, expires_at (state OAuth, vérif e-mail, anti-replay SAML)Tables métier (Drizzle) :
Project id, organization_id, name, description, slug, local_path_hint, created_at (rattaché à l'ORGANISATION)
ProjectToken id, project_id, hash, label, last_used_at, revoked_at (le bearer du .mcp.json)
CatalogTalent id, talent_id, version, kind, enforceable, name, summary, content_path, deps[], manifest_json
ProjectTalent id, project_id, talent_id, enabled, pinned_version, updated_at, updated_by (config web↔MCP)
ConfigRevision id, project_id, snapshot_json, source(web|mcp), created_at (last-write-wins)
Persona id, persona_id, name, description, required[], recommended[], created_at, updated_at (personas custom)
TraceEvent id, project_id, type, actor(web|mcp), route, status, duration_ms, payload_json, created_at (journal d'activité produit)
SecurityEvent id, type, actor_user_id, organization_id, project_id, payload_json, created_at (audit sécurité)
ProjectRelevanceSettings project_id, floor, beta, margin, budget, updated_at (pertinence sémantique par projet)Source de vérité : platform/backend/src/db/schema/*.ts (Drizzle) + migrations drizzle-kit
(platform/backend/src/db/migrations/).
- Installé = une ligne
ProjectTalentexiste. Actif =enabled = true. - Toute écriture (web ou MCP) crée une
ConfigRevision→ trace + last-write-wins. - Le catalogue : métadonnées en base (
catalog_talents, seedées depuiscatalog/), contenu des talents lu depuis le volume monté (/catalog). - Les personas builtin restent en YAML (
catalog/personas/*.yml) ; la tablepersonasne stocke que ceux créés/édités depuis le dashboard.trace_eventsalimente le journal d'activité (voir Tracing & activité) ;project_relevance_settingsporte les réglages de pertinence sémantique.
Contraintes & choix POC
- Streamable HTTP obligatoire (le stdio ne marche pas en distant) ;
mcpstateless. - Dépendance runtime :
mcp/backenddown ⇒ plus de talents (le prix du MCP vs fichiers). - Multitenant par organisation (isolation stricte, accès MCP conditionné à l'approbation) ; auth Better Auth ; ORM Drizzle ; catalogue lu d'un volume monté.