Architecture

Référence technique de l'architecture Pulse ERP — stack, couches, patterns transverses, sécurité et déploiement.

Architecture

Pulse ERP est une mono-app Nuxt 4 full-stack : un seul processus Node.js sert à la fois le rendu SSR et l'API Nitro REST. Pas de microservices, pas de message broker au départ — on n'y vient que si un besoin opérationnel le justifie.

Source de cette page : docs/02-architecture.md. Pour l'auth et le RBAC, voir docs/05-auth-rbac.md.

Stack technique

Runtime

Node.js 22 LTS · TypeScript 5.7+ strict (noUncheckedIndexedAccess, verbatimModuleSyntax) · pnpm workspaces

Frontend

Nuxt 4 SSR · Nuxt UI 4 · Tailwind CSS 4 · @nuxtjs/i18n v10 · date-fns v4 · dinero.js v2

Backend

Nitro file-based routing · Zod 3 · Prisma 5 · Better Auth 1.4+ · croner pour les jobs

Infrastructure

PostgreSQL 16 · Scaleway Object Storage (S3) · Brevo / Scaleway TEM · Docker Compose en local

Frontend — choix clés

ConcernChoix
StateuseState() Nuxt + composables (pas de Pinia tant que non nécessaire)
Data fetchinguseFetch / useAsyncData / $fetch — dedup SSR automatique
Validation clientHTML5 + Nuxt UI form state (revalidation serveur systématique)
TableauxUTable + composant maison DataTable (pagination, tri, CSV)
Icons@nuxt/icon — 200k+ icônes Iconify, tree-shakable
Utils@vueuse/nuxt

Backend — choix clés

ConcernChoix
Routingserver/api/{path}.{method}.ts (file-based)
HandlerdefineApiHandler() maison — perm check + parse + audit
SessionsCookie HttpOnly Secure SameSite=Lax, table session PostgreSQL, 30 j glissant
StockageDriver pluggable : filesystem (dev) · s3 (prod, Scaleway)
EmailProvider pluggable : console (dev) · brevo · scaleway-tem (prod)
Queue / Croncroner + table Job PostgreSQL (poll 5 s, pas de Redis)
Logspino JSON stdout — requestId, userId, organizationId, latencyMs
ErreursClasses ConflictError / ValidationError / NotFoundError / ForbiddenError mappées HTTP

Architecture en couches

Route API   server/api/**/*.ts
  │  parse Zod · vérif permission · appel service
  ▼
