Backend·9 min de lecture·1,715 mots

Architecture backend (Next.js)

Architecture — Backend (app/)

Executive summary

Application Next.js 16.2.2 full-stack multi-tenant. Une seule codebase sert 3 surfaces UI (vitrines publiques *.wari.pro, back-office gérant /admin, back-office SA /superadmin) et l'API REST consommée par l'app mobile (/api/mobile/*, 186 routes). Auth JWT custom avec jose, partagée entre cookie web (superapp_session) et Bearer mobile.

Technology stack

CatégorieTechnologieVersionPourquoi ce choix
FrameworkNext.js16.2.2 (App Router + Turbopack)SSR/SSG + API routes dans un seul build. Turbopack pour dev rapide. NB : breaking changes vs Next 14 — voir app/AGENTS.md et node_modules/next/dist/docs/ avant tout code généré par IA.
LangageTypeScript5.x strictTypage des contrats API et types Prisma générés
ORMPrisma7.6 + @prisma/adapter-pgModèles typés, migrations versionnées, adapter pg natif
DBPostgreSQL15-alpineJSONB (vitrine snapshots), CHECK constraints (XOR PushToken), indexes optimisés (P0 hardening)
CacheRedis7-alpine (ioredis)OTP TTL 15min, rate-limit, cache requêtes lourdes
Object storageMinIOlatestSelf-hosted S3-compatible, proxié Nginx sur /medias/
Authjose (JWT)6.xJWT HS256 custom — secret unique NEXTAUTH_SECRET, cookie httpOnly + Bearer mobile. Pas NextAuth malgré next-auth@5.0.0-beta.30 listé.
ValidationZod4.xValidation entrées API + types inférés
EmailResend6.xTransactionnel (commandes, paiements, RDV, accès)
SMSVonage3.xPartiel V1 (envoi OTP, message commande)
Pushexpo-server-sdk6.xEnvoi push expo-notifications (FCM + APNs)
SearchTypesense0.25.2 (Docker)Search image V2 Option A (vector search + texte)
AI@google/generative-ai (Gemini) + Anthropic VisionlatestSnap-to-list OCR (Gemini), recherche par image V1 (Claude)
EmbeddingsHuggingFace CLIPAPIVector embeddings pour Typesense
AnalyticsPostHog1.372 (client)Tracking funnel, events
MonitoringSentry10.53@sentry/nextjs server + client + instrumentation.ts
State (admin)Zustand 5 + ImmerlatestStores builder, panels, ...
Drag&drop@dnd-kit6/10/3Builder visuel + tri colonnes
StyleTailwind4 (PostCSS plugin)Utility-first
TestsVitest3.2 + @vitest/coverage-v8Tests lib/ (P0 hardening : 8 tests existants)
Cryptobcryptjs3Hash mdp gérants
Crypto Bcrypto-js4Tokens iCal, magic link
Imagesharp0.34Conversion + redim upload média

Architecture pattern

Pattern principal : modular monolith — un seul process Next.js sert vitrines, admin, superadmin, et API mobile. Modularité par dossier (admin/ vs superadmin/ vs api/mobile/) et par module métier (lib/notif/, lib/email/, lib/restaurant/, ...).

Sous-patterns :

  • Multi-tenancy par row (single DB) : chaque modèle porte tenantId String. Pas de schémas Postgres séparés.
  • JWT custom + cookie/Bearer unifié : getSessionFromRequest(req) lit cookie OU Authorization: Bearer. Le payload est identique. Le secret est unique.
  • Route Handlers App Router : src/app/api/**/route.ts exporte GET/POST/PATCH/DELETE. Pas d'API Pages legacy.
  • Helpers source-of-truth uniques dans src/lib/ — chaque concept métier a 1 fichier qui exporte les types + helpers (ex. acces-vitrine.ts, canaux.ts, restaurant.ts, ical.ts). Le frontend et l'API consomment les mêmes types.
  • Notification Engine polymorphique (lib/notif/) : table NotifEvent + table NotifDelivery + enum NotifCanal. Dispatch via abonnements (Abonnement polymorphic via cibleType + cibleId).
  • Snapshot-based vitrine builder : VitrineSnapshot.statut (DRAFT/PUBLISHED) + VitrinePage.type (APPLICATIVE/STATIQUE). isDirty calculé client-side via fast-deep-equal. Pages applicatives (Boutique, Services) ne sont pas persistées — rendues dynamiquement.

Data architecture

72 modèles Prisma, 36 enums, 73 migrations. Voir data-models-backend.md pour la cartographie complète.

Familles principales :

FamilleModels clés
Tenant + accèsTenant, TenantModule, TenantCanal, Market, TenantCategorieWari, ConfigAccesVitrine, ConfigPaiementGerant, NiveauAcces enum, AccesVitrine
IdentitéUser (gérants TENANT_ADMIN/SUPER_ADMIN), ClientAccount (clients finaux), MagicToken, PushToken (XOR userId/clientAccountId)
CatalogueCategorie, Produit, ProduitCategorie, Media, MediaAlbum, MediaAlbumMembership, ModeleProduit, ModeleAttribut, VarianteProduit, VariantePrestation, ModeleSpec, ProduitSpec, Tag, ProduitTag, PrestationTag, Collection, CollectionItem, Arrivage
Services / RDVCategorieService, Prestation, PrestationCategorie, ConfigReservationPrestation, CreneauDisponible, PlageIndisponible, Reservation, StatutReservation enum, TypeAcompte, ModeConfirmationRDV
RestaurationConfigRestaurant, MenuSection, Plat, OptionPlat, TableRestaurant, CommandeRepas, LigneCommandeRepas, ModeCommandeRepas/StatutCommandeRepas enums
CommerceClient (entité par tenant, distincte de ClientAccount), Commande, CommandeLigne, Panier, PanierLigne, Wishlist, ModePaiementCommande enum
Vitrine builderVitrineSection, SectionBloc, VitrinePage, VitrineSnapshot, VitrineTemplate, PageType/SnapshotStatut enums
Notification EngineAbonnement (polymorphique : cibleType ∈ tenant/vitrine/produit/client), NotifEvent, NotifDelivery, BroadcastCampagne, NotifCanal/NotifStatut/AudienceSegment enums
StoriesVitrineStory, VitrineStoryView, StoryMediaType enum
Messagerie / Review / ReseauxConversation, Message, TenantQuickReply, Review, Referral, CashbackTransaction
OpsAdminPrefs, SearchQuery, AuditLog

Voir aussi : app/Docs/architecture-gerant.excalidraw (espace gérant — 239 éléments, TabBar dynamique + 4 onglets Réglages + zone WP-228 restau + zone WP-232 Réglages) et architecture-client.excalidraw (5 onglets + flows réservation/repas/contact dynamique).

API design

280 routes au total ; 186 sous /api/mobile/*. Voir api-contracts-backend.md pour la liste détaillée.

Conventions :

  • App Router Route Handlers : src/app/api/<path>/route.ts exporte GET/POST/PATCH/DELETE/PUT/DELETE.
  • Auth uniforme : const session = await getSessionFromRequest(req) au début. Routes publiques mobile bypass via try/catch silencieux ; routes admin web vérifient session.role === "TENANT_ADMIN" ou "SUPER_ADMIN".
  • Validation Zod systématique pour les body POST/PATCH.
  • Réponses JSON standardisées : { ok: true, data } ou { error: "...", code? }. Status HTTP cohérents : 200/201/204 succès, 400/422 validation, 401/403 auth, 404 not found, 409 conflit, 500 serveur.
  • Séparation stricte /api/admin (web cookie) vs /api/mobile (Bearer) — décidée DEC-107 pour ne pas casser le web en touchant le mobile.

Routes pivot :

  • POST /api/auth/client/magic-link — public, génère OTP 6 chiffres + stocke Redis TTL 15min + envoie via Resend
  • POST /api/mobile/auth — public, échange { identifier, otp } ou { identifier, token } → Bearer JWT
  • GET /api/mobile/session — Bearer, retourne { connected, user, tenant }
  • GET /api/mobile/home — public, agrège 4 datasets en 1 requête (Accueil mobile)
  • POST /api/mobile/commandes — Bearer CLIENT, transaction Prisma (decrement stock + push gérant + email)
  • POST /api/mobile/reservations — Bearer CLIENT, transaction (overlap check + create + email iCal)
  • PATCH /api/mobile/vitrine/commandes/[id] — Bearer TENANT_ADMIN, transitions statut + push client + email
  • POST /api/mobile/push-tokens/register — Bearer, discrimine TENANT_ADMIN/CLIENT via session.role pour stocker dans userId OU clientAccountId
  • GET /api/ical/[token] + /api/ical/reservation/[token] — public, feeds .ics (abonnement calendrier)

Authentication flow

[Web]
  POST /api/auth/login (gérant) → bcrypt verify → Set-Cookie superapp_session (jose HS256)
  Cookie partagé .wari.pro via proxy_cookie_path Nginx (SSO sous-domaines)
  Toute route admin : getSessionFromRequest(req) → cookie → JWT.verify

[Mobile]
  POST /api/auth/client/magic-link { identifier } → OTP 6 chiffres en Redis 15min + email
  POST /api/mobile/auth { identifier, otp } → vérifie Redis → Bearer JWT (même secret, même payload)
  Stockage mobile : expo-secure-store + cache mémoire Android (perf)
  Toute route mobile : getSessionFromRequest(req) → Bearer Authorization → JWT.verify

Payload commun : { userId, role: "CLIENT"|"TENANT_ADMIN"|"SUPER_ADMIN", tenantId: string|null }
tenantId=null = contexte marketplace global "wari"

Helper unique lib/auth/session.ts → ne JAMAIS bypasser.

Multi-tenancy

Production : *.wari.pro
  - kady.wari.pro       → resolveTenant(host) → Tenant{ subdomain: "kady" }
  - salon-ouaga.wari.pro → ...
  - wari.pro (apex)     → tenantId=null = contexte marketplace

Dev :
  - localhost:3000?tenant=kady → resolveTenant(query) → Tenant{ subdomain: "kady" }

Toute requête Prisma : where: { tenantId } ou where: { tenant: { subdomain } }

Niveau d'accès vitrine (DEC-211/212/214/215) :

niveauAccesVisible LISTVisible DETAILContenu/actions
PUBLICTout selon ConfigAccesVitrine
CONNEXIONTout — gate visuel sur actions sans compte
SUR_DEMANDEBanner DemandeAcces ; gate via commandeSansCompte etc.
INVITATIONTeaser 200 (slug + logo + nom + description)AccesRequis screen côté mobile

ConfigAccesVitrine (10 booléens) cadre indépendamment ce qu'un visiteur voit (nomsVisibles, photosVisibles, prixVisibles, descriptionsVisibles, stockVisible, coordonneesVisibles) et peut faire (panierSansCompte, commandeSansCompte, reservationSansCompte, contactSansCompte).

Vitrine builder

Architecture snapshot-based :

  1. Gérant édite via /admin/vitrine/builder — état serializé en draft (table VitrineSnapshot.statut=DRAFT).
  2. isDirty calculé client-side avec fast-deep-equal entre draft local et dernier snapshot fetched.
  3. Autosave (debounced) → PATCH le snapshot draft.
  4. Publication → snapshot draft devient PUBLISHED + bump version.
  5. Page publique [slug]/page.tsx lit VitrineSnapshot.statut=PUBLISHED ; ?preview=live force lecture DB live (non snapshot).
  6. Pages applicatives (Boutique, Services) = PageType.APPLICATIVE — pas stockées en snapshot, rendues dynamiquement.

Sections legacy dans vitrine-page.tsx protégées par guards hasBuilderXXXinterdit de supprimer (CLAUDE.md).

Stockage médias

  • MinIO container Docker, données dans ./minio/data/.
  • API uploads : presigned URL via lib/minio/.
  • URLs publiques : toujours via Nginx proxy https://wari.pro/medias/<key>. Ne jamais exposer les URLs internes MinIO.
  • Sharp (sharp@0.34) pour redim + conversion (lib/medias/).

Crons internes

  • lib/cron/rappels-rdv.ts — Notifications J-1 et H-2 avant un RDV CONFIRME.
  • Stories expiration (lecture par expiresAt à chaque GET stories).
  • src/app/api/cron/* — endpoints crons (auth via CRON_SECRET header).
  • Backups disque : scripts/wari-backup.cron + backup-postgres.sh + backup-minio.sh.

Sécurité (P0 hardening 2026-05-20)

  • JWT fail-closed sur erreur de vérification (avant : fallback silencieux).
  • Open Redirect fix (Location header sanitisé).
  • OTP rate-limit Redis (3 tentatives / 5 min).
  • MIME allowlist sur upload (anti-injection).
  • 8 indexes Prisma ajoutés (perf + sec).
  • Vitest v3 + 8 tests lib/ + ESLint bloquant en CI.
  • Dockerfile prod multi-stage non-root.
  • Ports loopback dans docker-compose.yml (postgres, redis, minio, nextjs, typesense tous bindés 127.0.0.1).
  • Headers Nginx sécurité centralisés via snippets/security-headers.conf (HSTS preload + X-Content-Type-Options + Permissions-Policy + ...). Voir app/infra/nginx/.
  • Backups : runbook app/docs/runbook-backups.md.

Source tree

Voir source-tree-analysis.md section "Part backend".

Development workflow

Voir development-guide-backend.md.

Deployment

Voir deployment-guide.md.

Testing strategy

Cible : tests des helpers métier critiques dans app/src/lib/__tests__/ via Vitest 3. État actuel : 8 tests lib/ (post-hardening). Phase A (couverture API routes) à 0% — référencé dans audit 2026-05-20.

cd app
npm run test           # vitest run
npm run test:watch     # vitest
npm run test:coverage  # vitest run --coverage

Pas de tests E2E backend automatisés en CI — validation manuelle via cURL en fin de sprint (pattern documenté dans CLAUDE.md : "Tests cURL E2E n/n OK").