Backend api/ — vue d'ensemble & arborescence
À quoi sert le backend api/
Section intitulée « À quoi sert le backend api/ »Le dossier api/ est le cœur de Titreo E-Pay (nom interne OrchPay). C’est le service Node/Fastify qui orchestre un paiement composé de plusieurs instruments : une ou plusieurs cartes cadeaux (via Titreo) plus un moyen de paiement classique (PSP : Stripe, Adyen…). Tout le reste de l’écosystème — SDK client, plugins CMS, console marchand — n’est qu’un client de l’API publique exposée ici.
Concrètement, le backend :
- expose une API REST (sessions de paiement, legs, manifest de bootstrap, webhooks PSP, administration marchand) ;
- maintient l’état d’une
PaymentSessionet de sesPaymentLegvia des machines à états strictes (voir Machines à états) ; - garantit la cohérence des opérations multi-instruments grâce à un pattern outbox traité par un worker séparé (voir Outbox & orchestration) ;
- chiffre les secrets PSP, signe les manifests, hash les mots de passe, et applique l’idempotence sur les opérations externes.
L’ensemble suit une architecture hexagonale : le métier ne connaît ni Fastify, ni Firebird, ni Stripe. Il dialogue uniquement avec des interfaces (« ports »), dont les implémentations concrètes (« adapters ») vivent en périphérie.
Arborescence annotée de api/src
Section intitulée « Arborescence annotée de api/src »api/src/├── domain/ couche 1 — métier pur, zéro import framework/infra│ ├── session/ PaymentSession, PaymentLeg + leurs machines à états│ ├── merchant/ règles marchand (branding…)│ ├── auth/ scopes & utilisateurs (logique d'autorisation pure)│ ├── bootstrap/ construction/validation du manifest SDK│ └── shared/ Money, Currency, Id, erreurs (DomainError…)│├── application/ couche 2 — use cases + ports (interfaces)│ ├── ports/ interfaces des dépendances externes (repos, registries…)│ ├── sessions/ create / add-gift-leg / set-psp-leg / submit / refund…│ ├── gift/ débit / remboursement / lookup solde carte cadeau│ ├── webhooks/ handle-psp-webhook, dispatch webhook marchand│ ├── workers/ process-outbox-entry, cleanup-expired-sessions│ ├── bootstrap/ generate-manifest│ ├── auth/ signup / login / logout / verify-email│ ├── merchants/ CRUD marchand + onboarding│ ├── merchant-users/ invitations & membres d'équipe│ ├── api-keys/ création / révocation / authentification clés API│ ├── psp-configs/ upsert / get / list / delete config PSP│ ├── branding/ get / update branding│ └── audit/ write / list journaux d'audit│├── infrastructure/ couche 3 — implémentations concrètes des ports│ ├── db/ Firebird : pool, repos, UnitOfWork, migrations SQL│ ├── psp/ adapters Stripe & Adyen, registry, validateurs de config│ ├── titreo/ adapter carte cadeau Titreo (réel)│ ├── gift/ adapter carte cadeau mock (dev/test)│ ├── gift-card/ registry de fournisseurs de cartes cadeaux│ ├── crypto/ AEAD, Ed25519, scrypt, hash clé API, tokens session│ ├── bootstrap/ registry des client-adapters (manifest)│ ├── email/ ConsoleEmailSender (dev)│ ├── webhooks/ HttpWebhookSender + url-guard (anti-SSRF)│ ├── workers/ OutboxPoller, cleanup-scheduler (mécanique de polling)│ ├── clock.ts SystemClock│ └── id-generator.ts UuidGenerator│├── http/ couche 4 — Fastify (adapter d'entrée, jetable)│ ├── server.ts point d'entrée : charge config + assemble le container│ ├── app.ts buildHttpApp() : registre plugins + routes│ ├── routes/ un fichier par groupe d'endpoints (sessions, auth, admin…)│ ├── plugins/ auth, error-handler, request-context, idempotency, audit…│ ├── schemas/ schémas Zod partagés (sessions)│ └── serializers.ts sérialisation domaine → JSON de sortie│├── workers/ couche 5 — points d'entrée des processus séparés│ ├── outbox.ts worker outbox (confirme gifts après capture PSP)│ └── cleanup.ts worker d'expiration des sessions en attente│├── config/ lecture + validation Zod des variables d'env│ └── index.ts loadConfig()│└── container.ts COMPOSITION ROOT — câble ports ↔ adapters ↔ use casesTableau par dossier
Section intitulée « Tableau par dossier »| Dossier | Couche | Rôle | Dépend de |
|---|---|---|---|
domain/ |
1 — Domain | Entités, machines à états, Money/Currency, erreurs métier. Pur. | lui-même + zod |
application/ |
2 — Application | Use cases (un cas d’usage = une classe) + ports/ (interfaces). |
domain/ + ports/ |
application/ports/ |
2 — Application | Contrats des dépendances externes (repos, registries, clock, signer…). | domain/ |
infrastructure/ |
3 — Infrastructure | Implémentations concrètes : Firebird, Stripe/Adyen, Titreo, crypto, webhooks. | domain/, application/ports, libs externes |
http/ |
4 — HTTP | Serveur Fastify, routes, plugins, validation Zod. Aucune logique métier. | application (use cases) + container |
workers/ |
5 — Workers | Entry points de processus détachés (outbox, cleanup). | application + infrastructure (composition) |
config/ |
transverse | loadConfig() lit process.env, valide via Zod. Seul endroit qui lit l’env. |
zod |
container.ts |
composition root | Instancie les adapters et injecte tout dans les use cases. | tout (sauf domain directement) |
Pour le détail de chaque couche, voir Couche Domain, Couche Application, L’orchestrateur, Couche Infrastructure, Couche HTTP, Workers et Base de données.
Les 5 couches et la règle de dépendance
Section intitulée « Les 5 couches et la règle de dépendance »Une seule règle gouverne l’architecture : les flèches de dépendance pointent vers l’intérieur. Le métier ne dépend de rien ; tout dépend (indirectement) du métier.
flowchart TD
subgraph "Périphérie (jetable)"
HTTP["http/ (Fastify, routes)"]
WORK["workers/ (processus séparés)"]
INFRA["infrastructure/ (Firebird, Stripe, Titreo, crypto)"]
end
subgraph "Cœur (stable)"
APP["application/ (use cases + ports)"]
DOM["domain/ (entités, états, Money)"]
end
HTTP --> APP
WORK --> APP
WORK --> INFRA
INFRA --> APP
APP --> DOM
INFRA -.implémente.-> APP
Lecture : http/ et workers/ appellent des use cases de application/. infrastructure/ implémente les ports de application/. application/ ne connaît que domain/ et ses propres interfaces. domain/ ne connaît personne.
Interdits absolus (vérifiés à la revue) :
domain/n’importe jamaisfastify,firebird,stripe,bullmq, etc. ;application/n’importe jamais une implémentation concrète d’infrastructure — uniquement lesports;http/ne contient jamais de logique métier (une route = parse + appel use case + sérialise).
Le détail conceptuel et les bénéfices (migration Fastify → NestJS sans réécriture du métier) sont dans Architecture hexagonale.
Le composition root : container.ts
Section intitulée « Le composition root : container.ts »container.ts est l’unique endroit où le métier abstrait rencontre le monde concret. Il ne contient aucune logique métier : il se contente d’instancier chaque use case en lui injectant ses dépendances (par constructeur, pas de singleton global, pas de conteneur DI magique).
Sa surface publique :
- le type
Container— la liste exhaustive de ce que le reste de l’app peut consommer : les ports déjà résolus (sessions,outbox,merchants,pspRegistry,giftCardRegistry,clock…) et un sous-objetuseCases(une cinquantaine de use cases :createSession,submitSession,refundSession,handlePspWebhook,processOutboxEntry,generateManifest,loginMerchant…) ; - le type
ContainerDeps— les adapters bruts à fournir en entrée (un par port +consoleBaseUrl) ; - la fonction
buildContainer(deps: ContainerDeps): Container— qui câble tout.
Chemin d’appel typique au démarrage du serveur HTTP (http/server.ts) :
// 1. lire + valider l'environnementconst config = loadConfig()// 2. instancier les adapters concrets (infrastructure)const db = await FirebirdDb.connect(config.firebird)const aead = new AesGcmAead(loadKekFromBase64(config.pspConfigKek))// 3. câbler tous les use cases via le composition rootconst container = buildContainer({ clock: new SystemClock(), ids: new UuidGenerator(), sessions: new FirebirdSessionRepo(db, aead), pspRegistry: new ConfigBackedPspRegistry(factories, pspConfigs), giftCardRegistry, // Titreo réel si config.titreo, sinon MockGiftCardAdapter /* …une vingtaine d'autres ports… */})// 4. monter Fastify par-dessus le containerconst app = await buildHttpApp({ container, /* secrets, CORS, rate-limit… */ })await app.listen({ port: config.port, host: config.host })Le worker outbox (workers/outbox.ts) suit le même schéma de câblage, puis branche un OutboxPoller sur container.useCases.processOutboxEntry au lieu de monter un serveur HTTP.
Écarts entre ARCHITECTURE.md et le code réel
Section intitulée « Écarts entre ARCHITECTURE.md et le code réel »Le document de cadrage ARCHITECTURE.md (à la racine app/, pas dans api/) décrit une cible qui a divergé de l’implémentation. Points à connaître pour ne pas chercher des fichiers inexistants :
Où aller ensuite
Section intitulée « Où aller ensuite »| Pour comprendre… | Page |
|---|---|
| Le métier pur (entités, Money, erreurs) | Couche Domain |
| Les use cases et les ports | Couche Application |
| Le séquencement paiement + outbox | L’orchestrateur |
| Les adapters concrets (Firebird, PSP, Titreo, crypto) | Couche Infrastructure |
| Le serveur Fastify, routes & plugins | Couche HTTP |
| Les processus détachés (outbox, cleanup) | Workers |
| Le schéma SQL et les migrations | Base de données |
| Le trajet complet d’une requête | Parcours d’appel |
Côté concepts transverses : Machines à états, Outbox & orchestration, Idempotence, Contrats, Sécurité.