Service     server/services/*.service.ts
  │  logique métier · transactions Prisma · audit log · events / jobs
  ▼
Repository  server/repositories/*.repository.ts
  │  Prisma pur — aucun métier, aucun side-effect
  ▼
PostgreSQL
Règles inviolables
  1. Une route API ne fait jamais de logique métier.
  2. Un service ne touche jamais Prisma directement — il passe par un repository.
  3. Un repository ne contient aucune règle métier (pas de validation, pas de calcul, pas de side-effect).
  4. Le frontend ne connaît jamais le schéma Prisma — il connaît les DTO Zod exposés par l'API.

DTO pattern

Les schémas Zod dans shared/dto/ sont la source de vérité du contrat API. Le type TypeScript est inféré du schéma, jamais défini à part.

// shared/dto/asset.dto.ts
export const AssetCreateDto = z.object({
  label: z.string().min(1).max(120),
  modelId: z.string().uuid(),
  conditionState: z.enum(['NEW', 'EXCELLENT', 'GOOD', 'NEEDS_REPAIR']),
})
export type AssetCreateDto = z.infer<typeof AssetCreateDto>

Côté composable, on importe le type :

import type { AssetDto } from '~/../shared/dto/asset.dto'
export function useWorkspaceAssets() {
  return useFetch<AssetDto[]>('/api/workspace/assets')
}

Structure du repo (monorepo pnpm)

PROJET-ERP-CRM-v2/
├── app/
│   ├── pulse/              ← application Nuxt ERP/CRM
│   │   ├── app/            # pages · layouts · composables · components · middleware
│   │   ├── server/         # api · services · repositories · lib · middleware · jobs
│   │   ├── shared/         # DTO Zod, enums, helpers purs (app ↔ server)
│   │   ├── prisma/         # schema.prisma · migrations/ · seed.ts
│   │   ├── i18n/locales/   # fr.json · en.json
│   │   └── tests/          # unit/ · integration/ · e2e/
│   └── docs/               ← site de documentation (projet séparé)
└── pnpm-workspace.yaml
Toutes les commandes (pnpm dev, pnpm db:*, pnpm test*) se lancent depuis la racine du repo — ne pas cd app/pulse.

Portails applicatifs

PortailPagesRoutes API
Console interneapp/pages/workspace/server/api/workspace/
Portail clientapp/pages/client/server/api/client/
Portail bailleur / partenaireapp/pages/partner/server/api/partner/
Super-adminapp/pages/admin/server/api/admin/
Public (SAV QR, statut)app/pages/public/server/api/public/

Patterns transverses

Multi-tenant — isolation systématique

Toute entité business porte organizationId NOT NULL. Trois lignes de défense :

  1. DB : index composite (organizationId, …) sur toutes les requêtes ; FK vers Organization.
  2. Repository : prend organizationId en paramètre, l'injecte dans la clause where. Pas de surcharge possible.
  3. Service : lit organizationId depuis session.activeOrganizationId, jamais depuis le body.
Un test d'isolation est obligatoire en CI pour chaque endpoint : un utilisateur de l'org A doit recevoir 404 (pas 403) sur les ressources de l'org B.

Audit log

Chaque mutation sensible journalise dans AuditLog (append-only, rétention 10 ans) :

await auditLog({
  actorId: session.user.id,
  organizationId: tenant.id,
  action: 'lease_contract.unlock',
  targetType: 'LeaseContract',
  targetId: contractId,
  payload: { reason, previousStatus },
  ip: getRequestIP(event),
})

Gestion des erreurs

defineApiHandler attrape les classes d'erreur et sérialise une réponse uniforme :

{
  "error": {
    "code": "CONFLICT",
    "message": "Le numéro de série existe déjà",
    "details": { "field": "serialNumber" },
    "requestId": "01HXY..."
  }
}

Un intercepteur $fetch côté frontend mappe ces erreurs vers un toast.

State machines (workflows multi-étapes)

Les workflows complexes (cycle de vie affaire LLD, processus SAV) utilisent des state machines explicites dans server/services/*.workflow.ts avec une table de transitions validées. Chaque changement d'état émet un événement métier qui peut déclencher notifications, jobs asynchrones et audit log.

Jobs & cron

croner schedule les jobs définis dans server/jobs/. Chaque exécution est tracée dans la table Job (statut PENDING → RUNNING → SUCCEEDED / FAILED, max 5 tentatives, idempotence par clé de payload).

JobFréquence
send-pending-emails / send-pending-smstoutes les 1 min
lease-payment-reminder1×/jour
pappers-refresh1×/semaine
maintenance-preventive1×/jour
cleanup-expired-tokens1×/jour
audit-log-archive (export S3)1×/mois

Conventions de code

Naming

ÉlémentConventionExemple
Composants VuePascalCase, préfixé par domaineAssetCreateForm
ComposablesuseXxx, retourne { data, pending, error, refresh }useWorkspaceAssets
Routes APIkebab-case, name.method.ts[id].patch.ts
Services*.service.tslease.service.ts
Repositories*.repository.tsasset.repository.ts
DTOPascalCase + suffixe Dto / InputAssetCreateDto
Enums DBSCREAMING_SNAKE_CASEAssetCondition.NEEDS_REPAIR

Conventional Commits

Enforced par Husky + commitlint. Format : <type>(<scope>): <description>.

Types : feat fix chore docs refactor test perf build ci.

Scopes : auth identity annuaire assets crm sales lease catalog files notif admin ai ui db infra docs.


Tests

NiveauOutilCibleCommande
UnitVitestServices purs, utils, calculs Zodpnpm test
IntegrationVitest + DB testServices qui touchent Prismapnpm test:integration
E2E + a11yPlaywright + axe-coreParcours critiques (login, contrat, signature)pnpm test:e2e

Règle fondamentale : chaque nouveau .service.ts → un .integration.test.ts dans tests/integration/. Utiliser createTenantFixture() de tests/helpers/tenant-isolation.ts, seed dans try {}, nettoyage dans finally { await fx.cleanup() }.

Avant toute PR :

pnpm test && pnpm test:integration && pnpm lint && pnpm typecheck

Sécurité & déploiement

Sécurité

  • Auth : Better Auth — sessions cookie HttpOnly Secure SameSite=Lax, MFA TOTP, magic link.
  • Headers : security-headers.ts injecte HSTS, CSP, X-Frame-Options DENY, Permissions-Policy.
  • Rate limiting : token bucket — 5 req/min/IP sur POST /api/auth/*, 300 req/min/session sur GET /api/*.
  • Secrets : aucun dans le repo ; .env ignoré par git ; rotation API keys tous les 90 j.
  • RGPD : hébergement Scaleway fr-par, anonymisation (User.anonymizedAt), export données /api/account/data-export.

Environnements

EnvURLDB
localhttp://localhost:3999Docker Postgres
staginghttps://staging.pulse-erp.frPostgres managed
prodhttps://app.pulse-erp.frPostgres managed HA

Migrations DB

Stratégie zero-downtime : colonne d'abord nullable → déploiement code → backfill → NOT NULL en migration suivante. Ne jamais supprimer de fichier sous prisma/migrations/.

CI/CD

  • ci.yml (sur PR) : lint + typecheck + unit + build.
  • e2e.yml (nightly) : Playwright sur staging.
  • deploy-prod.yml (manuel) : promotion image staging → prod + migrations + smoke tests + rollback automatique si healthcheck KO.