Avant de commencer
Ce que fait le projet, comment il est rangé, le vocabulaire du club, et la règle qui sépare le développement de la production.
Une vitrine et un outil dans la même application#
L'Association Kung Fu Chambéry enseigne plusieurs arts martiaux dans les locaux de la MJC de Chambéry. Son application répond à deux besoins.
Le premier est une vitrine : un visiteur arrive sur akfc.fr et veut savoir quelles disciplines sont enseignées, à quelle heure, par qui, et quand aura lieu le prochain stage.
Le second est un outil : les administrateurs publient des actualités, rangent des photos, tiennent la liste des membres, leur transmettent des documents et répondent à leurs demandes ; les membres consultent leurs documents et partagent des fichiers dans les espaces de leurs groupes.
Les deux vivent dans une seule application, qui partage la base, les médias et l'authentification. Ce choix permet qu'une photo validée dans la bibliothèque apparaisse dans une galerie publique sans être copiée, qu'un compte serve aussi bien sur le site que dans le tableau de bord, et qu'une correction de modèle profite aux deux côtés à la fois.
Il a une conséquence que nous retrouverons dans chaque partie : dans une même arborescence cohabitent des pages que tout internet peut lire et des pages qui contiennent des données personnelles. Savoir où passe la frontière entre les deux, et qui la garde, est une compétence centrale sur ce projet.
L'idée directrice : une seule source de vérité, servie à deux publics. Chaque fois qu'une donnée existe en double dans le projet, c'est une exception, et un commentaire la justifie.
Comment lire cette documentation#
Les parties suivent l'ordre dans lequel un développeur découvre le projet : l'installer, apprendre à travailler avec, puis descendre couche par couche (le front, les données, l'API, l'authentification, le stockage) jusqu'au déploiement.
Chaque chapitre part d'un besoin concret, présente le choix retenu et ce qu'il permet, puis montre le code qui l'implémente. Les encadrés résument ce qu'il faut retenir ; la fin de chaque chapitre annonce la question que traite le suivant.
Le code montré est celui du dépôt. Quand un extrait est raccourci, il le dit.
Gardons le dépôt ouvert à côté. Chaque chemin cité s'ouvre dans l'éditeur : c'est la meilleure façon de vérifier que nous avons compris.
La carte du dépôt#
Le projet est un monorepo géré par pnpm : un seul dépôt Git, plusieurs paquets qui se connaissent par leur nom.
apps/webl'application Next.js (pages, composants, client tRPC)
src/apples routes : (public) pour le site, (admin)/dashboard pour le tableau de bord, docs pour cette documentation
src/featuresle code rangé par fonctionnalité (admin, finder, page-builder, tours…)
src/proxy.tsle filtre d'authentification qui garde /dashboard et /profil
content/docsles pages de documentation en MDX (guide, admin, dev)
packages/backendl'API : routeurs tRPC, accès base, e-mails, stockage
src/modulesun dossier par domaine (posts, disciplines, storage, tickets…)
src/trpcla construction de tRPC : contexte, procédures publiques, protégées, admin
packages/contractsles types et schémas partagés par le front et le back
packages/finder-corele moteur du finder (l'explorateur de fichiers), indépendant du stockage
prismale schéma de la base (47 modèles) et ses migrations
scriptsles vérifications lancées par pnpm check
docker-compose.dev.ymlla base, le stockage et la boîte mail de développement
Dockerfilel'image de production
Une règle découpe ce territoire : apps/web affiche, packages/backend décide. Le front ne parle jamais à la base ; il appelle une procédure tRPC, et c'est le backend qui vérifie les droits, valide l'entrée, lit ou écrit. Les types qui circulent entre les deux vivent dans packages/contracts. Ce découpage permet qu'un changement d'un côté casse la compilation de l'autre, et donc qu'une incompatibilité soit détectée sur notre machine plutôt que devant un membre.
La pile technique#
Couche | Outil | Ce qu’il apporte au projet |
|---|---|---|
Application | Next.js 16 (App Router), React 19 | Pages rendues côté serveur pour le site public, composants interactifs pour le tableau de bord, dans une seule application. |
Langage | TypeScript 5.9 | Un seul langage typé du schéma de base jusqu’au composant : une incompatibilité se voit à la compilation. |
API | tRPC 11, Zod 4 | Des appels de fonctions typés entre front et back, sans URL ni contrat à maintenir à la main ; validation des entrées. |
Données | Prisma 6, PostgreSQL | Un client généré depuis le schéma, des migrations versionnées, des transactions. |
Fichiers | Stockage compatible S3 (Cloudflare R2, MinIO en local) | Des fichiers envoyés directement par le navigateur, avec une autorisation signée par le backend. |
État client | React Query 5, Zustand 5 | Cache des requêtes et invalidation après mutation ; petit état partagé (la session). |
Interface | Tailwind CSS 4, Tiptap 3 | Styles par classes utilitaires et thème clair / sombre ; éditeur de texte riche. |
Outillage | pnpm 10 (monorepo), Docker | Paquets partagés entre applications ; même environnement en local et en production. |
Showing entries1to8entries
Le trajet d'une action#
Suivons ce qui se passe quand un administrateur publie une actualité. Ce trajet reviendra dans presque chaque partie.
- Le formulaire vit dans
apps/web. À l'envoi, il appelletrpc.post.create: un appel de fonction typé, pas une URL écrite à la main. - La requête arrive au routeur
postdepackages/backend. Le middlewareisAdminvérifie la session ; Zod valide le type du titre et du contenu. - Prisma écrit l'article dans PostgreSQL, dans une transaction qui enregistre aussi les médias qu'il référence.
- Le front invalide son cache : la liste des actualités se recharge d'elle-même.
Les fichiers prennent un autre chemin, détaillé dans la partie « Stockage et médias ». Un fichier lourd ne transite pas par notre API : le backend délivre une autorisation signée, et le navigateur envoie le fichier directement au stockage compatible S3 (Cloudflare R2 en production, MinIO sur notre machine). Le serveur n'a ainsi jamais à tenir un fichier de plusieurs dizaines de mégaoctets en mémoire.
Deux circuits : les données passent par tRPC et Prisma, les fichiers vont directement au stockage avec une autorisation signée. Il n'y a pas d'upload dans les routeurs tRPC.
Le vocabulaire du club#
Le code emploie le vocabulaire du club, et certains mots y ont un sens précis.
Mot dans l’interface | Modèle Prisma | Ce que c’est |
|---|---|---|
Discipline | Discipline | Un art martial enseigné, au sens conceptuel (ex. Tchoy Lee Fut), indépendant des dates et des lieux. |
Famille | DisciplineFamily | Un regroupement de disciplines proches, utilisé par le menu « Nos disciplines ». |
Catégorie | Category | La plus grosse maille de classement (ex. « Cours ») ; elle range aussi la médiathèque. |
Origine | Origin | L’origine culturelle d’une discipline, d’un stage ou d’un événement (ex. Chine, Philippines). |
Cours | Course | Un créneau HEBDOMADAIRE : un jour, une heure, un public. Pas une séance datée. |
Stage | Seminar | Un séminaire ponctuel, avec ses séances datées (SeminarSession). |
Événement | Event | Un rendez-vous plus léger : repas, portes ouvertes, forum ; ses dates vivent dans ses séances (EventSession). |
Groupe | MemberGroup | Un ensemble de membres ; s’il est collaboratif, il a un espace de fichiers partagé. Les groupes forment une hiérarchie. |
Document | MemberDocument | Un fichier du club publié aux membres, avec un suivi de lecture par membre. |
Média | MediaAsset | Un fichier de la bibliothèque, avec son statut : en attente, publié, à la corbeille. |
Ticket | Ticket | Une demande adressée au club, numérotée (AKFC-0042), avec son fil de messages. |
Page éditoriale | SitePage, PageVisibility | Une page au contenu libre (À propos, Contacts) qu’un admin publie ou laisse « en construction ». |
Showing entries1to12entries
Le point le plus souvent mal compris : un cours n'a pas de date. C'est « le mardi à 20 h ». Une séance précise n'existe que pour un stage ou un événement.
Trois environnements, une règle#
Le projet tourne à trois endroits.
La production vit sur un serveur Hetzner, dans des conteneurs Docker, et sert akfc.fr. Ses données sont celles de vrais membres. La sandbox recruteurs est une copie autonome, lancée par docker-compose.sandbox.yml, avec des comptes et des contenus de démonstration. Le développement se fait sur notre machine, avec sa propre base, son propre stockage et sa propre boîte mail.
Cette séparation est stricte. Elle permet d'essayer une migration, de lancer un script de nettoyage ou de réinitialiser des mots de passe de démonstration sans qu'aucune de ces opérations puisse atteindre un membre réel. Elle permet aussi de détruire et reconstruire l'environnement de développement à volonté.
Le développement ne touche jamais aux données de production. Notre base locale part de zéro, se remplit de données de démonstration, et peut être détruite à tout moment. Si une commande demande l'adresse du serveur de production, nous sommes au mauvais endroit.
À suivre#
Nous savons ce que fait le projet, comment il est rangé et ce qu'il ne doit jamais toucher. Reste à le faire tourner.
Un pnpm install suivi de pnpm dev ne suffit pas : dès la première page, l'application interroge une base de données, demande son logo à un stockage S3, et s'attend à pouvoir envoyer des e-mails. Aucun de ces services n'existe encore sur notre machine.
Chapitre suivant : la mise en route. Trois services locaux qui parlent les mêmes protocoles que la production, deux fichiers de configuration pour deux lecteurs différents, et une boîte mail qui reçoit tout sans rien envoyer.