📚 Documentation CerOps
Ce dossier contient la documentation fonctionnelle et technique du projet CerOps.
Objectif
Centraliser la vision produit, les choix techniques et les règles métier afin de :
- faciliter le développement
- aligner l’équipe
- préparer les livrables (école / pitch / démo)
Structure
parcelles.md→ gestion des parcelles agricolesagriculteurs.md→ gestion des utilisateurs agriculteurspilotes-drone.md→ gestion des pilotes de dronemarketplace.md→ mise en relation agriculteurs ↔ pilotesimagerie-parcellaire.md→ traitement et exploitation des imagesplan-actions.md→ recommandations et suivi d’actionsmobile-map-offline.md→ fonctionnement mobile + offlineapi-contrats.md→ contrats entre frontend et backend
Règles
- 1 fichier = 1 domaine
- Utiliser le template commun
- Mettre à jour la doc en même temps que le code
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
Capteurs marketplace
Contexte
Modele de transition
Les types de capteurs sont persistés dans la table sensor_kind.
Chaque entree utilise un code stable :
RGBMULTISPECTRALTHERMALLIDAR
Les capteurs declares sur un profil pilote referencent sensor_kind via sensor_kind_code.
Les missions declarent leurs capteurs requis via la relation explicite mission_required_sensor.
Les contrats API peuvent exposer des champs derives comme type ou requiredSensors pour l’ergonomie des clients, mais la source de verite est la base de donnees.
Donnees initiales
La migration 20260428112000_persist_sensor_kinds cree les capteurs standards, puis 20260428114500_remove_sensor_type_enum supprime l’ancien enum et les colonnes associees.
Les procedures API qui creent ou synchronisent des capteurs appellent aussi un helper d’amorcage idempotent, afin de garder les environnements de developpement initialises avec db push utilisables.
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
Pricing
Contexte
Le pricing actuel des missions est calcule cote serveur dans packages/api/src/routers/missions/pricing.ts.
Le calcul est volontairement simple et deterministe :
- une configuration globale de mission fournit les montants communs a toutes les missions ;
- chaque option d’analyse peut ajouter un forfait fixe et/ou un prix variable par hectare ;
- un minimum de mission peut relever le total final si le sous-total est trop bas.
L’objectif est de conserver le serveur comme source de verite du prix et de produire un detail exploitable pour l’UI, l’audit et les tests.
Donnees d’entree
Le calcul repose sur deux objets :
1. MissionPricingInput
totalSurfaceSurface totale de la mission.analysisOptionsListe des options d’analyse selectionnees.
Chaque option d’analyse contient :
idcodenamebasePricepricePerHa
2. MissionPricingConfig
La configuration globale de pricing contient :
idnamemissionBasePriceForfait applique une fois par mission.missionPricePerHaPrix de base applique par hectare.minimumMissionPriceMontant minimum facture pour la mission.
Algorithme actuel
La fonction calculateMissionPrice(input, pricingConfig) applique les regles suivantes dans cet ordre :
- Validation des donnees
- Ajout du forfait mission
- Ajout du prix mission par hectare
- Ajout des lignes de chaque option d’analyse
- Calcul du sous-total
- Ajout eventuel d’un ajustement de minimum
- Calcul du total final
1. Validation
Le calcul rejette :
- une
totalSurfacenegative ou non finie ; - un montant negatif ou non fini dans la configuration globale ;
- un montant negatif ou non fini sur une option d’analyse.
null et undefined sont acceptes pour basePrice et pricePerHa sur les options. Ils sont traites comme 0.
2. Forfait mission
Si missionBasePrice > 0, une ligne est ajoutee :
quantity = 1unitPrice = missionBasePriceamount = missionBasePrice
3. Prix mission par hectare
Si missionPricePerHa > 0, une ligne est ajoutee :
quantity = totalSurfaceunitPrice = missionPricePerHaamount = missionPricePerHa * totalSurface
4. Options d’analyse
Pour chaque option :
- si
basePrice > 0, une ligne forfaitaire est ajoutee ; - si
pricePerHa > 0, une ligne surfacique est ajoutee.
Autrement dit, la formule d’une option est :
optionTotal = (basePrice ?? 0) + (pricePerHa ?? 0) * totalSurface
5. Sous-total
Le sous-total correspond a la somme des amount de toutes les lignes generees avant application du minimum.
6. Minimum mission
Si minimumMissionPrice > subtotal, une ligne supplementaire est ajoutee pour combler l’ecart :
minimumAdjustment = minimumMissionPrice - subtotal
Cette ligne ne remplace pas les autres. Elle s’ajoute au detail pour rendre l’ajustement visible.
7. Arrondi
Chaque montant est arrondi a 2 decimales via roundMoney.
Le total final est lui aussi recalcule a partir des lignes et arrondi a 2 decimales.
Structure de sortie
La fonction retourne :
totalPricelineItems
Chaque lineItem contient :
typecodelabelquantityunitPriceamount
Role de type et code
Aujourd’hui, type et code ne configurent pas le prix. Ils decrivent les lignes produites par le calcul.
type
type categorise la nature metier de la ligne :
mission_baseforfait global de mission ;mission_surfaceprix de base par hectare ;mission_minimum_adjustmentajustement applique pour atteindre le minimum ;analysis_optionligne issue d’une option d’analyse.
type est utile pour :
- grouper ou afficher les lignes dans le front ;
- reconnaitre les categories de cout dans les tests ;
- garder une semantique stable cote API.
Mais type n’est pas lu comme une regle dynamique de pricing. Il est derive par le code, pas saisi en base pour piloter l’algorithme.
code
code identifie plus precisement la ligne.
Pour les lignes globales de mission, les codes sont statiques :
mission-basemission-surfacemission-minimum-adjustment
Pour les options d’analyse, le code est derive du code de l’option :
${analysisOption.code}:base${analysisOption.code}:surface
Exemples :
hydrometrie:basehydrometrie:surfaceazote:base
code sert donc a :
- rattacher une ligne a une option precise ;
- differencier la part forfaitaire et la part surfacique d’une meme option ;
- produire un identifiant stable pour le recapitulatif ou d’eventuels traitements aval.
Comme type, code est aujourd’hui une sortie descriptive, pas un parametre de configuration du calcul.
Ce qui configure reellement le prix aujourd’hui
Les vraies entrees de configuration sont :
- dans
MissionPricingConfigmissionBasePrice,missionPricePerHa,minimumMissionPrice; - dans
AnalysisOptionbasePrice,pricePerHa; - dans l’entree de calcul
totalSurfaceet la liste des options selectionnees.
En pratique :
- changer
missionBasePricechange la lignemission_base; - changer
missionPricePerHachange la lignemission_surface; - changer
minimumMissionPricechange l’eventuelle lignemission_minimum_adjustment; - changer
analysisOption.basePriceouanalysisOption.pricePerHachange les lignesanalysis_optioncorrespondantes ; - changer
analysisOption.codene change pas la formule, mais change lecoderetourne dans les lignes ; - changer
analysisOption.namene change pas la formule, mais change lelabelretourne.
Exemple complet
Avec :
totalSurface = 12.5missionBasePrice = 50missionPricePerHa = 2minimumMissionPrice = 0- option
HydrometriebasePrice = 120,pricePerHa = 4.5,code = hydrometrie - option
AzotebasePrice = 90,pricePerHa = 3.2,code = azote
Le detail produit est :
mission_basecodemission-basemontant50mission_surfacecodemission-surfacemontant25analysis_optioncodehydrometrie:basemontant120analysis_optioncodehydrometrie:surfacemontant56.25analysis_optioncodeazote:basemontant90analysis_optioncodeazote:surfacemontant40
Total :
50 + 25 + 120 + 56.25 + 90 + 40 = 381.25
Cycle de vie de la configuration globale
L’administration du pricing global passe par packages/api/src/routers/pricing/router.ts.
Une configuration :
- est creee en
DRAFT; - peut etre modifiee tant qu’elle reste en
DRAFT; - peut etre publiee ;
- peut etre archivee.
Contraintes actuelles :
- seule une configuration
DRAFTpeut etre modifiee ; - publier une configuration archive automatiquement toute configuration deja
PUBLISHED; - une configuration
ARCHIVEDne peut plus etre re-archivee utilement.
Les schemas d’entree packages/api/src/schemas/pricing/schemas.ts imposent :
- un
namenon vide, max 120 caracteres ; - des montants numeriques positifs ou nuls ;
- des montants exprimes en EUR avec jusqu’a 2 decimales selon la convention documentee.
Limites du modele actuel
Le modele actuel fonctionne bien pour un pricing compose de :
- forfait mission ;
- prix mission par hectare ;
- forfait par option ;
- prix par hectare par option ;
- minimum de mission.
En revanche, type et code ne permettent pas a eux seuls de rendre le pricing dynamique. Ils restent de simples marqueurs de sortie.
Si demain le projet veut rendre le modele tarifaire reellement configurable, il faudra introduire un moteur de regles versionne en base, avec des kind metier interpretes par le serveur, plutot que de transformer type ou code en champs libres pilotant le calcul.
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
🎯 Stratégie Offline Mobile
Contexte
L’application mobile CerOps est utilisée en plein champ, souvent sans connexion réseau. Le mode offline permet de consulter et modifier les données sans internet, puis de synchroniser au retour de la connectivité.
Objectif
Permettre à l’utilisateur de travailler hors ligne sur ses parcelles et actions, avec synchronisation automatique à la reconnexion.
Utilisateurs concernés
- Agriculteur
- Pilote de drone
Fonctionnalités (User Stories)
- En tant qu’agriculteur, je veux consulter mes parcelles et actions sans connexion.
- En tant qu’agriculteur, je veux créer et modifier des actions hors ligne, afin qu’elles soient synchronisées à la reconnexion.
- En tant qu’utilisateur, je veux télécharger une zone de carte à l’avance pour naviguer hors ligne.
- En tant qu’utilisateur, je veux savoir quelles données ne sont pas encore synchronisées.
Données manipulées
Ce qui est mis en cache offline
| Donnée | Faisabilité | Stratégie |
|---|---|---|
| Parcelles (métadonnées) | ✅ Oui | Cache complet |
| Actions / tâches | ✅ Oui | Cache complet + mise à jour optimiste |
| Missions de vol (métadonnées) | ✅ Oui | Cache métadonnées uniquement |
| Tuiles de carte | ✅ Oui (par zone) | Téléchargement à la demande (FMTC) |
| Imagerie NDVI | ⚠️ Sélectif | Miniatures uniquement, à la demande |
| Images brutes drone | ❌ Non | Serveur uniquement |
Champs offline ajoutés à chaque entité
needsSync(bool) : modification locale en attente de synchronisationlastModifiedAt(DateTime) : horodatage pour la résolution de conflits
Choix technologique
Base de données locale : Drift
Drift (SQLite ORM) est retenu pour les raisons suivantes :
- Requêtes filtrées typées à la compilation (SQL)
- Streaming natif compatible Riverpod (
watch()→StreamProvider) - Migrations de schéma versionnées — évolutions du modèle sans perte de données
- Transactions atomiques pour la cohérence lors des synchronisations
- Multi-plateforme : Android, iOS, Web, Desktop
Hive (clé-valeur) a été écarté : pas de migrations natives, pas de streaming filtré, scan mémoire O(n) pour les requêtes.
Détection de connectivité : connectivity_plus
Détecte les changements Wi-Fi / données mobiles. Note : connexion détectée ≠ internet disponible — tous les appels API restent protégés par timeout.
Cartes offline : flutter_map_tile_caching (FMTC)
Téléchargement de tuiles par zone géographique délimitée. Zoom 15–17, environ 15–30 Mo par zone de 5 km². Expiration à 30 jours.
Licence GPL — à vérifier avant intégration en production.
Résolution de conflits
Stratégie : Last-Write-Wins basée sur lastModifiedAt.
- Modification locale plus récente → envoyée au serveur
- Version serveur plus récente → écrase la version locale
- En cas d’égalité → version serveur prime
Écrans / UX
- Bannière discrète quand l’application est hors ligne
- Indicateur visuel sur les éléments avec
needsSync: true - Notification au retour de la connectivité : « X éléments synchronisés »
- Écran de téléchargement de zone carte (accessible depuis les paramètres)
Cas limites
- Premier démarrage sans connexion : impossible — le cache est vide, l’utilisateur doit se connecter une première fois.
- Session expirée hors ligne : l’utilisateur travaille jusqu’à expiration du token, aucune re-authentification possible sans réseau.
- Stockage insuffisant : erreur explicite, téléchargement annulé.
- Entité référencée absente du cache : placeholder affiché, pas d’erreur fatale.
- Évolution du schéma : migration Drift, aucune perte de données.
Critères d’acceptation
- L’application démarre sans connexion et affiche les données du cache.
- Les parcelles et actions sont lisibles hors ligne.
- Une action créée hors ligne est sauvegardée localement et synchronisée à la reconnexion.
- Le statut d’une action modifié hors ligne est mis à jour immédiatement (optimiste) et synchronisé à la reconnexion.
- Les éléments non synchronisés sont visuellement distingués.
- Une zone de carte peut être téléchargée et consultée hors ligne.
- Une mise à jour de l’application ne perd pas les données locales.
Dépendances
Packages Flutter
| Package | Usage |
|---|---|
drift + drift_flutter | Base de données locale SQLite |
drift_dev + build_runner | Génération de code (dev uniquement) |
connectivity_plus | Détection de connectivité |
flutter_map_tile_caching | Cache de tuiles carte (GPL) |
Backend
- Aucun endpoint spécifique offline requis au MVP.
- Chaque entité doit exposer
updatedAtpour la résolution de conflits LWW. - Post-MVP : endpoint delta-sync (entités modifiées depuis un timestamp).
MVP vs Post-MVP
MVP
- Cache Drift pour parcelles et actions (lecture + écriture hors ligne)
- Synchronisation manuelle au retour de la connexion (pull-to-refresh / démarrage)
- Mise à jour optimiste pour les actions
- Indicateur visuel offline + éléments en attente de sync
- Téléchargement de tuiles par zone (FMTC)
- Résolution de conflits Last-Write-Wins
Post-MVP
- Synchronisation automatique en arrière-plan (workmanager, toutes les 20 min)
- Delta-sync depuis un timestamp
- Cache miniatures NDVI à la demande
- Gestion automatique de l’espace disque
- Notification push pour déclencher une sync depuis le serveur
🎯 [Nom du module]
Contexte
Décrire le rôle de ce module dans CerOps.
Objectif
Quel problème ce module résout ?
Utilisateurs concernés
- Agriculteur
- Pilote de drone
- Admin
- Autre ?
Fonctionnalités (User Stories)
- En tant que …, je veux …, afin de …
Données manipulées
- Entités
- Champs importants
- Relations
API / Interfaces
- Endpoints concernés
- Inputs / outputs
Écrans / UX
- Pages / composants liés
- Comportements attendus
Cas limites
- Offline ?
- Erreurs ?
- Données manquantes ?
Critères d’acceptation
- …
- …
Dépendances
- Backend
- Mobile
- Drone
- Autres modules
MVP vs Post-MVP
MVP
- …
Post-MVP
- …
Import RPG
Les données géométriques des parcelles de la France entière sont disponibles via le RPG, maintenu par le Ministère de l’Agriculture.
Chaque année, une nouvelle version est publiée sous forme d’archives .7z découpées en plusieurs parties (ex. RPG_2024_Ile-de-France.7z.001, RPG_2024_Ile-de-France.7z.002, etc.).
Chaque archive contient un fichier GeoPackage (.gpkg) avec les données parcellaires d’une région spécifique.
Concepts métier
| Terme | Signification |
|---|---|
| Dataset | Collection annuelle (ex. “RPG 2024”). Statuts : DRAFT → COMPLETE → ACTIVE → ARCHIVED. |
| Job | Import d’une région dans un dataset (ex. “RPG 2024 — Ile-de-France”). |
| Région | Une des 13 régions administratives françaises, codées en dur dans regions.ts. |
| Activation | Passage de isActiveRpg = true sur les parcelles issues des jobs complétés. Un seul dataset ACTIVE à la fois. |
| Multipart 7z | Archives découpées en .7z.001, .7z.002, etc. Téléchargées séquentiellement, extraites comme une seule archive. |
Flux
Admin UI → API (démarrer import) → file pg-boss → Worker
events.emit(RPG_IMPORT_DISPATCH) ├─ Téléchargement des fichiers sources
├─ Extraction 7z → recherche rpg_parcelles.gpkg
├─ Lecture GeoPackage (SQLite via bun:sqlite)
└─ Upsert des parcelles par lots de 500 (SQL brut)
Machine à états du workflow d’import
┌─────────────────────────────────────┐
│ ▼
PENDING → DOWNLOADING → READY_TO_EXTRACT → EXTRACTING → PARSING → TRANSFORMING → IMPORTING → COMPLETED → ACTIVATED
│ │ │ │ │ │ │ │
│ └───────────────────────────────────────────────────────────────────────► FAILED │
│ │ │ │
└────────────────────────────┴────────────────────────────────────────────────────► CANCELLED │
▼
CANCELLED
FAILED est relançable quand :
- Le statut est
READY_TO_EXTRACT, ou - Le statut est
FAILEDet toutes les parties sont téléchargées (downloadedPartCount >= expectedPartCount), ou - Le statut est
PENDING(job bloqué avant le démarrage du pipeline)
Mode DIRECT : saute READY_TO_EXTRACT — après le téléchargement, le pipeline continue directement vers EXTRACTING.
Mode MULTIPART_7Z : se met en pause à READY_TO_EXTRACT et attend qu’un admin déclenche manuellement l’étape d’extraction.
Il y a 2 modes d’import :
DIRECTetMULTIPART_7Z. Le mode est déterminé automatiquement par le nombre de parties dans l’archive.DIRECTest utilisé pour les imports avec une seule archive.7z, tandis queMULTIPART_7Zest utilisé pour les imports avec des archives découpées en plusieurs parties.
Modules clés
transitionRpgImportJob(jobId, to, extraFields?)
Point d’entrée unique pour tous les changements de statut d’un job. Récupère le statut courant, valide la transition contre la machine à états, écrit de manière atomique. failRpgImportJob et completeRpgImportJob passent par cette fonction.
JobWorkspace
Propriétaire de tous les chemins filesystem du répertoire de travail d’un job :
workspace.dir— racine ($RPG_IMPORT_WORKDIR/<jobId>/)workspace.downloadsDir— parties d’archive téléchargéesworkspace.extractDir— GeoPackage extraitworkspace.cleanup()— supprime tout le workspaceworkspace.cleanupExtracted()— supprime uniquement les artefacts extraits (conserve les téléchargements pour une nouvelle tentative)
Créé une seule fois dans pipeline.ts au démarrage du job, transmis aux fonctions d’archive.
logImportStep(jobId, step, message, level?)
Fonction simple. Écrit une ligne RpgImportLog. Niveau par défaut : INFO. Appelée depuis le pipeline et le geopackage-importer — pas d’instanciation de classe ni d’injection.
flushImportChunk(rows)
Exécute un pipeline PostgreSQL à 8 CTEs :
- input — liaison des valeurs de ligne
- prepared — cast des types, décodage WKB hex → géométrie PostGIS (SRID 2154)
- normalized —
ST_MakeValidsur les géométries invalides →ST_CollectionExtract(3)→ST_Multi - deduplicated —
GROUP BY rpgSourceKey,ST_UnaryUnionpour les géométries multi-parties - annotated — marquage
isEmpty,alreadyExists - upserted —
INSERT … ON CONFLICT (rpgSourceKey) DO UPDATE - SELECT — retourne les compteurs : insertedRows, updatedRows, skippedRows, validRows, invalidRows
Retourne ImportChunkStats. Aucun effet de bord sur le statut du job.
Décisions d’architecture
- SQL brut pour l’upsert : la normalisation géométrique PostGIS + l’upsert ON CONFLICT ne peuvent pas être exprimés via Prisma.
flushImportChunkutiliseprisma.$queryRawUnsafe. - Streaming via bun:sqlite : lecture SQLite directe depuis le
.gpkg, découpée en lots de 500 lignes. Pendant qu’un lot est flushé en DB, le suivant est parsé (pipeline via chaîne de promessespendingFlush). - pg-boss pour la durabilité : les jobs survivent aux redémarrages serveur.
localConcurrency: 1garantit le traitement séquentiel — pas d’imports concurrents, pas de race condition surisActiveRpg. - Clé source déterministe : hash SHA-256 de
(sourceYear, regionCode, id_parcel, code_cultu, code_group, culture_d1, culture_d2, cat_cult_p)— relancer le même import fait un upsert au lieu de dupliquer. - Limites de sécurité au téléchargement : 6 Gio par fichier, 30 Gio total, timeout de 20 minutes. Liste blanche d’hôtes via la variable d’environnement
RPG_IMPORT_ALLOWED_HOSTS. - Conservation des téléchargements en cas d’échec : si l’extraction ou l’import échoue mais que les téléchargements sont complets, le workspace est conservé. La nouvelle tentative saute le téléchargement et reprend directement à l’extraction.
⚙️ DevOps & Infrastructure
Monorepo
CerOps est organisé en monorepo géré par Turborepo et Bun. Toutes les applications et packages partagés cohabitent dans un seul dépôt Git, ce qui garantit la cohérence des types TypeScript entre le backend et les frontends, et simplifie les déploiements.
Bun est utilisé à la fois comme runtime JavaScript, package manager et outil de compilation (le serveur Fastify est compilé en binaire natif via bun build --compile). Les versions des dépendances partagées sont centralisées dans le workspace catalog pour éviter toute dérive entre packages.
Application mobile Flutter
L’application mobile agriculteurs est développée en Flutter dans un dépôt séparé. Elle n’est pas intégrée au monorepo pour deux raisons principales : l’écosystème Flutter (Dart, pub.dev) est incompatible avec la chaîne d’outils Bun/Node, et les cycles de release mobiles (App Store / Play Store) suivent un rythme indépendant du déploiement web.
L’app communique avec le même backend via les mêmes endpoints ORPC.
Intégration Continue (GitHub Actions)
Quatre workflows couvrent l’ensemble du cycle de qualité :
- CI (
ci.yaml) : build, tests unitaires et lint (Biome, knip, sherif) — déclenché sur chaque PR et push surmain - E2E (
e2e.yaml) : tests Playwright avec une base PostGIS éphémère — déclenché sur chaque PR - PR title (
pr-title.yaml) : validation Conventional Commits - Docs (
docs.yaml) : compilation mdBook — déclenché uniquement sidocs/**est modifié
Le cache Turborepo est restauré entre les runs pour ne reconstruire que les packages affectés par la PR.
Conteneurisation (Docker)
Chaque application a son propre Dockerfile multi-stage : le pruning Turborepo extrait uniquement les fichiers nécessaires à l’app ciblée, puis le build produit une image minimale. Le serveur est distribué comme binaire compilé, les frontends Next.js en mode standalone.
Déploiement : Coolify
Coolify est le PaaS self-hosted qui orchestre tous les services. Aujourd’hui, en phase de développement, Coolify effectue lui-même les builds directement depuis le dépôt Git à chaque push sur main.
À terme, cette responsabilité sera transférée à GitHub Actions : un workflow dédié se chargera de builder et publier une image Docker taguée (ex. v1.2.0) sur le registry, et Coolify n’aura plus qu’à déployer cette image prébuilt sur l’environnement cible (production, pré-production, etc.) sans refaire de build. Cette séparation garantit que le même artefact immuable traverse tous les environnements.
Services applicatifs
Les trois applications (server, web-agri, web-pilots) sont chacune un service Coolify distinct pointant sur leur Dockerfile respectif.
Base de données
La base de données est un service PostGIS (PostgreSQL + extension géospatiale) provisionné par Coolify. L’extension PostGIS est utilisée pour les calculs géographiques liés aux zones agricoles et aux missions terrain.
Services annexes
Coolify héberge également :
- OpenObserve — observabilité unifiée (logs, métriques, traces)
- RustFS — stockage objet compatible S3 (uploads, livrables de missions)
- pgAdmin — interface d’administration PostgreSQL
- mdBook — cette documentation
Architecture de déploiement (actuelle — full dev)
GitHub (push main)
│
▼ webhook + build
Coolify
├── server (API Fastify)
├── web-agri (frontend agriculteurs)
├── web-pilots (frontend pilotes)
├── postgis (base de données)
├── openobserve (observabilité)
├── rustfs (stockage objet)
├── pgadmin (admin BDD)
└── mdbook (documentation)
Architecture de déploiement (cible)
GitHub (tag vX.Y.Z)
│
▼ GitHub Actions (build + push image)
Container Registry
│
▼ deploy image
Coolify
├── prod → image :v1.2.0
├── preprod → image :v1.2.0-rc1
└── ...
Release & Déploiement
Ce chapitre détaille le processus de release et les différentes stratégies de déploiement selon l’environnement cible.
Processus de Release
Le processus de release est déclenché manuellement via GitHub Actions (workflow_dispatch) :
- Déclencheur : execution manuelle du workflow avec un input de version (
patch,minor,majorou1.2.3) - Préparation : bump des versions dans les 3
package.jsonvia bumpp - Build : création des images Docker pour les 3 applications
- Publication : push des images taguées dans le registry Github en privé
- Commit & Tag : commit des changements de version + création du tag Git
- GitHub Release : création de la release GitHub avec le changelog généré par git-cliff
Le tag Git est créé après le build réussi, pas avant. Cela garantit que le tag pointe toujours vers du code qui a été buildé et testé.
Versionnement
Le projet suit Semantic Versioning :
- MAJOR : changement breakant l’API
- MINOR : nouvelle fonctionnalité backward-compatible
- PATCH : correction de bug
Environnements
Développement
L’environnement de développement est déployé automatiquement via Coolify. À chaque push sur la branche main, Coolify détecte le changement et lance un build direct depuis le dépôt Git.
GitHub (push main)
│
▼ webhook
Coolify dev
├── server (build auto)
├── web-agri (build auto)
└── web-pilots (build auto)
Cet environnement sert aux tests d’intégration et au développement quotidien.
Production
La production n’est déployée qu’à partir de tags Git validés. L’image Docker est prébuildée par le workflow de release, puis déployée par Coolify.
Keep a Changelog
Le projet utilise git-cliff pour maintenir un historique clair des changements. Le changelog est généré automatiquement lors de la création de la release GitHub, en se basant sur les commits depuis la dernière release.
Architecture logicielle — Cerops
Vue d’ensemble
Cerops est une plateforme mettant en relation des agriculteurs et des pilotes de drone certifiés pour la surveillance et l’analyse de parcelles agricoles.
Type d’application
| Application | Type | Technologie |
|---|---|---|
web-agri | Application web riche (SPA/SSR) | Next.js 16 + React 19 |
web-pilots | Application web riche (SPA/SSR) | Next.js 16 + React 19 |
mobile | Application mobile native | Flutter (iOS + Android) |
server | Serveur API | Fastify + oRPC |
Méthode de gestion de projet
Approche Agile avec les pratiques suivantes, toutes vérifiées et enforced par le CI :
- TDD — 21 fichiers de tests unitaires couvrant l’intégralité des procédures métier, écrits avant ou simultanément au code
- Conventional Commits — format de commit validé automatiquement par GitHub Actions (
pr-title.yaml) - Versioning sémantique — releases déclenchées manuellement, changelog généré via
git-cliff - CI/CD — pipeline GitHub Actions (build, typecheck, lint, tests unitaires, tests E2E) sur chaque PR
Architecture retenue : Monolithe modulaire en monorepo
Justification (ADR-002)
L’équipe est restreinte et le périmètre fonctionnel bien défini. Une architecture microservices aurait apporté une complexité opérationnelle (service discovery, monitoring distribué) inadaptée. Le monolithe modulaire permet un développement rapide avec une séparation claire des responsabilités via les routers oRPC par domaine.
Structure monorepo (ADR-001)
cerops/
├── apps/
│ ├── web-agri/ # Frontend agriculteur (port 3001)
│ ├── web-pilots/ # Frontend pilote (port 3002)
│ └── server/ # API Fastify (port 3000)
└── packages/
├── api/ # Routers oRPC + logique métier
├── auth/ # Better-Auth
├── db/ # Prisma + schéma PostgreSQL
├── env/ # Validation des variables d'environnement
└── web-shared/ # Utilitaires partagés entre les deux frontends
Géré par Turborepo pour le cache de build et l’exécution parallèle des tâches. La dépendance mobile/ (Flutter) est volontairement séparée du monorepo TypeScript (ADR-009).
Choix technologiques justifiés
Serveur : Fastify + oRPC (ADR-005, ADR-006)
Fastify pour ses performances et son écosystème de plugins. oRPC assure la sécurité de type de bout en bout (serveur → client) sans génération de code. Exposition OpenAPI automatique via @orpc/openapi.
Frontend web : Next.js (ADR-004)
Deux applications distinctes par audience (agriculteurs / pilotes) pour éviter toute fuite de logique entre rôles. App Router Next.js pour le SSR natif et les Server Components (appels oRPC sans overhead réseau côté serveur).
Mobile : Flutter (ADR-009)
Performances proches du natif, accès caméra/GPS, et mode offline robuste via Drift (SQLite embarqué). Une seule codebase iOS + Android. Choisi face à React Native (offline plus complexe) et PWA (accès natif limité).
Base de données : PostgreSQL + PostGIS + Prisma (ADR-007, ADR-008)
PostgreSQL pour les transactions ACID et PostGIS pour les requêtes géospatiales (parcelles, viewport map). Prisma comme ORM pour la type-safety et les migrations versionées.
Authentification : Better-Auth (ADR-011)
Sessions httpOnly cookies, bearer tokens pour les clients API, CAPTCHA Cloudflare Turnstile, plugin Stripe pour les abonnements agriculteurs.
Runtime : Bun (ADR-003)
Remplacement de Node.js pour des performances d’installation et d’exécution supérieures. Compatible avec l’écosystème npm.
Paiements : Stripe (ADR-012)
Pré-autorisation à la création de mission, capture à la livraison du livrable.
Schéma de communication
Browser / Mobile App
│
│ HTTPS (cookies httpOnly / Bearer token)
▼
┌──────────────────┐
│ Fastify Server │ port 3000
│ ┌────────────┐ │
│ │ oRPC Layer │ │ /rpc/*
│ └────────────┘ │
│ ┌────────────┐ │
│ │ Better-Auth│ │ /api/auth/*
│ └────────────┘ │
│ ┌────────────┐ │
│ │ OpenAPI │ │ /docs
│ └────────────┘ │
└──────┬───────────┘
│
├── PostgreSQL + PostGIS (port 5432)
├── Ollama LLM (port 11434)
├── S3-compatible (fichiers livrables)
└── Stripe API (paiements)
Use Cases — Cerops
Extraits des fichiers de tests (packages/api/test/) qui constituent la spécification exécutable du système.
Acteurs
| Acteur | Description |
|---|---|
| Agriculteur | Publie des missions, gère ses parcelles et plans d’action |
| Pilote | Accepte et exécute des missions de drone |
| Admin | Gère les datasets RPG, la tarification, les options d’analyse |
| Système | Importe les parcelles RPG, calcule les prix, gère le score pilote |
UC-01 : Gestion des parcelles
Acteur principal : Agriculteur
| # | Cas d’utilisation | Précondition | Résultat attendu |
|---|---|---|---|
| 1.1 | Consulter ses parcelles | Authentifié | Liste des parcelles ordonnée par date de modification |
| 1.2 | Créer une parcelle manuellement | Authentifié, géométrie valide (< 500 ha) | Parcelle créée avec surface calculée depuis la géométrie |
| 1.3 | Créer une parcelle avec type de culture | Authentifié | Parcelle créée avec cultureType |
| 1.4 | Modifier une parcelle | Authentifié, propriétaire | Nom et/ou type de culture mis à jour |
| 1.5 | Supprimer une parcelle | Authentifié, propriétaire | Parcelle supprimée |
| 1.6 | Accéder à une parcelle RPG active | Authentifié | Accès autorisé même sans ownership |
| 1.7 | Revendiquer des parcelles RPG | Authentifié, parcelles non encore revendiquées | Parcelles rattachées à l’agriculteur |
| 1.8 | Consulter la carte (viewport) | Authentifié, zoom suffisant | GeoJSON des parcelles dans le viewport |
Cas d’erreur notables :
- Parcelle > 500 ha → rejetée
- Parcelle qui chevauche une parcelle existante de l’utilisateur → rejetée
- Accès à la parcelle d’un autre utilisateur →
FORBIDDEN - Parcelle RPG inactive →
NOT_FOUND
UC-02 : Marketplace des missions
Acteurs : Agriculteur (publication), Pilote (acceptation/livraison)
| # | Cas d’utilisation | Acteur | Résultat attendu |
|---|---|---|---|
| 2.1 | Estimer le prix d’une mission | Agriculteur | Prix total + détail des lignes (base + ha + options) |
| 2.2 | Créer une mission | Agriculteur | Mission PUBLISHED, prix calculé automatiquement, config de tarification associée |
| 2.3 | Lister les missions disponibles | Pilote | Missions PUBLISHED, filtrées par compatibilité capteurs, prix min/max, distance |
| 2.4 | Accepter une mission | Pilote compatible | Mission passe à SCHEDULED, pilote assigné |
| 2.5 | Annuler une mission (pilote) | Pilote assigné | Mission repasse à PUBLISHED, score pilote -2 |
| 2.6 | Annuler une mission (agriculteur) | Agriculteur propriétaire | Mission CANCELLED, score pilote non affecté |
| 2.7 | Livrer un livrable | Pilote assigné (mission SCHEDULED ou IN_PROGRESS) | Mission DELIVERED, score pilote +1, fichiers stockés |
| 2.8 | Consulter ses missions (agriculteur) | Agriculteur | Liste de ses missions avec statut, pilote assigné, livrable |
| 2.9 | Consulter ses missions (pilote) | Pilote | Missions groupées par date d’acceptation |
Cas d’erreur notables :
- Prix fourni par le client → rejeté (prix calculé côté serveur uniquement)
- Parcelles appartenant à un autre agriculteur → rejeté
- Aucun config de tarification publiée → rejeté
- Deuxième pilote tente d’accepter →
CONFLICT - Pilote déjà à 5 missions
SCHEDULED→FORBIDDEN - Pilote sans capteurs requis →
FORBIDDEN - Score pilote plancher à -20 (ne peut pas descendre plus bas)
UC-03 : Plans d’action
Acteur principal : Agriculteur
| # | Cas d’utilisation | Résultat attendu |
|---|---|---|
| 3.1 | Créer un plan d’action | Plan créé, titre unique par utilisateur |
| 3.2 | Créer une tâche dans un plan | Tâche créée avec statut todo par défaut |
| 3.3 | Associer une tâche à une parcelle | Tâche liée à une parcelle de l’utilisateur |
| 3.4 | Modifier le statut d’une tâche | Statut mis à jour (todo → in_progress → completed) |
| 3.5 | Réordonner les tâches | rank mis à jour |
| 3.6 | Définir une échéance | deadline enregistrée |
| 3.7 | Supprimer un plan d’action | Plan et toutes ses tâches supprimés (cascade) |
Cas d’erreur notables :
- Accès au plan d’action d’un autre utilisateur →
FORBIDDEN - Association d’une tâche à la parcelle d’un autre utilisateur →
FORBIDDEN
UC-04 : Profil pilote
Acteur principal : Pilote
| # | Cas d’utilisation | Résultat attendu |
|---|---|---|
| 4.1 | Consulter son profil | Profil auto-créé si inexistant, avec liste de drones et capteurs |
| 4.2 | Mettre à jour son profil | Champs scalaires mis à jour |
| 4.3 | Mettre à jour sa flotte de drones | Liste de drones remplacée intégralement |
| 4.4 | Mettre à jour ses capteurs | Liste de capteurs remplacée intégralement |
UC-05 : Import RPG (Admin)
Acteur principal : Admin
| # | Cas d’utilisation | Résultat attendu |
|---|---|---|
| 5.1 | Créer un dataset RPG | Dataset DRAFT créé |
| 5.2 | Lancer un import par région | Job créé, pipeline asynchrone déclenché (téléchargement → extraction → parsing → import) |
| 5.3 | Activer un dataset | Parcelles RPG rendues visibles aux agriculteurs |
| 5.4 | Archiver un dataset | Parcelles masquées |
UC-06 : Assistant IA agronomique
Acteur principal : Agriculteur
| # | Cas d’utilisation | Résultat attendu |
|---|---|---|
| 6.1 | Poser une question agronomique | Réponse de l’assistant (modèle qwen2.5vl:3b via Ollama) |
| 6.2 | Soumettre une photo de culture | Analyse visuelle : diagnostic, causes probables, actions recommandées |
Diagramme de séquence — Création et acceptation d’une mission
Agriculteur API (Fastify/oRPC) Base de données
│ │ │
│── createMission ──────>│ │
│ (plotIds, type, │── vérifier ownership ─>│
│ windowStart/End, │<─ plots OK ────────────│
│ analysisOptionIds) │── lire pricingConfig ─>│
│ │<─ config publiée ──────│
│ │── calculer prix │
│ │── insérer mission ────>│
│<── mission (PUBLISHED)─│<─ OK ──────────────────│
│ │ │
Pilote API (Fastify/oRPC) Base de données
│ │ │
│── acceptMission ──────>│ │
│ (missionId) │── vérifier capteurs ──>│
│ │<─ compatible ──────────│
│ │── vérifier quota (< 5)─>│
│ │<─ OK ──────────────────│
│ │── UPDATE SCHEDULED ───>│
│<── mission (SCHEDULED)─│<─ OK ──────────────────│
Politique de tests — Cerops
Principes
Les tests sont la spécification exécutable du système. Chaque procédure métier doit être couverte avant ou simultanément à son implémentation (TDD). Un test qui passe en CI est la définition de “fonctionne”.
Niveaux de tests
Tests unitaires / d’intégration (couche API)
Framework : Bun test (natif, compatible Jest)
Localisation : packages/api/test/*.test.ts
Base de données : PostgreSQL + PostGIS réelle, éphémère par session de test
Chaque fichier correspond à un domaine métier :
| Fichier | Domaine |
|---|---|
plots.test.ts | Gestion des parcelles |
missions.test.ts | Marketplace missions + profil pilote |
tasks.test.ts | Plans d’action et tâches |
action-plans.test.ts | Plans d’action |
pricing.test.ts | Calcul de prix des missions |
pricing-config.test.ts | Configuration de tarification |
analysis-options.test.ts | Options d’analyse |
pilots.test.ts | Profil pilote |
account.test.ts | Compte utilisateur et onboarding |
admin-access.test.ts | Contrôle d’accès admin |
admin-missions.test.ts | Gestion admin des missions |
admin-plots.test.ts | Gestion admin des parcelles |
ai.test.ts | Assistant IA |
geometry.test.ts | Calculs géospatiaux |
healthcheck.test.ts | Santé de l’API |
rpg-import-*.test.ts | Import données RPG (2 fichiers) |
sensor-kinds.test.ts | Types de capteurs |
Structure type d’un test :
describe("createMission", () => {
test("creates mission with derived price and pricing config", async () => { ... })
test("throws FORBIDDEN when called by a pilot", async () => { ... })
test("throws UNAUTHORIZED without session", async () => { ... })
test("rejects plots owned by another farmer", async () => { ... })
})
Chaque cas d’erreur (FORBIDDEN, UNAUTHORIZED, NOT_FOUND, CONFLICT) est explicitement testé.
Tests E2E
Framework : Playwright
Localisation : packages/e2e/tests/
Environnement : PostgreSQL + PostGIS éphémère en CI (GitHub Actions)
Couverture actuelle : Flux d’authentification (auth/auth.test.ts)
Exécution
# Tests unitaires
bun run test
# Tests E2E
bun run test:e2e
# CI (sans watch, avec rapport)
bun run test:ci
Intégration CI (GitHub Actions)
Deux workflows distincts :
ci.yaml — déclenché sur chaque push/PR vers main :
- Typecheck TypeScript (
check-types) - Lint + formatage Biome (
check:ci) - Vérification dépendances inutilisées (
knip) - Cohérence des versions workspace (
sherif) - Tests unitaires (
test:ci)
e2e.yaml — déclenché sur les PRs et dispatch manuel :
- Démarrage d’un container PostgreSQL + PostGIS
- Génération du client Prisma
- Migration de la base de données
- Exécution des tests Playwright
Conventions de nommage
describe: nom de la procédure oRPC (createMission,listPlots, etc.)test: phrase descriptive du comportement attendu, commençant par un verbe ou “throws”- Cas nominal :
"creates mission with derived price and pricing config" - Cas d’erreur :
"throws FORBIDDEN when called by a pilot" - Cas limite :
"score floor: score cannot go below -20 after many cancels"
- Cas nominal :
Ce qui n’est pas encore couvert
- Tests E2E des parcours utilisateur complets (web-agri, web-pilots) — uniquement auth pour l’instant
- Tests de charge / performance
- Tests de la couche mobile Flutter (hors périmètre de ce rendu)
CerOps — Business Plan 2026
Le ciel au service de l’agriculture Imagerie satellite & drone · Analyse agronomique par IA
Les problématiques — Les défis de l’agriculteur moderne
| Problème | Description |
|---|---|
| Données fragmentées | Météo, analyses de sol, images satellite, conseils coop : tout est éparpillé sur des outils différents. La vision d’ensemble manque au moment de décider. |
| Précision insuffisante | Les images satellite disponibles restent limitées (précision > 1 m) pour détecter les anomalies précocement ou intervenir à l’échelle de la parcelle. |
| Coût des erreurs | Un dosage ou un timing de traitement incorrect se paye cher : marge rognée, risques agronomiques, non-conformité réglementaire. Prix de l’eau et engrais en hausse. |
| Absence d’outil simple et directement actionnable | Les agriculteurs n’ont pas accès à un outil simple, intégré et directement actionnable, transformant des données complexes en recommandations concrètes. |
En France, 349 600 exploitations agricoles recensées en 2023 (Agreste ESEA 2023) — toutes pourraient bénéficier d’une meilleure exploitation de leurs données agronomiques.
Solution & Avantages Concurrentiels
Notre Solution
- Analyse d’images satellite & drone + IA
- Heat maps de biomasse et de stress hydrique (NDVI…)
- Suggestions de traitement localisées par niveau de risque
- Export de fichiers ISO-XML pour machines agricoles compatibles
- Module RSE : estimation de l’impact environnemental & reporting
- Application mobile utilisable depuis les champs
- Notification directement accessible sur téléphone
Avantages Clés
| Avantage | Détail |
|---|---|
| Analyse en < 20 min | Des images brutes à une décision en moins de 20 minutes |
| ISO-XML natif | Prescription directement exploitable par la machine |
| Module RSE intégré | Preuves environnementales mobilisables |
| Sans expertise numérique | Interface pensée pour l’agriculteur |
Étude de Marché — Agriculture de précision, France & Europe
| Marché | Taille | Description |
|---|---|---|
| TAM — Total Addressable Market | 11,5 Md€ | Marché mondial de l’agriculture de précision (2024). Croissance ~13 %/an, portée par la réduction des intrants et les exigences RSE. |
| SAM — Serviceable Available Market | 500 M€ | France + Europe de l’Ouest. Exploitations céréalières > 50 ha, viticulteurs, maraîchers et CUMA (~120 000 exploitations potentielles). |
| SOM — Serviceable Obtainable Market | 4 – 8 M€ | Objectif réaliste à 36 mois : 1–2 % du SAM. Soit 1 200–2 400 exploitations clientes à 150–300 €/mois (plans Starter à Pro). |
Sources : MarketsandMarkets 2024, Chambres d’Agriculture France, Agreste 2023
Description de l’Offre
Parcours utilisateur actuel (sans CerOps)
- Observation terrain — L’agriculteur constate visuellement un problème (jaunissement, verse, stress) sans pouvoir le quantifier ni le localiser précisément.
- Collecte de données — Il interroge plusieurs sources (appli météo, coopérative, bulletin phytosanitaire) sans agrégation possible. Temps moyen : 2–3h par décision.
- Décision à l’aveugle — En l’absence de carte de prescription, il traite souvent en plein (dose uniforme), perdant en moyenne 10 à 20 % d’intrants inutilement (USDA ERS ; EDIS/UFL).
- Avec CerOps — Il commande son vol et 20 min après le passage du pilote, il reçoit une heat map, des zones prioritaires et un fichier ISO-XML prêt pour sa machine.
Pipeline de traitement
Import image → Analyse IA → Heat Maps → Prescription ISO-XML → Recommandation
Satellite / Drone Traitement NDVI, stress, Fichier machine Optimisation agronomique
< 20 min biomasse & impact environnemental
Livrables inclus dans chaque analyse
- Rapport PDF avec recommandations agronomiques localisées et impact environnemental (RSE)
- Dashboard web interactif : suivi, historique, archivage
- Fichier ISO-XML prêt à être utilisé dans les machines agricoles
- Estimation des économies d’intrants (objectif : -10 % / parcelle)
Contraintes
Réglementaires
- RGPD : données parcellaires et coordonnées GPS considérées comme données personnelles
- Réglementation drone (DGAC) pour l’acquisition d’images aériennes
- Directives phytosanitaires : les recommandations doivent rester des « aides à la décision » et non des prescriptions certifiées
Techniques
- Dépendance à la couverture nuageuse pour les images satellite (solution : drone en complément)
- Résolution satellite commerciale : 30–50 cm (Pléiades Neo/1A) — 10 m minimum (Sentinel-2 Copernicus)
- Compatibilité ISO-XML limitée aux machines récentes ; besoin d’accompagnement pour le parc vieillissant
Approvisionnement / Partenariats
- Accès aux images : partenariat avec Airbus (Pléiades / SPOT) ou Maxar/Planet
- Partenariat coopératives et CUMA pour mutualiser les coûts d’abonnement
- Interopérabilité avec les logiciels de gestion parcellaire existants (Smag, ISAGRI…)
Objectifs & Stratégie Commerciale
Objectifs SMART à 12, 24 et 36 mois
| Horizon | Objectifs |
|---|---|
| 12 mois | 50 exploitations pilotes signées (SOM ~0,04 %) · MRR : 12 500 € · Taux d’adoption des suggestions IA > 60 % · Lancement MVP |
| 24 mois | 350 exploitations clientes actives · MRR : 87 500 € — ARR : 1,05 M€ · 1 partenariat coopérative ou CUMA signé (accès à 20–50 exploitations) · Module drone opérationnel |
| 36 mois | 1 200 exploitations actives · ARR : 3,6 M€ · Expansion Europe : Belgique, Pays-Bas, Espagne · Intégration ISO-XML V2 pour 90 % du parc compatible |
Modèle tarifaire
| Plan | Prix | Contenu |
|---|---|---|
| Free | 0 €/mois | Carte satellite de base, 1 analyse / mois |
| Starter | 150 €/mois | Jusqu’à 5 analyses/mois · Dashboard web + accès vol drone |
| Pro | 300 €/mois | Analyses illimitées · ISO-XML · Rapport RSE |
| CUMA / Coop | Sur devis | Licences multi-exploitations · API intégration |
Canaux d’acquisition
| Canal | Description |
|---|---|
| Coopératives & CUMA | Canal prioritaire : 1 accord = accès immédiat à 20–50 exploitations. Démonstrations terrain. |
| Prospection directe | Ciblage exploitations > 50 ha via bases Agreste, salons (SIMA, Innov-Agri, Sitevi). |
| Content Marketing | Blog agronomique, webinaires, études de cas ROI. SEO sur mots-clés agriculture de précision. |
| Ads ciblées | Facebook (très utilisé en agricole), Google Ads (recherche métier). |
Analyse Concurrentielle
Positionnement de CerOps face aux acteurs existants
| Critère | Smag/ISAGRI | John Deere Ops Center | xarvio | FieldView | EarthDaily Agro | CerOps |
|---|---|---|---|---|---|---|
| Analyse satellite | Partielle | Oui | Oui | Oui | Oui | Oui |
| < 20 min recommandations | Non | Non | Non | Non | Non | Oui |
| Export ISO-XML machine | Non (PAC-XML) | Oui (flotte JD) | Non | Oui | Non | Oui |
| Module RSE intégré | Non | Non | Partiel | Non | Non | Oui |
| Indépendant marque/chimie | Oui | Non (JD) | Non (BASF) | Non (Bayer) | Oui | Oui |
| Prix indicatif / an | ~399 € | Freemium | Sur devis* | ~1 999 € pack | Sur devis | 1 800–3 600 € |
* xarvio (BASF) : gratuit ou inclus avec l’achat de produits phyto BASF — Sources : aladin.farm ; climatefieldview.fr/tarifs ; ptxtrimble.com ; xarvio.com
CerOps : indépendant de toute marque, livraison < 20 min, ISO-XML + RSE réunis — les seules fonctions non proposées en combinaison chez aucun concurrent.
Business Plan Financier
1. Grille tarifaire & marges unitaires
| Produit | Prix de vente | Coût unitaire | Marge |
|---|---|---|---|
| Abonnement Starter (0–80 Ha) | 150 €/mois | 1 €/mois | 149 €/mois |
| Abonnement Pro (80–150 Ha) | 300 €/mois | 2 €/mois | 298 €/mois |
| Vol de drone | 500 € | 380 € | 120 € |
| Module RSE | 6 €/Ha/an | 0,20 €/Ha/an | 5,80 €/Ha/an |
| Module Assurance | 12 €/Ha/an | 0,50 €/Ha/an | 11,50 €/Ha/an |
2. Objectifs de ventes sur 3 ans
| Produit | Année 1 | Année 2 | Année 3 | CA An 3 |
|---|---|---|---|---|
| Abonnement Starter (0–80 Ha) | 17 clients | 140 clients | 540 clients | 972 000 € |
| Abonnement Pro (80–150 Ha) | 7 clients | 60 clients | 230 clients | 828 000 € |
| Vol de drone | 10 vols | 70 vols | 240 vols | 120 000 € |
| Module RSE | — | 1 400 Ha | 4 800 Ha | 28 800 € |
| Module Assurance | — | 1 400 Ha | 4 800 Ha | 57 600 € |
| Total CA | 60 800 € | 528 200 € | 2 006 400 € | 2 006 400 € |
3. Investissements initiaux (CAPEX)
| Poste | Montant |
|---|---|
| Logo + Identité visuelle | 2 000 € |
| Honoraires (avocat, comptable) pour création de société | 2 000 € |
| Traitement informatique N-1 | 1 000 € |
| Total CAPEX | 5 000 € |
4. Coûts opérationnels sur 3 ans (OPEX)
| Poste | 2027 | 2028 | 2029 |
|---|---|---|---|
| Ressources Humaines | |||
| CEO | 0 € | 12 000 € | 40 000 € |
| CTO | 0 € | 12 000 € | 40 000 € |
| Consultant RSE | — | 10 000 € | 20 000 € |
| Consultant Assurance | — | 10 000 € | 20 000 € |
| Consultant Développeur | — | 20 000 € | 40 000 € |
| Consultant IA | — | 10 000 € | 40 000 € |
| Administratif | |||
| Comptable | 2 400 € | 2 400 € | 2 400 € |
| Opérations | |||
| Infrastructure Cloud | 1 100 € | 1 100 € | 1 100 € |
| Infrastructure Cloud IA | 200 € | 200 € | 200 € |
| Divers | 5 000 € | 10 000 € | 20 000 € |
| Marketing | 20 000 € | 30 000 € | 40 000 € |
| Coûts annuels | 28 700 € | 117 700 € | 263 700 € |
| Coûts mensuels | 2 392 € | 9 808 € | 21 975 € |
5. Compte de résultat prévisionnel
| 2027 | 2028 | 2029 | |
|---|---|---|---|
| VENTES | |||
| Abonnement Starter | 30 600 € | 252 000 € | 972 000 € |
| Abonnement Pro | 25 200 € | 216 000 € | 828 000 € |
| Vol de drone | 5 000 € | 35 000 € | 120 000 € |
| Module RSE | — | 8 400 € | 28 800 € |
| Module Assurance | — | 16 800 € | 57 600 € |
| Total des ventes | 60 800 € | 528 200 € | 2 006 400 € |
| CHARGES VARIABLES | |||
| Abonnement Starter | 204 € | 1 680 € | 6 480 € |
| Abonnement Pro | 168 € | 1 440 € | 5 520 € |
| Vol de drone | 3 800 € | 26 600 € | 91 200 € |
| Module RSE | 0 € | 280 € | 960 € |
| Module Assurance | 0 € | 700 € | 2 400 € |
| CHARGES FIXES (OPEX) | |||
| CEO | 0 € | 12 000 € | 40 000 € |
| CTO | 0 € | 12 000 € | 40 000 € |
| Consultant RSE | — | 10 000 € | 20 000 € |
| Consultant Assurance | — | 10 000 € | 20 000 € |
| Consultant Développeur | — | 20 000 € | 40 000 € |
| Consultant IA | — | 10 000 € | 40 000 € |
| Comptable | 2 400 € | 2 400 € | 2 400 € |
| Infrastructure Cloud | 1 100 € | 1 100 € | 1 100 € |
| Infrastructure Cloud IA | 200 € | 200 € | 200 € |
| Divers | 5 000 € | 10 000 € | 20 000 € |
| Marketing | 20 000 € | 30 000 € | 40 000 € |
| CAPEX amorti | 1 667 € | 1 667 € | 1 667 € |
| Total des charges | 34 539 € | 150 067 € | 371 927 € |
| RÉSULTAT NET | +26 261 € | +378 133 € | +1 634 473 € |
| Rentabilité | 43,2 % | 71,6 % | 81,5 % |
6. Plan de financement
Avec une tarification à 150/300 €/mois, le modèle est rentable dès l’année 1. Le seul besoin de financement est le CAPEX initial de démarrage.
| Poste | Montant |
|---|---|
| CAPEX initial (logo, honoraires, infra N-1) | 5 000 € |
| Besoin total | 5 000 € |
| Source | Apport |
|---|---|
| Fondateur | 3 000 € |
| CTO (associé) | 2 000 € |
| Total couvert | 5 000 € |
Aucun emprunt bancaire nécessaire. Les bénéfices de l’année 1 (+26 261 €) couvrent largement les investissements de croissance de l’année 2.
Analyse SWOT
Forces
- Analyse en < 20 min
- Export ISO-XML natif — passage direct de l’analyse à l’action
- Module RSE différenciant, en ligne avec les exigences filières
- Stack technique moderne : Next.js, Flutter, Fastify, IA scalable
Faiblesses
- Dépendance à la couverture nuageuse pour les images satellite
- Équipe réduite : risque de bottleneck R&D et commercial
- Pas encore de revenus récurrents validés à grande échelle
- Notoriété quasi-nulle face aux acteurs établis (Smag, Trimble)
Opportunités
- Pression réglementaire RSE et PAC 2023–2027 → besoin de traçabilité
- Standardisation ISO-XML accélérée par les constructeurs agricoles
- Forte pression sur les intrants : marché prêt à payer pour +10 % d’économies
- Segment CUMA : effet levier massif (1 contrat = n exploitations)
Menaces
- Entrée de grands groupes (BASF, Bayer) avec budgets R&D élevés
- Accessibilité croissante de Sentinel-2 (gratuit) : banalisation de la donnée
- Résistance au changement : digitalisation lente dans les exploitations traditionnelles
- Dépendance fournisseurs d’images satellite (pouvoir de négociation limité)
Diagrammes
Modèle Conceptuel de Données (MCD)
Généré depuis packages/db/prisma/schema/.
erDiagram
User {
string id PK
string name
string email
enum accountType
boolean onboardingCompleted
}
Plot {
uuid id PK
string name
enum source
float surface
geometry geom
string cultureType
string userId FK
boolean isActiveRpg
}
ActionPlan {
string id PK
string title
string userId FK
}
Task {
string id PK
string title
enum status
int rank
datetime deadline
string actionPlanId FK
string plotId FK
}
PilotProfile {
string id PK
string userId FK
boolean available
int score
}
Drone {
string id PK
string name
string model
string pilotProfileId FK
}
Sensor {
string id PK
string name
string sensorKindCode FK
string pilotProfileId FK
}
SensorKind {
string code PK
string name
boolean active
}
Mission {
string id PK
string farmerId FK
enum type
enum status
float totalSurface
float price
datetime windowStart
datetime windowEnd
string pilotProfileId FK
}
AnalysisOption {
string id PK
string code
string name
float basePrice
float pricePerHa
}
Deliverable {
string id PK
string missionId FK
string[] files
}
Payment {
string id PK
string missionId FK
float amount
enum status
}
Flight {
string id PK
datetime scheduledAt
enum status
string plotId FK
string pilotId FK
}
User ||--o{ Plot : "possède"
User ||--o{ ActionPlan : "crée"
User ||--o| PilotProfile : "est"
User ||--o{ Mission : "publie"
ActionPlan ||--o{ Task : "contient"
Task }o--o| Plot : "liée à"
PilotProfile ||--o{ Drone : "possède"
PilotProfile ||--o{ Sensor : "équipé de"
Sensor }o--|| SensorKind : "de type"
PilotProfile ||--o{ Mission : "accepte"
Mission }o--o{ Plot : "couvre"
Mission }o--o{ SensorKind : "requiert"
Mission }o--o{ AnalysisOption : "inclut"
Mission ||--o| Deliverable : "livre"
Mission ||--o| Payment : "facturée"
Plot ||--o{ Flight : "survol"
Architecture Logicielle
Monolithe modulaire · Monorepo Turborepo · 3 apps + 6 packages partagés.
graph TB
subgraph Clients
WA["web-agri\nNext.js · :3001\nAgriculteurs"]
WP["web-pilots\nNext.js · :3002\nPilotes"]
MOB["Mobile\nFlutter · iOS & Android"]
end
subgraph Server["Serveur Fastify · :3000"]
ORPC["oRPC /rpc/*"]
AUTH["Better-Auth /api/auth/*"]
subgraph Routers["packages/api"]
direction LR
R1["plots"] --- R2["missions"]
R2 --- R3["actions"]
R3 --- R4["ai"]
R4 --- R5["pilots"]
R5 --- R6["weather"]
end
end
subgraph Infra["Infrastructure"]
PG[("PostgreSQL\n+ PostGIS")]
OLLAMA["Ollama LLM\n:11434"]
S3["S3 Storage"]
STRIPE["Stripe"]
end
WA -->|"HTTPS + cookies"| ORPC
WP -->|"HTTPS + cookies"| ORPC
MOB -->|"HTTPS + Bearer"| ORPC
ORPC --> Routers
AUTH --> PG
Routers --> PG
R4 --> OLLAMA
R2 --> S3
R2 --> STRIPE
Diagramme de Séquence — Cycle de vie d’une mission
sequenceDiagram
actor Agri as Agriculteur
participant API as API Fastify/oRPC
participant DB as PostgreSQL
participant S3 as S3 Storage
actor Pilote as Pilote
rect rgb(232, 245, 237)
Note over Agri,DB: Création de mission
Agri->>API: createMission(plotIds, type, window, analysisOptionIds)
API->>DB: vérifier ownership des parcelles
DB-->>API: plots OK
API->>DB: lire pricingConfig (PUBLISHED)
DB-->>API: config OK
API->>API: prix = max(min, base + surface×€/ha + Σ options)
API->>DB: INSERT mission (PUBLISHED)
DB-->>API: OK
API-->>Agri: mission { status: PUBLISHED, price }
end
rect rgb(220, 240, 228)
Note over Pilote,DB: Acceptation
Pilote->>API: acceptMission(missionId)
API->>DB: vérifier capteurs compatibles
DB-->>API: compatible
API->>DB: COUNT missions SCHEDULED < 5
DB-->>API: OK
API->>DB: UPDATE status → SCHEDULED
API-->>Pilote: mission { status: SCHEDULED }
end
rect rgb(208, 235, 218)
Note over Pilote,S3: Livraison
Pilote->>API: uploadDeliverable(missionId, files[])
API->>S3: upload fichiers
S3-->>API: URLs
API->>DB: INSERT deliverable
API->>DB: UPDATE status → DELIVERED
API->>DB: pilotProfile.score + 1
API-->>Pilote: deliverable OK
API-->>Agri: notification DELIVERED
end
Diagramme de Use Cases
graph LR
AGRI(["Agriculteur"])
PILOTE(["Pilote"])
ADMIN(["Admin"])
subgraph Parcelles
UC1["Créer / modifier parcelle"]
UC2["Revendiquer parcelles RPG"]
UC3["Consulter la carte"]
end
subgraph Marketplace
UC4["Créer une mission"]
UC5["Consulter les missions"]
UC6["Accepter une mission"]
UC7["Livrer un livrable"]
end
subgraph Actions["Plans d'Action"]
UC8["Gérer plans d'action"]
UC9["Gérer les tâches"]
end
subgraph IA["Assistant IA"]
UC10["Question agronomique"]
UC11["Analyser une photo"]
end
subgraph Administration
UC12["Import données RPG"]
UC13["Configurer tarification"]
end
AGRI --> UC1
AGRI --> UC2
AGRI --> UC3
AGRI --> UC4
AGRI --> UC8
AGRI --> UC9
AGRI --> UC10
AGRI --> UC11
PILOTE --> UC5
PILOTE --> UC6
PILOTE --> UC7
ADMIN --> UC12
ADMIN --> UC13
Énumérations du modèle de données
| Enum | Valeurs |
|---|---|
AccountType | farmer, buyer, cooperative, pilot, admin |
PlotSource | manual, rpg |
FlightStatus | planned, in_progress, completed, cancelled |
TaskStatus | todo, in_progress, completed |
MissionType | SURVEILLANCE, MAPPING, INSPECTION, SPRAYING |
MissionStatus | PUBLISHED, SCHEDULED, IN_PROGRESS, DELIVERED, CANCELLED |
PaymentStatus | PRE_AUTHORIZED, CAPTURED, REFUNDED |
MissionPricingConfigStatus | DRAFT, PUBLISHED, ARCHIVED |
RpgDatasetStatus | DRAFT, COMPLETE, ACTIVE, ARCHIVED |
RpgImportJobStatus | PENDING, DOWNLOADING, EXTRACTING, PARSING, IMPORTING, COMPLETED, ACTIVATED, CANCELLED, FAILED |
Points clés du modèle
- Plot stocke la géométrie en
MultiPolygon SRID 2154(Lambert 93) via PostGIS. La surface est calculée automatiquement depuis la géométrie. - Mission regroupe des parcelles via la table de jonction
MissionPlotet peut appartenir à unMissionGrouppour les missions groupées. - Score pilote :
PilotProfile.score— +1 par livraison, -2 par annulation, plancher à -20. - Parcelles RPG : importées depuis le Registre Parcellaire Graphique. Une parcelle active (
isActiveRpg=true) est visible par tous les agriculteurs jusqu’à ce qu’elle soit revendiquée. - Tarification mission :
prix = max(minimumMissionPrice, basePrice + surface × pricePerHa + Σ analysisOptions)
ADR-001 - Architecture monorepo avec Turborepo et Bun workspaces
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le projet Cerops comprend plusieurs applications (web-agri, web-pilots, server) et packages partagés (api, auth, db, env, config).
Ces composants sont fortement couplés : un changement de schéma de base de données impacte simultanément le backend et les deux frontends.
Il est donc nécessaire de choisir une stratégie d’organisation du code source qui minimise la friction entre équipes et garantit la cohérence des types partagés.
Décision
Nous organisons le projet sous forme de monorepo géré avec Turborepo et Bun workspaces.
Alternatives considérées
- Polyrepo : un dépôt Git par application/package — versioning indépendant mais synchronisation complexe entre dépôts, gestion fastidieuse des versions des packages partagés
- Monorepo sans outil de build : organisation simple mais pas de cache ni de pipeline de build optimisé
- Nx : alternative à Turborepo, plus complet mais plus complexe à configurer pour une petite équipe
Conséquences
- Partage de code facilité entre les packages (
api,auth,db) et les applications - Refactoring atomique : un seul commit peut modifier frontend, backend et packages partagés
- Cache de build et exécution parallèle des tâches via Turborepo
- Configuration unifiée (TypeScript, Biome, tests) à la racine du dépôt
- Catalog Bun workspaces pour centraliser les versions des dépendances et éviter les dérives
- Dépôt potentiellement plus lourd à cloner pour un contributeur ne travaillant que sur une partie
- Nécessite une bonne discipline de gestion des dépendances (outils
sherif,knipen place) - En cas de croissance de l’équipe, les conflits de merge peuvent augmenter sur les fichiers partagés
ADR-002 - Architecture monolithe modulaire
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le projet est développé sur une durée courte avec une équipe restreinte. Le périmètre fonctionnel est bien défini (gestion de parcelles, missions de drone, marketplace) et ne nécessite pas de distribution complexe des services. Le choix architectural doit permettre un développement rapide tout en maintenant une bonne séparation des responsabilités.
Décision
Nous choisissons une architecture monolithique modulaire : un seul serveur Fastify avec une organisation interne par domaines métier via les packages oRPC (packages/api).
Alternatives considérées
- Microservices : indépendance de déploiement par service, mais complexité opérationnelle (service discovery, communication inter-services, monitoring distribué) inadaptée à la taille du projet
- Architecture serverless : coûts maîtrisés à faible charge, mais cold starts et difficulté de débogage local
- Monolithe non structuré : développement encore plus rapide initialement, mais dette technique rapide et difficultés de maintenabilité
Conséquences
- Développement et débogage plus rapides (un seul process à lancer)
- Déploiement simplifié (un seul artefact serveur)
- Moins de complexité opérationnelle
- Bonne séparation des responsabilités via les routers oRPC par domaine
- Moins scalable horizontalement qu’une architecture microservices — acceptable pour le volume actuel
- Un bug critique dans un module peut affecter l’ensemble du serveur
- Migration vers des microservices rendue plus difficile si le besoin émerge, bien qu’atténuée par la modularité interne
ADR-003 - Bun comme runtime JavaScript
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le projet nécessite un runtime JavaScript pour exécuter le serveur Fastify et les scripts du monorepo. L’équipe cherche à maximiser la rapidité de démarrage, la performance des scripts de build et la simplicité de la chaîne d’outils (un seul outil pour runtime, package manager et bundler).
Décision
Nous utilisons Bun comme runtime JavaScript, package manager et bundler pour l’ensemble du monorepo.
Alternatives considérées
- Node.js + npm/pnpm : écosystème mature, large communauté, tooling bien documenté
- Node.js + pnpm workspaces : meilleure gestion des monorepos que npm, mais performances inférieures à Bun
- Deno : sécurité by default, support natif TypeScript, mais compatibilité limitée avec l’écosystème npm
Conséquences
- Démarrage du serveur et exécution des scripts significativement plus rapides qu’avec Node.js
- Compatibilité native TypeScript sans étape de transpilation séparée
- Package manager intégré avec support des workspaces et catalog
- Compilation du serveur en binaire natif possible via
bun build --compile - Runtime encore jeune : certaines APIs Node.js ne sont pas encore totalement compatibles
- Communauté et documentation moins matures que Node.js
- Certains packages npm peuvent présenter des comportements inattendus sous Bun
- L’équipe doit rester vigilante aux breaking changes entre versions de Bun
ADR-004 - Next.js pour les applications web (architecture multi-app)
- Statut : Accepté
- Date : 2026-04-05
Contexte
L’application web doit servir deux audiences aux besoins très différents :
- Agriculteurs (
web-agri) : consultation de parcelles, suivi d’actions, accès à la marketplace - Pilotes de drone (
web-pilots) : gestion de missions, suivi de vol, interface opérationnelle
Les rôles, les parcours utilisateurs et les interfaces sont suffisamment distincts pour justifier une séparation applicative. L’équipe a de l’expérience en React et cherche une solution avec SSR et bon support TypeScript.
Décision
Nous utilisons Next.js avec TypeScript pour les deux frontends web, organisés en deux applications distinctes dans le monorepo : web-agri (port 3001) et web-pilots (port 3002), partageant les packages api, auth et env.
Alternatives considérées
- Une seule application Next.js avec routing par rôle : moins de duplication de configuration, mais couplage plus fort entre les deux surfaces et risque de fuite de code entre rôles
- Angular : framework complet avec opinions fortes, mais courbe d’apprentissage plus raide et moins adapté à l’écosystème oRPC/TanStack
- React sans framework (Vite + React Router) : plus léger, mais sans SSR natif ni App Router
- Vue.js / Nuxt : écosystème plus léger, mais moins maîtrisé par l’équipe
Conséquences
- SSR natif via App Router pour de meilleures performances initiales et le SEO
- Architecture dual-client oRPC : appels directs en Server Components (zéro overhead réseau), hooks TanStack Query en Client Components
- Séparation claire des surfaces par audience, sans risque de fuite de logique ou d’UI entre rôles
- Duplication de la configuration Next.js entre les deux apps (atténuée par
packages/config) - Deux processus de build distincts à maintenir
- Dépendance forte à l’écosystème Next.js / Vercel pour les évolutions du framework
ADR-005 - Fastify pour le serveur backend
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le backend doit exposer une API performante, typée, avec un bon support TypeScript et Bun. L’équipe cherche un framework léger permettant une mise en place rapide sans imposer une structure trop rigide.
Décision
Nous utilisons Fastify comme framework HTTP pour le serveur backend.
Alternatives considérées
- Express.js : très répandu et bien documenté, mais moins performant, peu opiniated sur TypeScript, pas de support natif des schémas de validation
- NestJS : structure imposée, DI intégrée, adapté aux grandes équipes, mais sur-ingénierie pour ce projet et compatibilité Bun non garantie
- Hono : très léger, excellente compatibilité Bun, mais écosystème de plugins moins riche que Fastify
- Spring Boot : inadapté à l’écosystème TypeScript du projet
Conséquences
- Performances élevées (l’un des frameworks Node.js/Bun les plus rapides)
- Validation intégrée via JSON Schema (complémentaire à Zod via oRPC)
- Système de plugins (
@fastify/cors) pour étendre les fonctionnalités - Bonne compatibilité avec Bun
- Moins de structure imposée que NestJS : la discipline d’organisation repose sur l’équipe
- Certains plugins Fastify peuvent ne pas être totalement compatibles avec Bun
- Communauté plus petite qu’Express
ADR-006 - oRPC pour la couche de communication API
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le projet nécessite une communication typée de bout en bout entre les frontends Next.js et le serveur Fastify. L’application est interne, sans contrainte d’exposition publique de l’API à des tiers. L’équipe veut réduire le boilerplate lié à la définition, la validation et la consommation des endpoints.
Décision
Nous utilisons oRPC pour la communication front-back, avec génération automatique d’une documentation OpenAPI via @orpc/openapi.
Alternatives considérées
- API REST classique : standard universel, facilement consommable par des tiers, mais nécessite de la duplication de types et de la validation des deux côtés
- GraphQL : flexibilité de requêtage, mais complexité de setup (schema, resolvers, codegen) disproportionnée pour ce projet
- tRPC : concurrent direct d’oRPC, bon écosystème, mais oRPC offre une meilleure intégration OpenAPI et un support natif Zod v4
Conséquences
- Type safety de bout en bout : le contrat API est défini une seule fois dans
packages/apiet partagé entre server et clients - Réduction du boilerplate de validation (Zod schemas réutilisés côté serveur et client)
- Documentation OpenAPI auto-générée exposée sur
/docs - Architecture dual-client : appels directs en Server Components (sans HTTP), hooks TanStack Query en Client Components
- Couplage plus fort entre frontend et backend qu’une API REST découplée
- Moins standard qu’une API REST : intégration difficile pour des clients externes ou des outils tiers
- Dépendance à un écosystème plus jeune qu’Express+REST ou GraphQL
ADR-007 - PostgreSQL comme base de données relationnelle
- Statut : Accepté
- Date : 2026-04-05
Contexte
L’application manipule des données fortement relationnelles :
- utilisateurs, rôles (agriculteurs, pilotes)
- parcelles agricoles et leurs métadonnées géographiques
- missions de drone et plans de vol
- actions et tâches sur les parcelles
- transactions de la marketplace
Elle nécessite des transactions fiables et une modélisation stricte des relations entre entités.
Décision
Nous utilisons PostgreSQL comme base de données principale.
Alternatives considérées
- MongoDB : flexibilité du schéma utile pour des données non structurées, mais inadapté aux relations complexes et aux transactions multi-documents
- MySQL : bonne alternative relationnelle, mais fonctionnalités avancées (types JSON, window functions, extensions géospatiales) moins riches que PostgreSQL
- SQLite : parfait pour le développement local, mais inadapté à la production multi-utilisateurs et à la concurrence d’écriture
- PlanetScale / Turso : bases managées intéressantes, mais complexité supplémentaire et coût
Conséquences
- Forte cohérence des données et support complet des transactions ACID
- Excellente modélisation des relations entre entités métier
- Support des types avancés (JSONB, tableaux, UUID) utiles pour les métadonnées de parcelles
- Extension PostGIS disponible si des requêtes géospatiales avancées s’avèrent nécessaires
- Nécessite une modélisation stricte du schéma en amont
- Requiert une instance PostgreSQL dédiée (Docker en développement, service managé en production)
- Montée en charge horizontale plus complexe que des bases NoSQL distribuées
ADR-008 - Prisma comme ORM
- Statut : Accepté
- Date : 2026-04-05
Contexte
Le projet nécessite un accès base de données simple, typé et rapide à mettre en œuvre.
Le schéma est centralisé dans packages/db et partagé entre le serveur et les packages internes.
L’équipe veut éviter d’écrire du SQL brut tout en maintenant un contrôle sur les migrations.
Décision
Nous utilisons Prisma comme ORM, avec l’adaptateur @prisma/adapter-pg pour la connexion PostgreSQL et la génération de types ESM compatible Bun.
Alternatives considérées
- TypeORM : mature, décorateurs TypeScript, mais configuration complexe et compatibilité Bun incertaine
- Drizzle ORM : très léger, performant, SQL-like, bonne compatibilité Bun — alternative sérieuse mais adoption moins répandue dans l’équipe
- Kysely : query builder typé, contrôle total sur le SQL, mais plus verbeux et sans gestion de migrations intégrée
- Requêtes SQL brutes : contrôle maximal, mais perte du typage automatique et maintenance plus lourde
Conséquences
- Schéma lisible et déclaratif dans les fichiers
.prisma - Types TypeScript auto-générés à partir du schéma (zéro dérive entre base et code)
- Migrations versionnées via
prisma migratepour la production - Prisma Studio pour explorer et modifier les données en développement
- Abstraction qui peut limiter certaines optimisations SQL avancées (requêtes complexes nécessitent
$queryRaw) - Génération du client requise après chaque modification de schéma (
bun db:generate) - Performances légèrement inférieures à un query builder bas niveau pour des requêtes massives
- Le fichier de schéma multi-fichiers (
schema.prisma+auth.prisma) nécessite Prisma >= 6
ADR-009 - Flutter pour l’application mobile
- Statut : Accepté
- Date : 2026-04-05
Contexte
L’application mobile est utilisée par les agriculteurs et les pilotes de drone en plein champ, souvent dans des zones à faible connectivité. Les besoins incluent : consultation de parcelles, suivi de missions, affichage de cartes, mode offline, et accès caméra pour la prise de photos. Le choix mobile est un point de divergence majeur avec d’autres projets de l’équipe qui utilisent une PWA.
Décision
Nous choisissons de développer l’application mobile en Flutter (Dart), ciblant iOS et Android à partir d’une base de code unique.
Alternatives considérées
- Progressive Web App (PWA) : une seule base de code web+mobile, mais accès limité aux APIs natives (GPS précis, caméra avancée, stockage local robuste), performances inférieures sur mobile, et fonctionnement offline moins fiable en conditions terrain
- React Native : partage de code possible avec le frontend web (React), mais performances et accès natif inférieurs à Flutter, et gestion du mode offline plus complexe
- Application native iOS/Android séparée : performances et intégration native maximales, mais double base de code, double maintenance, double coût de développement
- Capacitor (Ionic) : proche du PWA avec accès natif, mais performances UI insuffisantes pour des cartes et interactions terrain
Conséquences
- Performances proches du natif grâce au moteur de rendu Skia/Impeller de Flutter
- Accès natif à la caméra, au GPS, au stockage sécurisé et à la connectivité réseau
- Mode offline robuste via Drift (SQLite embarqué) — voir ADR-010
- Un seul codebase pour iOS et Android
- Langage Dart distinct du reste du stack TypeScript : la base de code mobile est totalement séparée
- Les développeurs web du projet ne peuvent pas contribuer directement à la partie mobile sans apprendre Dart/Flutter
- Taille de l’APK/IPA plus importante qu’une app native pure
- Déploiement via les stores (App Store, Google Play) : processus de validation et de mise à jour plus lent qu’une PWA
ADR-010 - Drift pour la persistance locale sur mobile
- Statut : Accepté
- Date : 2026-04-05
Contexte
L’application mobile est utilisée en plein champ, dans des zones sans couverture réseau fiable. Les agriculteurs et pilotes de drone doivent pouvoir consulter leurs parcelles et créer des actions hors ligne, puis synchroniser les données à la reconnexion. Un mécanisme de persistance locale est donc indispensable sur le mobile Flutter.
Décision
Nous utilisons Drift (anciennement Moor) comme ORM SQLite pour la persistance locale sur mobile.
Alternatives considérées
- Hive : base de données clé-valeur NoSQL légère pour Flutter, rapide pour de la lecture simple, mais pas adaptée aux relations et aux requêtes complexes
- Isar : base orientée objet pour Flutter, très performante, mais sans support des transactions relationnelles
- sqflite : accès SQLite bas niveau, contrôle total, mais verbeux et sans typage automatique
- SharedPreferences : uniquement pour des données simples de type clé-valeur, inadapté à un cache structuré
Conséquences
- Schéma SQLite typé et déclaratif en Dart, cohérent avec l’approche Prisma côté serveur
- Support des relations entre tables, des transactions et des requêtes complexes
- Génération de code via
drift_devetbuild_runnerpour les types et les requêtes - Stratégie de synchronisation par flag
needsSync: les entités modifiées hors ligne sont marquées et synchronisées à la reconnexion - La synchronisation bidirectionnelle (conflits) doit être gérée manuellement — logique non triviale
- Nécessite une étape de génération de code (
build_runner) après chaque modification de schéma - Deux schémas à maintenir en cohérence : Prisma (serveur) et Drift (mobile) — risque de dérive si les entités évoluent
ADR-011 - Better-Auth pour l’authentification
- Statut : Accepté
- Date : 2026-04-05
Contexte
L’application nécessite une gestion de l’authentification pour les utilisateurs web (agriculteurs, pilotes) et mobiles (Flutter). Le système doit gérer les sessions, les rôles, et s’intégrer avec Stripe pour les abonnements. L’équipe cherche une solution clé-en-main, typée TypeScript, sans dépendre d’un service tiers payant.
Décision
Nous utilisons Better-Auth comme bibliothèque d’authentification, avec l’adaptateur Prisma (@better-auth/prisma-adapter) et le plugin Stripe (@better-auth/stripe).
Alternatives considérées
- Auth.js (NextAuth) : très répandu dans l’écosystème Next.js, mais couplé au framework Next.js et difficile à partager avec un serveur Fastify indépendant
- Clerk : solution SaaS complète avec UI intégrée, mais coût mensuel récurrent et dépendance à un service tiers externe
- Lucia : bibliothèque minimaliste et flexible, bonne alternative, mais nécessite plus de code custom pour les fonctionnalités avancées
- Auth custom : contrôle total, mais implémentation de la sécurité (hachage, sessions, CSRF) sujette aux erreurs
Conséquences
- Authentification sessions-based via cookies httpOnly — pas de gestion de JWT côté client
- Schéma de base de données généré et géré automatiquement via
auth.prisma - Client Flutter via
better_auth_client(package communautaire) — moins mature que le SDK web - Plugin Stripe intégré pour la gestion des abonnements sans code custom
- Documentation OpenAPI auto-générée sur
/api/auth/reference - Dépendance à une bibliothèque relativement jeune : risque de breaking changes lors des mises à jour majeures
- Le package Flutter
better_auth_clientest un package communautaire non officiel — sa maintenance n’est pas garantie - Moins de composants UI préconstruits que Clerk : les formulaires de connexion sont à implémenter manuellement
ADR-012 - Stripe pour la gestion des paiements
- Statut : Accepté
- Date : 2026-04-05
Contexte
Cerops inclut une marketplace où des services et produits agricoles peuvent être échangés. La plateforme doit gérer des abonnements et/ou des transactions entre utilisateurs. Le traitement des paiements est un domaine sensible qui nécessite conformité PCI-DSS, gestion des remboursements et des litiges.
Décision
Nous utilisons Stripe comme solution de paiement, intégré via le plugin @better-auth/stripe côté serveur.
Alternatives considérées
- PayPal : notoriété internationale, mais API moins développeur-friendly et intégration plus complexe
- Mollie : bonne alternative européenne, APIs modernes, mais écosystème de plugins moins riche
- Paiement custom : contrôle total, mais développement long, conformité PCI-DSS à gérer manuellement — risque de sécurité majeur
- LemonSqueezy : solution SaaS tout-en-un pour les abonnements, mais moins flexible pour une marketplace B2B
Conséquences
- Conformité PCI-DSS déléguée à Stripe : les données de carte ne transitent jamais par nos serveurs
- Gestion des abonnements, des remboursements et des webhooks via l’API Stripe
- Intégration simplifiée via
@better-auth/stripequi synchronise les abonnements avec les sessions utilisateurs - Stripe Elements / Stripe.js pour les formulaires de paiement côté client
- Commission Stripe sur chaque transaction (1.4% + 0.25€ pour les cartes européennes) — à intégrer dans la politique tarifaire
- Dépendance forte à un service tiers : une panne Stripe impacte directement les paiements de la plateforme
- Les paiements sont en euros par défaut — la gestion multi-devises nécessiterait une configuration supplémentaire
- Les webhooks Stripe nécessitent un endpoint public accessible — à prévoir dans l’infrastructure de déploiement