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/web

    l'application Next.js (pages, composants, client tRPC)

    • src/app

      les routes : (public) pour le site, (admin)/dashboard pour le tableau de bord, docs pour cette documentation

    • src/features

      le code rangé par fonctionnalité (admin, finder, page-builder, tours…)

    • src/proxy.ts

      le filtre d'authentification qui garde /dashboard et /profil

    • content/docs

      les pages de documentation en MDX (guide, admin, dev)

  • packages/backend

    l'API : routeurs tRPC, accès base, e-mails, stockage

    • src/modules

      un dossier par domaine (posts, disciplines, storage, tickets…)

    • src/trpc

      la construction de tRPC : contexte, procédures publiques, protégées, admin

  • packages/contracts

    les types et schémas partagés par le front et le back

  • packages/finder-core

    le moteur du finder (l'explorateur de fichiers), indépendant du stockage

  • prisma

    le schéma de la base (47 modèles) et ses migrations

  • scripts

    les vérifications lancées par pnpm check

  • docker-compose.dev.yml

    la base, le stockage et la boîte mail de développement

  • Dockerfile

    l'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.

Le trajet d'une action#

Suivons ce qui se passe quand un administrateur publie une actualité. Ce trajet reviendra dans presque chaque partie.

  1. Le formulaire vit dans apps/web. À l'envoi, il appelle trpc.post.create : un appel de fonction typé, pas une URL écrite à la main.
  2. La requête arrive au routeur post de packages/backend. Le middleware isAdmin vérifie la session ; Zod valide le type du titre et du contenu.
  3. Prisma écrit l'article dans PostgreSQL, dans une transaction qui enregistre aussi les médias qu'il référence.
  4. 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 ».

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.

Sur cette page