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égorie | Technologie | Version | Pourquoi ce choix |
|---|---|---|---|
| Framework | Next.js | 16.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. |
| Langage | TypeScript | 5.x strict | Typage des contrats API et types Prisma générés |
| ORM | Prisma | 7.6 + @prisma/adapter-pg | Modèles typés, migrations versionnées, adapter pg natif |
| DB | PostgreSQL | 15-alpine | JSONB (vitrine snapshots), CHECK constraints (XOR PushToken), indexes optimisés (P0 hardening) |
| Cache | Redis | 7-alpine (ioredis) | OTP TTL 15min, rate-limit, cache requêtes lourdes |
| Object storage | MinIO | latest | Self-hosted S3-compatible, proxié Nginx sur /medias/ |
| Auth | jose (JWT) | 6.x | JWT HS256 custom — secret unique NEXTAUTH_SECRET, cookie httpOnly + Bearer mobile. Pas NextAuth malgré next-auth@5.0.0-beta.30 listé. |
| Validation | Zod | 4.x | Validation entrées API + types inférés |
| Resend | 6.x | Transactionnel (commandes, paiements, RDV, accès) | |
| SMS | Vonage | 3.x | Partiel V1 (envoi OTP, message commande) |
| Push | expo-server-sdk | 6.x | Envoi push expo-notifications (FCM + APNs) |
| Search | Typesense | 0.25.2 (Docker) | Search image V2 Option A (vector search + texte) |
| AI | @google/generative-ai (Gemini) + Anthropic Vision | latest | Snap-to-list OCR (Gemini), recherche par image V1 (Claude) |
| Embeddings | HuggingFace CLIP | API | Vector embeddings pour Typesense |
| Analytics | PostHog | 1.372 (client) | Tracking funnel, events |
| Monitoring | Sentry | 10.53 | @sentry/nextjs server + client + instrumentation.ts |
| State (admin) | Zustand 5 + Immer | latest | Stores builder, panels, ... |
| Drag&drop | @dnd-kit | 6/10/3 | Builder visuel + tri colonnes |
| Style | Tailwind | 4 (PostCSS plugin) | Utility-first |
| Tests | Vitest | 3.2 + @vitest/coverage-v8 | Tests lib/ (P0 hardening : 8 tests existants) |
| Crypto | bcryptjs | 3 | Hash mdp gérants |
| Crypto B | crypto-js | 4 | Tokens iCal, magic link |
| Image | sharp | 0.34 | Conversion + 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 OUAuthorization: Bearer. Le payload est identique. Le secret est unique. - Route Handlers App Router :
src/app/api/**/route.tsexporteGET/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/) : tableNotifEvent+ tableNotifDelivery+ enumNotifCanal. Dispatch via abonnements (Abonnementpolymorphic viacibleType+cibleId). - Snapshot-based vitrine builder :
VitrineSnapshot.statut(DRAFT/PUBLISHED) +VitrinePage.type(APPLICATIVE/STATIQUE).isDirtycalculé client-side viafast-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 :
| Famille | Models clés |
|---|---|
| Tenant + accès | Tenant, 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) |
| Catalogue | Categorie, Produit, ProduitCategorie, Media, MediaAlbum, MediaAlbumMembership, ModeleProduit, ModeleAttribut, VarianteProduit, VariantePrestation, ModeleSpec, ProduitSpec, Tag, ProduitTag, PrestationTag, Collection, CollectionItem, Arrivage |
| Services / RDV | CategorieService, Prestation, PrestationCategorie, ConfigReservationPrestation, CreneauDisponible, PlageIndisponible, Reservation, StatutReservation enum, TypeAcompte, ModeConfirmationRDV |
| Restauration | ConfigRestaurant, MenuSection, Plat, OptionPlat, TableRestaurant, CommandeRepas, LigneCommandeRepas, ModeCommandeRepas/StatutCommandeRepas enums |
| Commerce | Client (entité par tenant, distincte de ClientAccount), Commande, CommandeLigne, Panier, PanierLigne, Wishlist, ModePaiementCommande enum |
| Vitrine builder | VitrineSection, SectionBloc, VitrinePage, VitrineSnapshot, VitrineTemplate, PageType/SnapshotStatut enums |
| Notification Engine | Abonnement (polymorphique : cibleType ∈ tenant/vitrine/produit/client), NotifEvent, NotifDelivery, BroadcastCampagne, NotifCanal/NotifStatut/AudienceSegment enums |
| Stories | VitrineStory, VitrineStoryView, StoryMediaType enum |
| Messagerie / Review / Reseaux | Conversation, Message, TenantQuickReply, Review, Referral, CashbackTransaction |
| Ops | AdminPrefs, 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.tsexporteGET/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érifientsession.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 ResendPOST /api/mobile/auth— public, échange{ identifier, otp }ou{ identifier, token }→ Bearer JWTGET /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 + emailPOST /api/mobile/push-tokens/register— Bearer, discrimine TENANT_ADMIN/CLIENT via session.role pour stocker dans userId OU clientAccountIdGET /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) :
| niveauAcces | Visible LIST | Visible DETAIL | Contenu/actions |
|---|---|---|---|
| PUBLIC | ✅ | ✅ | Tout selon ConfigAccesVitrine |
| CONNEXION | ✅ | ✅ | Tout — gate visuel sur actions sans compte |
| SUR_DEMANDE | ✅ | ✅ | Banner DemandeAcces ; gate via commandeSansCompte etc. |
| INVITATION | ❌ | Teaser 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 :
- Gérant édite via
/admin/vitrine/builder— état serializé en draft (tableVitrineSnapshot.statut=DRAFT). isDirtycalculé client-side avecfast-deep-equalentre draft local et dernier snapshot fetched.- Autosave (debounced) → PATCH le snapshot draft.
- Publication → snapshot draft devient
PUBLISHED+ bump version. - Page publique
[slug]/page.tsxlitVitrineSnapshot.statut=PUBLISHED;?preview=liveforce lecture DB live (non snapshot). - Pages applicatives (Boutique, Services) =
PageType.APPLICATIVE— pas stockées en snapshot, rendues dynamiquement.
Sections legacy dans vitrine-page.tsx protégées par guards hasBuilderXXX — interdit 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 RDVCONFIRME.- Stories expiration (lecture par
expiresAtà chaque GET stories). src/app/api/cron/*— endpoints crons (auth viaCRON_SECRETheader).- 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 (
Locationheader 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és127.0.0.1). - Headers Nginx sécurité centralisés via
snippets/security-headers.conf(HSTS preload + X-Content-Type-Options + Permissions-Policy + ...). Voirapp/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 --coveragePas 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").