Grace
Architecture

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
ServiceTechRôleExposition
mcpNode/TS, @modelcontextprotocol/sdk, Streamable HTTPAdaptateur de protocole. Valide le bearer, traduit chaque outil en appel HTTP backend. Aucune logique métier.publique
backendNestJSCœur. Resolver, validation, CRUD config, auth (Better Auth), catalogue. Source unique de logique.interne / /api
frontendReact + Vite (nginx)Dashboard. Tape l'API backend.publique
postgresPostgreSQL 16Config, catalogue, tokens, révisions.interne
caddyCaddy 2Ingress + 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

SurfaceAuthPour
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 (pendingapproved/rejected, approvedsuspended) 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 :

CoucheContenuDépend de
domainmodèles, logique pure, ports (interfaces)rien d'externe
applicationuse cases ; n'accède à l'extérieur que par des portsdomain
infrastructureadaptateurs (Drizzle, Better Auth, modèle d'embeddings…) implémentant les portsdomain
interfacecontrôleurs HTTP, thinapplication

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 ProjectTalent existe. 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 depuis catalog/), contenu des talents lu depuis le volume monté (/catalog).
  • Les personas builtin restent en YAML (catalog/personas/*.yml) ; la table personas ne stocke que ceux créés/édités depuis le dashboard. trace_events alimente le journal d'activité (voir Tracing & activité) ; project_relevance_settings porte les réglages de pertinence sémantique.

Contraintes & choix POC

  • Streamable HTTP obligatoire (le stdio ne marche pas en distant) ; mcp stateless.
  • Dépendance runtime : mcp / backend down ⇒ 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é.

On this page