Code partagé¶
Les plateformes de l'écosystème sont différentes à l'extérieur, uniques à l'intérieur. Pour l'utilisateur, chacune a sa marque, son domaine et ses services — pourtant plus de 90 % du code provient d'une seule source.
Cette page explique comment ce code partagé est distribué.
Pourquoi cela est devenu nécessaire¶
Au début, chaque plateforme était copiée depuis le modèle. Résultat : une seule correction devait être répétée à la main dans 8 dépôts ; en oublier un, et cette plateforme prenait du retard sans que personne ne le voie.
La mesure a montré que ~95 % du frontend était réellement partagé ; la divergence réelle représentait environ 40 lignes portant le nom de la marque. Autrement dit, la duplication n'était pas une exigence technique mais un héritage de la copie.
Trois mécanismes¶
Le code partagé circule différemment selon la couche. Aucun de ces mécanismes n'est un « copier-coller » : tous sont versionnés et réversibles.
| Couche | Forme | Mécanisme |
|---|---|---|
| Noyau backend | module Go | dépendance go.mod |
| Couche frontend | paquet npm | dépendance package.json |
| Squelette de plateforme | historique git | git merge + autosync quotidien |
1. Noyau backend — un module Go¶
Authentification, contrôle d'accès par rôles (RBAC), passerelle API, audit,
chaîne d'IA, intégration eID/SSO : tout cela vit dans un seul module Go. Le
main.go d'une plateforme fait généralement une trentaine de lignes : démarrer
le noyau, puis ajouter les routes propres à cette plateforme.
Le noyau comporte une seule couche directe :
open-gerege-core— la base ouverte consommée directement par tous les backends des lignes gouvernementale et Gerege.
Jusqu'au 2026-08-02, private-gerege-core se trouvait entre les deux. Comme il
ne contenait ni logique supplémentaire ni migration, il a été retiré de la
chaîne et archivé. La logique commerciale reste dans chaque dépôt produit.
2. Couche frontend — @gerege/ui-core¶
Le même problème résolu par le noyau côté backend, résolu à nouveau côté frontend. Le paquet contient :
lib/**— client d'API, utilitaires BFF, dictionnaire i18n, thème, session,components/**— coquille, administration, espace utilisateur, eID, passerelle,api/**— la logique de 158 routes BFF.
Le paquet est publié sous forme de code source TypeScript (non compilé) ;
l'application le compile donc via transpilePackages de Next.js. La
distribution se fait par tarball HTTPS ouvert : aucune authentification
requise, fonctionne y compris dans une construction Docker.
Pourquoi les routes BFF conservent une enveloppe
Next.js enregistre les routes via le système de fichiers ; chaque application conserve donc une réexportation d'une ligne par chemin :
// src/app/api/org/[id]/route.ts
export { GET, PUT, DELETE } from '@gerege/ui-core/api/org/[id]';
export const dynamic = 'force-dynamic';
Les 158 fichiers pourraient se réduire à un unique [...path], mais cela
détruirait une liste d'autorisation de sécurité : la liste des routes
définit quels chemins du backend le navigateur peut atteindre. L'enveloppe
est un prix délibéré.
3. Squelette de plateforme — héritage git¶
Ce qui n'appartient pas au paquet (structure des pages, globals.css,
configuration de déploiement) est hérité du modèle par git merge. Un
autosync quotidien récupère les changements du modèle amont et ouvre une pull
request dans les dépôts applicatifs : tout changement atteignant la
production passe par une revue humaine.
Les fichiers qui doivent rester propres à chaque plateforme (marque,
déploiement, CI, documentation) sont protégés par merge=ours dans
.gitattributes.
merge=ours ne protège pas des changements unilatéraux
Ce pilote ne résout que les conflits. Si le modèle amont supprime un fichier, la fusion le suit — le pilote n'est jamais invoqué. La vraie protection consiste à ce que chaque fichier de marque ou de configuration ait un contenu différent des deux côtés.
Ce qui reste la propriété de la plateforme¶
| Dans le paquet / le noyau | Propriété de la plateforme |
|---|---|
lib/**, components/**, logique BFF |
brand.config.ts — nom, domaine, couleurs, URL de documentation |
| Authentification, RBAC, passerelle, audit | components/landing/** — texte marketing |
| Intégration eID / SSO | app/**/page.tsx — enregistrement des routes (enveloppes fines) |
| Dictionnaire i18n partagé (846 clés × 7 langues) | lib/<platform>I18n.ts — la terminologie de la plateforme |
Structure du menu (AppShell) |
nav.config.ts — les sections que la plateforme dessert |
app/globals.css — jetons de couleur de marque |
|
deploy/**, .github/** — déploiement, CI |
Pourquoi la terminologie de plateforme reste dans l'application¶
La règle : le dictionnaire partagé ne connaît que la surface partagée. Les mots propres à une seule plateforme — le vocabulaire IBAN/relevés du portefeuille, le catalogue d'API du portail développeur, la terminologie des processus métier de Ring — résident dans le dictionnaire propre à l'application.
La raison en est le coût : placer les 1,104 termes de Ring dans le dictionnaire partagé, c'est les faire porter par kiosk, POS et le portefeuille — et chaque langue ajoutée multiplie ce coût par sept.
La mise en œuvre suit le même motif dans chaque dépôt :
// lib/walletI18n.ts — les 15 termes du portefeuille × 4 langues
export function useWalletT() { … } // repli sur l'anglais pour les langues non traduites
Lorsqu'un composant transmet T en prop à ses composants enfants, le
scinder en deux fonctions (T + wt) reviendrait à scinder chaque prop. Dans
ce cas, on écrit un unique résolveur : si la clé appartient à la plateforme, on
la prend dans son dictionnaire, sinon dans le paquet (lib/lang.ts de
ring-dgov).
Navigation — structure partagée, service propre à la plateforme¶
AppShell a la même organisation sur toutes les plateformes (Super admin ·
Admin · Manager · Citoyen), mais chaque plateforme n'en implémente qu'un
sous-ensemble : le portefeuille n'a ni gateway, ni relay, ni modules de
registre.
| Réglage | Rôle |
|---|---|
navRoutes |
Les routes que l'application dessert réellement ; le menu est filtré par elles |
navSystemLabels |
Le nom des systèmes dans le rail (me → « Portefeuille ») |
navExtra |
Les entrées de menu propres à cette seule plateforme (les 21 entrées BPM de Ring) |
navExtra provient d'un composant CLIENT
UiCoreProvider est un composant client appelé depuis le root layout
serveur. Les icônes de menu (composants React) et les fonctions de libellé
ne franchissent pas la frontière server→client. L'application crée donc
une fine enveloppe client et les transmet depuis celle-ci :
// src/nav.config.tsx
'use client';
export default function AppNav({ children }) {
return <UiCoreProvider navExtra={NAV_EXTRA}>{children}</UiCoreProvider>;
}
Une entrée de menu que peu de plateformes desservent peut être marquée
optIn: true dans le paquet : elle n'apparaît alors que sur les
plateformes qui l'ont explicitement déclarée dans navRoutes.
Trois garde-fous automatiques¶
Le code partagé crée trois types de dépendance différents. Chacun se manifeste autrement lorsqu'il casse ; les garde-fous sont donc trois eux aussi :
| Dépendance | Garde-fou | Ce qui se passe en cas de casse |
|---|---|---|
| Code du paquet ← code de l'application | tsc |
La compilation échoue — visible immédiatement |
| Route du paquet ← enveloppe BFF de l'application | check-routes |
L'endpoint disparaît silencieusement |
| Classe du paquet ← CSS de l'application | check-styles |
L'écran perd silencieusement son style |
-
check-brand— la construction échoue si le nom d'une plateforme apparaît dans le code hors debrand.config.ts. Le nom de la plateforme est lu depuisbrand.config.ts, la liste ne se périme donc pas à la main.Ce que le garde-fou a attrapé
La page de connexion proposait de s'authentifier via « Gerege SSO (sso.gerege.mn) » alors que les plateformes de la ligne gouvernementale redirigent en réalité vers
sso.dgov.mn. L'hôte est désormais lu depuis leSSO_ISSUERdu backend. -
check-routes— exige une enveloppe applicative pour chaque route du paquet. Sans elle, un nouvel endpoint du paquet disparaîtrait silencieusement sur cette plateforme (la logique n'étant pas visible dans l'application, rien ne semblerait cassé).Le garde-fou ne prouve pas que l'EXCLUDE est juste
Une route délibérément non exposée s'inscrit dans
EXCLUDE— l'écart devient explicite. Mais le garde-fou n'attrape pas une entrée écrite à tort. C'est ainsi quepublic/languagess'est retrouvé exclu sur une plateforme et que le sélecteur de langue est resté vide : tous les garde-fous au vert, l'écran cassé. -
check-styles— le paquet ne contient pas de CSS : le style réside dans leglobals.cssde chaque dépôt. Lorsque le paquet nomme une nouvelle classe, ou que le CSS d'un dépôt vieillit, le composant perd silencieusement son style — le bouton retombe sur l'habillage gris par défaut du navigateur, le tableau perd ses bordures. Ce garde-fou confronte lesclassNamedu paquet au CSS du dépôt.
Versionnage¶
Les trois mécanismes suivent le semver. Dès qu'une nouvelle version paraît,
Dependabot ouvre une pull request dans les dépôts consommateurs ; la mise à
jour elle-même est un changement d'une ligne dans go.mod ou package.json.
Relever la version du modèle ne suffit pas¶
Comme chaque plateforme hérite du modèle, on croit volontiers qu'« il suffit de relever le modèle pour que cela se propage partout ». En réalité, les deux types de dépendance ne fonctionnent pas de la même façon :
| Fichier | merge=ours ? |
Se propage depuis le modèle ? |
|---|---|---|
backend/go.mod |
oui | ❌ jamais |
frontend/package.json |
non | ✅ oui |
La protection de go.mod est une exigence structurelle : la ligne module
diffère dans chaque dépôt (…/gerege-app-mn/backend contre
…/wallet-gerege-mn/backend), si bien que chaque fusion entrerait en conflit
dès la première ligne. Relever la version du noyau backend dans le modèle ne
propage donc rien : il faut une pull request par dépôt.
L'arbre d'héritage compte en outre trois étages (public template → private
template → application) ; même un fichier capable de se propager met plusieurs
cycles d'autosync à atteindre les feuilles.
Un changement cassant bloque les pull requests de dépendances
Passer le dictionnaire de quatre langues à sept a cassé chaque endroit où
figurait Record<Lang, …>. Résultat : chaque pull request de Dependabot
échouait au tsc, personne ne les fusionnait, la suivante s'empilait
par-dessus — et la flotte s'est dispersée de la v0.4.0 à la v0.10.2.
Aussi, pour un changement cassant dans un paquet : (a) consigner les instructions de migration dans les notes de version, (b) livrer le correctif dans les dépôts consommateurs en même temps. Les mises à jour automatiques exigent une étape manuelle en cas de changement cassant.
Le retard est silencieux
Si les pull requests de dépendances s'accumulent, les plateformes s'éparpillent entre plusieurs versions et la promesse « une correction atteint tout le monde » se brise. Fermer régulièrement ces pull requests est une condition de fonctionnement de cette structure, non un agrément facultatif.
Voir aussi¶
- Pile technologique
- Conventions de plateforme
- Authentification et autorisation — le réglage
AUTH_MODE