-
Notifications
You must be signed in to change notification settings - Fork 10
Architecture
Vue d'ensemble des mécanismes techniques de la plateforme.
Audience principale : nouveaux développeurs (onboarding). L'équipe métier / PO peut utiliser ce document en survol pour situer les composants cités lors des arbitrages.
Ce document complète
docs/features.md(vue fonctionnelle) etdocs/parcours-utilisateurs.md(flux end-to-end). Pour les conventions de code détaillées, voirpackages/app/CLAUDE.md.
- Vue d'ensemble
- Stack technique
- Structure du repo
- Couche domain
- Authentification, autorisation, impersonation
- API tRPC
- Base de données (Drizzle + PostgreSQL)
- Stockage de fichiers (S3-compatible)
- Audit logging
- Sécurité
- UI, DSFR, accessibilité
- Observabilité (Sentry)
- Tests
- CI/CD et déploiement
- Dépendances externes
EGAPRO V2 est une application Next.js 16 (App Router, React 19) servant un site public et un espace authentifié, déployée dans un cluster Kubernetes via Kontinuous. Le code applicatif vit dans un monorepo pnpm (packages/app/) ; le package packages/api/ est un placeholder vide hérité de la V1.
flowchart LR
subgraph "Utilisateur"
U[Navigateur]
end
subgraph "Cluster Kubernetes"
I[Ingress]
A[Pod app — Next.js]
AP[Pod APISIX-suit]
DB[(PostgreSQL)]
S3[(MinIO / S3)]
AV[clamavd]
end
subgraph "Externes"
PC[ProConnect]
GIP[GIP-MDS]
SI[INSEE Sirene]
ST[SUIT]
end
U -->|HTTPS| I
I --> A
ST -->|/api/v1| AP --> A
A <--> DB
A <--> S3
A --> AV
A <--> PC
A --> GIP
A --> SI
Trois grandes catégories de surface technique :
- Pages publiques (recherche, FAQ, mentions légales) : Server Components, lecture seule, pas d'authentification
-
Espace déclarant (
/mon-espace,/declaration-remuneration/*,/avis-cse/*) : authentifié via ProConnect, écritures protégées par tRPC -
API privées : tRPC interne (
/api/trpc/*) pour le front, et REST-like (/api/v1/*,/api/export/*,/api/pdf/*,/api/upload,/api/declaration-lock/release) pour les intégrations externes et les uploads
| Couche | Outil | Version | Rôle |
|---|---|---|---|
| Framework | Next.js (App Router) | ^16 | Serveur rendu, routing, RSC |
| UI | React | ^19 | Composants serveur + client |
| Langage | TypeScript | ^5 (strict) | Typage fort, noUncheckedIndexedAccess
|
| Design system | DSFR | ^1.14 | Système officiel de l'État (sans react-dsfr) |
| Styling | SCSS Modules + DSFR | sass | Mixins DSFR auto-injectés |
| API interne | tRPC | ^11 | RPC typé bout-en-bout |
| ORM | Drizzle | ^0.45 | SQL typé, migrations versionnées |
| BDD | PostgreSQL | 14.17 | Local : Docker ; prod : managed |
| Auth | NextAuth | 4.x | Wrap ProConnect (OAuth/OIDC) |
| Validation | Zod | ^4 | Schemas partagés front + back |
| Lint / Format | Biome | ^2 | Remplace ESLint + Prettier |
| Tests unit | Vitest | ^4 | + coverage ≥ 75% global, 100% sur domain/
|
| Tests E2E | Playwright | ^1.58 | Une E2E par page.tsx minimum |
| Observabilité | Sentry | — | Erreurs serveur + client |
| Nodemailer + maildev | — | Transactional, maildev en local | |
| Stockage fichiers | MinIO local / S3 prod | — | Accessible via @aws-sdk/client-s3
|
| Antivirus | ClamAV (clamavd) |
— | Scan des PDF avant stockage |
| Cache (optionnel) | Valkey (Redis-compat) | — | Présent en docker-compose |
| Package manager | pnpm workspaces | ^10 | Workspace packages/*
|
egapro/
├── packages/
│ ├── app/ # Application Next.js (tout le code actif)
│ │ ├── src/
│ │ │ ├── app/ # Routes Next.js (App Router) — wrappers fins
│ │ │ ├── modules/ # Logique métier + composants par domaine
│ │ │ ├── server/ # Code server-only (api, auth, db, audit, services)
│ │ │ ├── trpc/ # Client tRPC (react, server, query-client)
│ │ │ ├── env.js # Variables d'env typées (@t3-oss/env-nextjs)
│ │ │ ├── middleware.ts # Edge middleware (admin guard + APISIX defense)
│ │ │ ├── instrumentation.ts # Sentry server + edge
│ │ │ └── instrumentation-client.ts # Sentry client
│ │ ├── e2e/ # Tests Playwright
│ │ ├── public/dsfr/ # Assets DSFR copiés (git-ignored)
│ │ └── scripts/ # Scripts utilitaires (audit-cleanup.mjs, copy-dsfr.mjs)
│ └── api/ # Placeholder vide (héritage V1)
├── .kontinuous/ # Manifests Kubernetes (Helm-like) pour dev/preprod/prod
├── .github/workflows/ # CI/CD GitHub Actions
├── docs/ # Cette documentation
├── docker-compose.yml # Stack locale (db, minio, clamavd, maildev, valkey)
└── CLAUDE.md # Instructions globales (agents IA + devs)
Organisation par domaine fonctionnel, pas par type de fichier. Chaque module expose un index.ts (barrel) ; les consommateurs importent uniquement depuis le barrel.
modules/
domain/ # Règles métier pures (cf. §4)
layout/ # Header, Footer, SkipLinks
home/ # Page d'accueil
login/ # Page de connexion ProConnect
auth/ # SessionProvider, useReadOnlyGuard, useIsImpersonating
profile/ # Profil utilisateur (téléphone)
my-space/ # /mon-espace
declaration-remuneration/ # Wizard déclaration index
cseOpinion/ # Avis CSE (formulaires + upload PDF)
declarationPdf/ # Génération PDF récap & reçu
noSanctionAttestation/ # Attestation no-sanction PDF
export/ # Exports XLSX + API publique
admin/ # Espace administrateur DGT
referents/ # Annuaire référents (public)
aide/ # Pages d'aide + contact
faq/ # FAQ statique
legal/ # Pages légales
audit/ # Constantes & types audit
mail/ # Mails transactionnels
analytics/ # Matomo
shared/ # Hooks et composants transverses (useZodForm, useFileUploadForm,
# FileUpload, uploadFile, fileNameValidation, …)
Règle d'or : pas de composant custom dans src/app/. Les fichiers page.tsx sont des wrappers fins qui importent depuis un module. Cette règle est bloquée par le hook block-bad-patterns (rejet de l'edit).
server/
api/
routers/ # Procédures tRPC, une par domaine
# (declaration, admin, cseOpinion, declarationLock, …)
trpc.ts # Builder + procédures (publicProcedure, protectedProcedure,
# declarationLockedWriteProcedure, …)
root.ts # Composition : appRouter
auth/ # Configuration NextAuth (ProConnect provider + callbacks)
audit/ # Middleware tRPC + withAuditedRoute + cachedAuth + logAction
db/ # Schéma Drizzle + connexion PostgreSQL
services/ # Intégrations tierces (gipMds, uploadPipeline,
# declarationLockService, …)
src/modules/domain/ concentre toutes les règles métier pures : isomorphes (utilisables côté serveur ET client), aucune dépendance React / tRPC / Drizzle.
domain/
index.ts # Barrel — point d'import unique
types.ts # GapLevel, DeclarationStatus, …
shared/
constants.ts # GAP_ALERT_THRESHOLD, MAX_CSE_FILES, COMPANY_SIZE_*
campaign.ts # getCurrentYear, getCseYear (règles temporelles)
companySize.ts # isCseRequired, COMPANY_SIZE_RANGES
declarationFlags.ts # isComplianceProcessRequired, isComplianceProcessRevisionRequired
declarationLock.ts # DEFAULT_LOCK_TIMEOUT_MINUTES, LOCK_HEARTBEAT_INTERVAL_MS
declarationStatus.ts # computeDeclarationStatus, isCancelled, isDeclarationSubmitted
gap.ts # computeGap, computeGapBetween, computeGapRatio, gapLevel, formatGap
percentage.ts # percentageOf, proportionOf
siren.ts # extractSiren, formatSiren, validateSiren
workforce.ts # computeWorkforceTotal, sumQuartileWorkforce, sumCategoryWorkforce
… # (submissionRate, quartile, regions, number, format, …)
__tests__/ # 100% coverage sur toutes les fonctions
Pourquoi cette discipline :
- Les règles changent peu souvent mais sont critiques (un bug ici impacte 100% des déclarations).
- Centraliser permet de tester exhaustivement sans monter une page.
- Les contraintes du règlement (seuils, calendriers) sont lisibles à un endroit unique, ce qui simplifie l'audit métier.
- La source unique évite les divergences silencieuses entre le wizard, l'export et les routers tRPC.
Hooks de garde — le hook block-bad-patterns bloque les patterns suivants en dehors de domain/ et des tests :
| Pattern bloqué | Alternative obligatoire |
|---|---|
new Date().getFullYear() |
getCurrentYear() / getCseYear() depuis ~/modules/domain
|
siret.slice(0, 9), .substring(0, 9), .substr(0, 9)
|
extractSiren(siret) depuis ~/modules/domain
|
.getMonth() / .getDate()
|
helpers de campagne depuis ~/modules/domain
|
cancelledAt !== null (réimplémentation de isCancelled) |
isCancelled({ cancelledAt }) depuis ~/modules/domain
|
workforce >= 100 (réimplémentation de isCseRequired) |
isCseRequired(workforce) / isComplianceProcessRequired(...) depuis ~/modules/domain
|
| Seuils hardcodés (5, 50, 100) | constantes nommées (GAP_ALERT_THRESHOLD, COMPANY_SIZE_ANNUAL_MIN, …) |
Toujours importer depuis le barrel :
import {
getCurrentYear, GAP_ALERT_THRESHOLD,
isCseRequired, isCancelled, isDeclarationSubmitted,
isComplianceProcessRequired, isComplianceProcessRevisionRequired,
computeWorkforceTotal, sumQuartileWorkforce,
percentageOf, proportionOf,
} from "~/modules/domain";Auth déléguée à ProConnect, le SSO de l'État. Configuration dans src/server/auth/. NextAuth 4.x stocke la session dans un JWT cookie (pas de table sessions côté DB).
En local, le fournisseur de test est FIA1V2 (compte test@fia1.fr sans mot de passe).
sequenceDiagram
participant User as Utilisateur
participant App as App Next.js
participant PC as ProConnect
participant DB as PostgreSQL
User->>App: GET /login
App->>PC: Redirect (OAuth code)
PC-->>User: Login form
User->>PC: Submit credentials
PC-->>App: Callback ?code=...
App->>PC: POST /token
PC-->>App: id_token + access_token
App->>DB: upsert users (email, firstName, lastName)
Note over App: jwt callback enrichit token<br/>avec userId, isAdmin
App-->>User: Set-Cookie next-auth.session-token<br/>Redirect /mon-espace
Le callback jwt (NextAuth) :
- À la connexion : upsert dans
users(email, prénom, nom), récupèreidetisAdmin. - Injecte
userId,email,isAdmindans le token. - Si l'utilisateur est admin et qu'une impersonation est active (via
session.update({ siren })), injecteimpersonation: { siren, startedAt }.
Deux responsabilités, deux scopes URL :
| Scope | Garde | Comportement si KO |
|---|---|---|
/admin/* |
Décode le JWT, exige isAdmin === true
|
Redirect /login?callbackUrl=... si pas de token, ou /mon-espace si token sans isAdmin |
/api/v1/* |
Vérifie le header X-Gateway-Forwarded (constant-time) |
403 si présent mais invalide |
Defense in depth : src/app/admin/layout.tsx re-vérifie la session côté Node runtime pour les tokens dépourvus du flag isAdmin (fallback de migration).
L'admin DGT peut incarner une entreprise pour la dépanner. Le flux :
sequenceDiagram
participant Admin
participant App
participant DB
Admin->>App: /admin/impersonate (search siren)
App->>App: session.update({ impersonation: { siren } })
Note over App: jwt callback re-fire<br/>persiste impersonation dans le token
App->>DB: insert adminImpersonationEvents
Admin->>App: navigate /mon-espace<br/>(en tant que l'entreprise)
Note over App: useReadOnlyGuard + companyWriteProcedure<br/>bloquent les mutations
Note over App: useDeclarationLock désactivé<br/>(pas d'acquisition de verrou)
L'écriture est bloquée à deux niveaux : front (useReadOnlyGuard) et back (companyWriteProcedure rejette si impersonation). Tracé dans adminImpersonationEvents + audit log. Le verrou collaboratif est également désactivé en mode impersonation.
Une procédure tRPC = un appel typé bout-en-bout (input Zod, output inféré). Le client React utilise @trpc/react-query pour bénéficier de SWR / cache.
| Builder | Middleware appliqués | Audience |
|---|---|---|
publicProcedure |
rien | Public (non authentifié) |
protectedProcedure |
session valide | Utilisateur connecté |
companyProcedure |
session + binding SIREN (depuis le contexte) | Utilisateur agissant pour une entreprise |
companyWriteProcedure |
+ read-only guard (refus si impersonation) | Utilisateur, écriture |
declarationProcedure |
companyProcedure + résolution déclaration |
Utilisateur, lecture déclaration |
declarationWriteProcedure |
+ read-only guard | Utilisateur, écriture déclaration |
declarationLockedWriteProcedure |
declarationWriteProcedure + vérification verrou actif |
Utilisateur, écriture déclaration (gardée par le verrou collaboratif) |
adminProcedure |
session + isAdmin === true
|
Admin DGT |
Définies dans src/server/api/trpc.ts. Les routers (un par domaine) composent ces builders selon le besoin.
declarationLockedWriteProcedure : rejette avec CONFLICT si aucun verrou actif n'est tenu par l'utilisateur courant sur ctx.declarationId. Utilisé par les procédures de mutation du wizard (updateStep1…submit) pour garantir qu'aucune écriture concurrente n'est possible.
| Router | Fichier | Principales procédures |
|---|---|---|
declaration |
routers/declaration.ts |
getOrCreate, updateStep1…updateStep4, updateEmployeeCategories, submit, saveCompliancePath, submitSecondDeclaration, submitJointEvaluation
|
declarationLock |
routers/declarationLock.ts |
getActiveLockForCurrentDeclaration, acquireLock, heartbeat, releaseLock, getLockState
|
declarationDraft |
routers/declarationDraft.ts |
Gestion du brouillon |
cseOpinion |
routers/cseOpinion.ts |
get, saveOpinions, uploadFile, deleteFile, getFiles, getFileContentTypes, setFileContentTypes, finalize
|
admin |
routers/admin.ts |
Gestion admin générale |
adminDeclarations |
routers/adminDeclarations.ts |
search, getById, getRecap, releaseLock
|
adminSettings |
routers/adminSettings.ts |
getOverview, getDeadlinesByYear, upsertCampaignDeadlines, getLockTimeout, updateLockTimeout
|
adminReferents |
routers/adminReferents.ts |
CRUD référents |
adminStats |
routers/adminStats.ts |
Statistiques campagne |
profile |
routers/profile.ts |
get, updatePhone
|
company |
routers/company.ts |
get, list, getWithDeclarations, getSanctionStatus
|
publicReferents |
routers/publicReferents.ts |
search, getById
|
gipMds |
routers/gipMds.ts |
importFromUrl |
mail |
routers/mail.ts |
resendReceipt |
Les schémas Zod sont la single source of truth : utilisés à la fois par le formulaire React (useZodForm) et par la procédure tRPC (.input(schema)). Ils vivent dans src/modules/{domain}/schemas.ts, jamais inline dans src/server/api/routers/ (règle bloquée par hook).
Un middleware tRPC global lit la map PROCEDURE_TO_ACTION (dans src/server/audit/trpcMiddleware.ts). Si la procédure courante y figure, un appel à logAction(...) est ajouté après l'exécution (succès ou échec). Voir §9.
Définition dans src/server/db/. Tables principales :
| Table | Rôle |
|---|---|
users |
Utilisateurs (email ProConnect, firstName, lastName, isAdmin) |
userCompanies |
N-N user × siren (rattachement) |
companies |
Entreprises (siren, name, nafCode, workforce, hasCse) |
declarations |
Déclarations index (id, siren, year, status, currentStep, …) |
declarationLocks |
Verrou collaboratif d'édition (un seul par déclaration) |
jobCategories |
Catégories d'emploi (déclaration, optionnel) |
employeeCategories |
Indicateur G (par catégorie) |
cseOpinions |
Avis CSE (deux types : exactitude + écarts) |
cseOpinionFiles |
Associations fichier ↔ type de contenu (par declarationNumber + type) |
files |
PDF stockés sur S3 (cse_opinion, joint_evaluation) |
referents |
Annuaire référents régionaux |
campaignDeadlines |
Deadlines par année (configuration admin) |
globalSettings |
Paramètres globaux (table à une seule ligne, id = 1) : declarationLockTimeoutMinutes
|
gipMdsData |
Pré-remplissage GIP-MDS (par siren + year) |
adminImpersonationEvents |
Trace des impersonations admin |
audit.action_log |
Log d'audit (schéma Postgres dédié audit) |
Table declaration_lock — au plus une ligne par declaration_id (unique index).
| Colonne | Type | Rôle |
|---|---|---|
id |
varchar(255) PK |
UUID généré automatiquement |
declarationId |
varchar(255) FK → declarations.id |
Déclaration concernée (cascade delete) |
lockedByUserId |
varchar(255) FK → users.id |
Détenteur du verrou |
lockedAt |
timestamp tz |
Horodatage de l'acquisition initiale |
lastHeartbeatAt |
timestamp tz |
Dernier heartbeat reçu |
expiresAt |
timestamp tz |
Date d'expiration (index pour lectures rapides) |
Table global_setting — table à une seule ligne (id = 1).
| Colonne | Type | Rôle |
|---|---|---|
id |
integer PK |
Toujours 1
|
activeCampaignYear |
integer |
Année de campagne active (optionnel) |
declarationLockTimeoutMinutes |
integer NOT NULL DEFAULT 30 |
Délai d'expiration du verrou en minutes |
updatedAt |
timestamp tz |
Date de dernière mise à jour |
updatedBy |
varchar FK → users.id |
Admin ayant effectué la dernière mise à jour |
Toutes les propriétés de schéma sont camelCase côté TypeScript, automatiquement mappées en snake_case côté SQL via casing: "snake_case" (configuré dans src/server/db/index.ts et drizzle.config.ts). Ne jamais spécifier de nom de colonne explicite.
pnpm db:generate # génère un fichier SQL après modif schéma
pnpm db:migrate # applique les migrations en attente
pnpm db:push # applique le schéma directement (dev only, sans migration)
pnpm db:studio # UI Drizzle Studio (inspection)Les migrations sont versionnées dans le repo (packages/app/drizzle/). En CI/CD, le job migrate applique les migrations en attente avant de démarrer l'app.
Toute opération qui touche plusieurs tables doit utiliser db.transaction(...). Règle enforcée par structural-auditor et security-auditor.
Les PDF (avis CSE, évaluation conjointe) sont stockés sur MinIO en local (service docker-compose minio) et sur S3 en cluster. L'accès se fait via @aws-sdk/client-s3.
L'upload est centralisé dans la Route Handler POST /api/upload (src/app/api/upload/route.ts). Le flux de traitement s'exécute en une seule requête, sans fenêtre d'orphelin S3 :
sequenceDiagram
participant User
participant App
participant AV as clamavd
participant S3
participant DB
User->>App: POST /api/upload<br/>(X-Flow-Type, X-Filename, Content-Type, body=stream)
App->>App: auth (401 si pas de session)
App->>App: validation nom de fichier (400 si invalide)
App->>App: validation MIME (400 si type non autorisé)
App->>AV: scan du flux
alt Virus détecté
AV-->>App: virus detected
App-->>User: 422 + nom du virus
else Fichier sain
AV-->>App: clean
App->>S3: PutObject (key = siren/year/<flow>/<uuid>.pdf)
App->>DB: insert files (declarationId, type, s3Key, fileName)
App-->>User: { fileId, fileName }
end
En-têtes requis :
| En-tête | Rôle |
|---|---|
X-Flow-Type |
cse_opinion ou joint_evaluation — sélectionne la logique métier |
X-Filename |
Nom du fichier (validé côté serveur) |
Content-Type |
MIME type déclaré (vérifié contre la liste autorisée) |
Le module ~/modules/shared/fileNameValidation.ts fournit validateFileName(fileName, mimeType). Cette validation est appliquée :
-
Côté client : dans le composant
FileUpload.tsx, avant la vérification MIME et la vérification de taille. -
Côté serveur : dans la Route Handler
POST /api/upload, après la vérification MIME déclarée, avant de lancer le pipeline (ClamAV + S3).
| Vérification | Détail |
|---|---|
| Non vide | Refus si la valeur trimmée est vide |
| Longueur | Refus si > MAX_FILENAME_LENGTH (200 caractères) |
| Caractères interdits |
< > : " | ? * ; / \ et caractères de contrôle (U+0000–U+001F, U+007F) |
| Caractères de format Unicode | Toute la catégorie Cf est rejetée : largeur nulle (U+200B–U+200D, U+FEFF) et contrôles bidirectionnels (RLO/LRO/RLE/LRE/PDF, LRI/RLI/FSI/PDI) |
| Cohérence extension-MIME | L'extension (EXTENSION_MIME_MAP) doit correspondre au MIME déclaré |
Le schéma Zod fileNameSchema (exporté depuis le même module) encapsule ces règles pour les réutiliser dans des formulaires ou des procédures.
Le service clamavd scanne les uploads via le protocole ClamAV (TCP). Si le scanner est indisponible, le fichier est rejeté (503). Si un virus est détecté, le fichier est rejeté avant tout stockage S3 (422 + nom de la signature).
Les fichiers sont servis via une Route Handler /api/v1/files/:fileId qui fait du streaming depuis S3. Auth duale : header APISIX (côté SUIT) ou session NextAuth (côté front).
Module : src/modules/audit/ (constantes, types) + src/server/audit/ (runtime). Documentation détaillée : .claude/rules/audit-logging.md.
Conformité CNIL / DGT : tracer toutes les actions de mutation et toutes les lectures de données sensibles (PII, données entreprise, PDF), avec rétention bornée.
id, user_id, user_email, siren, action, status,
ip_address, user_agent, metadata (jsonb), created_at
Le metadata jsonb est automatiquement sanitizé : les clés password, token, refresh_token, secret, client_secret, authorization, apikey, api_key, accesskey, access_key, private_key sont strippées récursivement.
Définies dans src/modules/audit/shared/constants.ts :
| Catégorie | Rétention | Exemples |
|---|---|---|
mutation |
365 j | Toutes les écritures (déclaration, CSE, admin, verrou) |
read_sensitive |
180 j |
profile.get, declaration.getOrCreate, declaration.getStatusHistory, recherche admin, PDF, état du verrou |
public_search |
180 j | Recherche / vue de référents publics |
auth |
365 j | Login OK / KO, logout |
export |
365 j | API publique d'export |
system |
365 j | Import GIP, cron de cleanup |
AUDIT_RETENTION_DAYS_SHORT = 180, AUDIT_RETENTION_DAYS_LONG = 365. Surchargeables via les variables d'environnement EGAPRO_AUDIT_RETENTION_SHORT_DAYS / EGAPRO_AUDIT_RETENTION_LONG_DAYS.
Toute nouvelle action audited requiert 3 points :
- Constante dans
actionKeys.ts(AUDIT_ACTIONS.NEW_THING) - Catégorie dans
AUDIT_ACTION_CATEGORIES(drives la rétention) - Surface :
- Pour une procédure tRPC → entrée dans
PROCEDURE_TO_ACTION(middleware auto) - Pour une Route Handler → wrapper
withAuditedRoute({ action, resolveContext }, handler) - Pour un événement auth ou un cron → appel direct à
logAction(...)
- Pour une procédure tRPC → entrée dans
Clé AUDIT_ACTIONS
|
Valeur en BDD | Catégorie | Surface |
|---|---|---|---|
DECLARATION_LOCK_ACQUIRED |
declaration.lock_acquired |
mutation |
declarationLock.acquireLock (tRPC, logAction direct) |
DECLARATION_LOCK_RELEASED |
declaration.lock_released |
mutation |
declarationLock.releaseLock (tRPC) + POST /api/declaration-lock/release (withAuditedRoute) |
ADMIN_DECLARATION_RELEASE_LOCK |
admin_declaration.release_lock |
mutation |
adminDeclarations.releaseLock (tRPC, PROCEDURE_TO_ACTION) |
DECLARATION_LOCK_STATE_READ |
declaration.lock_state_read |
read_sensitive |
declarationLock.getLockState (tRPC, PROCEDURE_TO_ACTION) |
ADMIN_SETTINGS_UPDATE_LOCK_TIMEOUT |
admin_settings.update_lock_timeout |
mutation |
adminSettings.updateLockTimeout (tRPC, PROCEDURE_TO_ACTION) |
packages/app/scripts/audit-cleanup.mjs tourne quotidiennement (CronJob Kubernetes) et purge les lignes au-delà de leur fenêtre de rétention. Modifications de ce script → test d'intégration obligatoire.
L'API privée consommée par SUIT est protégée par une passerelle APISIX standalone :
flowchart LR
SUIT -->|HTTPS<br/>Authorization: Bearer| I[Ingress<br/>api-suit.host]
I --> AP[apisix-suit<br/>gateway]
AP -->|key-auth<br/>limit-req<br/>proxy-rewrite| AP2{plugins}
AP2 -->|+ X-Gateway-Forwarded| APP[Pod app]
APP -->|Edge middleware<br/>vérifie en constant-time| RH[Route Handler]
RH --> BL[Business logic]
Plugins APISIX actifs : key-auth, limit-req (~10 req/s, burst 5), proxy-rewrite (injecte X-Gateway-Forwarded).
Toute entrée externe est validée via Zod : formulaires, procédures tRPC, Route Handlers, variables d'environnement (src/env.js).
Déclarées et validées dans src/env.js. Jamais lire process.env directement (bloqué par hook).
Aucune valeur secrète dans le repo. Gérés via des sealed-secrets sous .kontinuous/.
Le router declarationLock résout toujours l'ID de déclaration côté serveur à partir du SIREN de la session (resolveOwnDeclarationId) — jamais depuis le seul input client. Cela empêche un co-déclarant de verrouiller ou libérer une déclaration appartenant à une autre entreprise en forgeant un declarationId arbitraire.
Le Système de Design de l'État est utilisé en mode "natif" : on importe le CSS et le JS DSFR directement, sans wrapper React (react-dsfr n'est pas utilisé). Concrètement :
-
Assets : copiés dans
public/dsfr/parscripts/copy-dsfr.mjs(git-ignored, regénéré surdev/build). -
CSS : chargé via
<link>danssrc/app/layout.tsx. -
JS : chargé via
<Script type="module" strategy="beforeInteractive">. Gère modales, dropdowns, theme toggle, navigation clavier. Ne jamais dupliquer ce comportement en React — utiliser les attributsdata-fr-*.
Discipline RSC stricte : Server Component par défaut. "use client" uniquement pour les hooks, événements navigateur ou Web APIs. Isoler la partie interactive au niveau le plus bas possible.
Priorité stricte : 1) classes DSFR → 2) utilities DSFR + CSS variables → 3) SCSS Module scopé (dernier recours).
style={} inline est bloqué par hook. Les @media (width|screen) en SCSS aussi (forcer @include respond-from(md)).
Activé via data-fr-scheme="system" sur <html>. Cookie fr-theme lu par un script inline en tête. Modale ThemeModal pour le choix utilisateur.
Score Lighthouse accessibilité = 100% (seuil bloquant CI dans .lighthouserc.json). Audit quotidien automatisé (rgaa-audit.yaml).
Trois entrées Sentry, une par runtime :
| Fichier | Runtime | Rôle |
|---|---|---|
src/instrumentation.ts |
Server (Node) | Erreurs SSR, Server Components, tRPC |
src/sentry.edge.config.ts |
Edge | Middleware src/middleware.ts
|
src/instrumentation-client.ts |
Client (browser) | Erreurs React + global handlers |
src/app/global-error.tsx capture les erreurs non gérées de l'arbre React et les remonte à Sentry.
| Type | Outil | Localisation | Couverture cible |
|---|---|---|---|
| Unit | Vitest | src/modules/**/__tests__/ |
≥ 75% global, 100% sur domain/
|
| E2E | Playwright | packages/app/src/e2e/ |
Au moins une E2E par page.tsx
|
| A11y | Lighthouse CI | .lighthouserc.json |
100% accessibilité (bloquant) |
| RGAA quotidien | Workflow GitHub Actions | .github/workflows/rgaa-audit.yaml |
Cron 06:00 UTC L–V |
| Intégration BDD | Vitest + Docker | *.integration.test.ts |
Obligatoire pour code touchant audit.action_log
|
Les mocks standards (next/link, next/navigation, next/image, next-auth/react, server-only, ~/trpc/server) sont définis une seule fois dans src/test/setup.ts et auto-chargés par Vitest. Ne jamais les dupliquer dans les fichiers de test.
pnpm test # Vitest (watch mode interactif)
pnpm test:e2e # Playwright (nécessite pnpm dev sur :3000)
pnpm test:lighthouse # Lighthouse CI (nécessite pnpm dev sur :3000)
pnpm test:integration # Tests intégration BDD (nécessite Docker)| Workflow | Trigger | Rôle |
|---|---|---|
ci.yaml |
push | Build + lint + format + typecheck + tests |
e2e.yaml |
push | Tests E2E Playwright |
lighthouse.yaml |
deployment_status |
Audit Lighthouse sur l'env de review |
db-schema.yaml |
push (master, alpha) | Génération doc schéma BDD |
review-auto.yaml / review.yaml
|
push branches | Déploiement environnement de review (par PR) |
deactivate.yaml |
PR closed / branch deleted | Cleanup environnement de review |
preproduction.yaml |
push branche beta
|
Déploiement preprod |
production.yaml |
push tag | Déploiement prod |
release.yml |
manuel | semantic-release (versionnement automatique) |
rgaa-audit.yaml |
cron 06:00 UTC L–V | Audit RGAA quotidien |
claude-question.yml / claude-revue-rgaa.yml
|
issue/PR labels | Intégrations IA (questions ; revue RGAA) |
Kontinuous templatise les manifests Kubernetes. Structure dans .kontinuous/ :
.kontinuous/
Chart.yaml # Sub-charts (app, postgres, apisix-suit, …)
config.yaml # Config par défaut
values.yaml # Valeurs par défaut
templates/ # Manifests (Deployment, Service, ConfigMap, sealed-secrets, …)
env/
dev/ # Surcharges dev
preprod/ # Surcharges preprod
prod/ # Surcharges prod
Trois environnements gérés : dev (review apps), preprod (branche beta), prod (tags Git).
docker-compose.yml à la racine lance les services nécessaires au dev :
| Service | Image | Rôle |
|---|---|---|
db |
postgres:14.17 |
Base de données principale |
migrate |
node:22-slim |
Applique les migrations Drizzle au démarrage |
minio |
minio/minio |
Stockage S3-compatible |
maildev |
— | Capteur de mails dev (web UI sur :1080) |
clamavd |
— | Antivirus pour l'upload de PDF |
valkey |
— | Cache Redis-compatible (optionnel) |
docker compose up -d # démarre tout
pnpm dev:app # lance Next.js sur :3000| Système | Rôle | Critique ? |
|---|---|---|
| ProConnect | SSO d'État (auth utilisateurs) | Oui (pas de fallback en prod) |
| GIP-MDS | Calcul des indicateurs A–F (CSV importé chaque mars) | Non (déclaration possible sans pré-remplissage) |
| INSEE Sirene | Identification des entreprises (raison sociale, NAF, effectif) | Lecture (cache) |
| SUIT / Delphes | Inspection du travail (consomme /api/v1/*) |
Non (intégration sortante) |
| D@ccords | Dépôt des accords collectifs | Non (lien externe) |
| AWS S3 (ou MinIO) | Stockage des PDF | Oui pour l'upload CSE |
| Mailer SMTP (prod) | Envoi des reçus de déclaration | Important (pas bloquant si défaillant) |
Les pannes de ProConnect bloquent complètement la connexion ; aucune procédure de secours côté app.
-
Conventions de code détaillées :
packages/app/CLAUDE.md -
Règles de qualité automatisées :
.claude/rules/(audit logging, code-quality, database-drizzle, react-components, styling-dsfr, testing, trpc-api, …) - Wiki Spec V2 (réglementation) : https://github.com/SocialGouv/egapro/wiki/Spec-V2
-
Features (vue fonctionnelle) :
docs/features.md -
Parcours utilisateurs :
docs/parcours-utilisateurs.md