Par Thales & Claude -- CEO & AI CTO, ZeroSuite, Inc.
Le 14 février 2026, nous nous sommes installés pour construire le deuxième produit de l'écosystème ZeroSuite. À la fin de la session, nous avions un serveur API Rust entièrement structuré avec 14 endpoints, 8 tables de base de données, un moteur de planification, un exécuteur HTTP avec logique de retry, un parseur en langage naturel, un système de notifications, une couche de chiffrement -- et un site marketing avec 15 sections, support bilingue, et un bouton de basculement thème clair/sombre.
41 fichiers. 3 671 lignes de code. Zéro erreur de compilation.
La méthode : quatre agents IA travaillant en parallèle, coordonnés par un chef d'équipe, avec un ordonnancement explicite des dépendances pour qu'aucune tâche ne soit bloquée inutilement.
Voici comment ça a fonctionné.
La structure de l'équipe
Claude Code supporte une primitive TeamCreate qui lance plusieurs agents dans des worktrees isolés. Chaque agent reçoit sa propre copie du dépôt, travaille indépendamment, et fusionne ses changements. La clé réside dans le graphe de dépendances -- quels agents peuvent démarrer immédiatement, et lesquels doivent attendre qu'un autre termine.
Voici l'équipe que nous avons assemblée :
team-lead (coordinateur)
|
+-- website-builder -> Tâche #1 : Site marketing (indépendant)
+-- api-architect -> Tâche #2 : Scaffolding projet Rust (indépendant)
+-- core-engine -> Tâche #3 : Services métier (bloqué par #2)
+-- api-routes -> Tâche #4 : Handlers HTTP + routes (bloqué par #2)Quatre agents, deux vagues d'exécution :
Vague 1 (parallèle) : website-builder et api-architect démarrent simultanément. Le site marketing n'a aucune dépendance avec le code de l'API, et le scaffolding de l'API n'a aucune dépendance avec le site. Ils peuvent travailler en isolation complète.
Vague 2 (parallèle, après la vague 1) : Une fois que api-architect a terminé la structure du projet -- Cargo.toml, arborescence de répertoires, définitions de modèles, schéma de base de données, types d'erreurs -- les agents core-engine et api-routes se débloquent. core-engine écrit la couche de services (planificateur, exécuteur, parseur NLP, notifications, secrets). api-routes écrit les handlers HTTP, les types requête/réponse, les middlewares, et l'assemblage du routeur. Les deux s'appuient sur le scaffolding mais travaillent dans des répertoires différents, donc ils avancent en parallèle.
Le website-builder fonctionne indépendamment tout du long. Il a en fait été le dernier à terminer parce qu'il a produit le plus gros fichier unique -- 597 lignes de HTML avec CSS et JavaScript inline.
Vague 1 : le scaffolding API
Le travail de l'agent api-architect était de créer les fondations sur lesquelles les deux autres agents allaient construire. C'est la tâche la plus critique dans la chaîne de dépendances -- si les types sont faux, ou si la structure de répertoires est maladroite, ou si le pattern de gestion d'erreurs est incohérent, tout en aval hérite de ces problèmes.
L'agent a produit la structure suivante :
0cron-core/
+-- Cargo.toml 28 dépendances
+-- Dockerfile build Rust multi-stage -> debian-slim
+-- .env.example 6 variables d'environnement
+-- src/
+-- main.rs Point d'entrée du serveur
+-- config.rs AppConfig depuis l'environnement
+-- error.rs Enum AppError + IntoResponse
+-- models/
| +-- mod.rs Réexportations
| +-- user.rs Struct User
| +-- team.rs Team + TeamMember
| +-- job.rs Job + JobConfig + RetryConfig
| +-- execution.rs Struct Execution
| +-- secret.rs Struct Secret
| +-- monitor.rs Struct Monitor
| +-- api_key.rs Struct ApiKey
+-- db/
| +-- mod.rs Struct Database + connect + migrate
| +-- migrations/
| +-- 001_initial.sql Schéma complet (8 tables + index)
+-- services/
| +-- mod.rs Déclarations de modules (stubs)
+-- api/
+-- mod.rs Squelette du routeur + AppState
+-- types.rs Placeholder
+-- middleware/
+-- handlers/Le livrable critique était le struct AppState et le schéma de base de données. Tout le reste du système découle de ces deux artefacts.
L'AppState est volontairement minimal :
rust#[derive(Debug, Clone)]
pub struct AppState {
pub db: Database,
pub redis: redis::Client,
pub config: Arc<AppConfig>,
}Trois champs. Database encapsule un PgPool de SQLx. redis::Client est la fabrique de connexions pour Redis. AppConfig contient les paramètres dérivés de l'environnement, encapsulés dans un Arc pour un clonage peu coûteux entre les tâches asynchrones.
Ce struct est passé comme état Axum à chaque handler. Pas de framework d'injection de dépendances, pas de trait objects, pas de pattern service locator. Le pool de base de données gère le multiplexage des connexions en interne. Le client Redis crée des connexions à la demande. La configuration est immuable après le démarrage.
Le schéma à 8 tables correspond directement au domaine :
| Table | Objectif | Choix de conception clé |
|---|---|---|
users | Comptes avec e-mail, hash de mot de passe, OAuth, fuseau horaire, plan | ID client Stripe stocké directement pour l'intégration facturation |
teams | Conteneurs organisationnels | Chaque utilisateur reçoit une équipe par défaut à l'inscription |
team_members | Membres avec rôles (admin, éditeur, lecteur) | Clé primaire composite sur (team_id, user_id) |
jobs | Définitions des tâches cron | config en JSONB pour la flexibilité ; statistiques runtime dénormalisées |
executions | Historique complet des exécutions | Requête et réponse capturées ; suppression en cascade avec la tâche |
secrets | Valeurs chiffrées en AES-256-GCM | Scopées à l'équipe, uniques sur (team_id, key) |
monitors | Moniteurs heartbeat/ping | Token de ping unique pour les vérifications de santé entrantes |
api_keys | Tokens d'authentification | Hash stocké, préfixe indexé pour la recherche |
Vague 1 : le site marketing (en parallèle)
Pendant que api-architect disposait les types Rust et les schémas SQL, website-builder produisait un livrable complètement indépendant : la page marketing.
Le brief était précis : un fichier HTML unique avec CSS et JavaScript inline, correspondant au système de design de 0diff.dev (le site du premier produit ZeroSuite), avec une internationalisation complète anglais/français et un bouton de basculement thème clair/sombre.
L'agent a livré 597 lignes couvrant 15 sections :
- Navigation fixe avec effet blur et menu hamburger pour mobile
- Section héros avec démo de terminal animée et barre de statistiques
- Énoncés de problèmes -- six cartes de points de douleur avec citations de développeurs
- Barre de statistiques de coût (38 % du temps dev sur les problèmes de planification, 4 100 $ de coût annuel moyen, 12 h de temps de débogage moyen, 73 % utilisant des outils gratuits peu fiables)
- Vitrine de fonctionnalités -- neuf cartes avec extraits de code intégrés
- Démo de terminal interactive montrant l'interface du tableau de bord
- Onglets de cas d'utilisation (Indie, Équipe, WordPress, DevOps) avec scénarios spécifiques
- Carte de tarification avec effet de lueur verte et comparaison avec la concurrence
- Flux « Comment ça marche » en trois étapes (Définir, Connecter, Relaxer)
- Tableau de comparaison avec cron-job.org, EasyCron et Cronhub
- Exemples de code API avec vues à onglets (JavaScript, Python, cURL, Go)
- Exemples de planification montrant le mapping NLP vers cron
- Promotion croisée de l'écosystème ZeroSuite (six cartes produits)
- Appel à l'action final
- Pied de page
Chaque chaîne de texte utilise des attributs data-i18n. Une fonction switchLanguage() permute tout le contenu textuel entre anglais et français. La préférence de langue persiste dans localStorage et se détecte automatiquement depuis le navigator.language du navigateur à la première visite.
Vague 2 : le moteur central
Une fois le scaffolding en place, core-engine a reçu les modèles, le schéma de base de données et les types d'erreurs comme contexte. Sa mission : implémenter les sept modules de services qui contiennent toute la logique métier.
C'est là que résident les vraies décisions d'ingénierie. Les services sont l'endroit où la théorie de la planification rencontre la réalité de la production.
Le fichier le plus important est scheduler.rs -- 198 lignes qui forment le coeur battant de l'ensemble du produit. Nous le couvrons en détail dans la partie 3 de cette série, mais la boucle principale mérite d'être montrée ici :
rustpub struct Scheduler {
redis: redis::Client,
pool: PgPool,
config: Arc<AppConfig>,
}
impl Scheduler {
pub fn start(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
tracing::info!("Scheduler started");
loop {
if let Err(e) = self.poll().await {
tracing::error!("Scheduler poll error: {e}");
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
})
}
}Une tâche tokio en arrière-plan qui poll chaque seconde. À l'intérieur de poll(), elle interroge un sorted set Redis pour les tâches dont le score (horodatage de la prochaine exécution) est inférieur ou égal à now, acquiert un verrou distribué pour empêcher la double exécution, lance une tâche d'exécution, et replanifie la tâche pour sa prochaine exécution.
L'exécuteur (executor.rs, 309 lignes après évolution) gère le cycle de vie réel de la requête HTTP : interpolation des secrets dans les URL, en-têtes et corps ; timeout configurable ; retry avec backoff exponentiel, linéaire ou fixe ; journalisation complète requête/réponse dans la table executions ; et dispatch des notifications en cas de succès ou d'échec.
Le parseur NLP (nlp_parser.rs, 151 lignes) traduit le langage naturel en expressions cron en utilisant environ 20 patterns regex. Pas d'inférence LLM, pas d'appels API externes. Déterministe, auditable, rapide.
L'inventaire complet des services produits par cet agent :
| Service | Lignes | Responsabilité |
|---|---|---|
scheduler.rs | 198 | Polling de sorted set Redis, verrous distribués, calcul du prochain run |
executor.rs | 228 | Exécution HTTP, interpolation de secrets, logique de retry, journalisation des résultats |
nlp_parser.rs | 151 | Traduction du langage naturel en expressions cron (~20 patterns) |
notifications.rs | 229 | Slack Block Kit, embeds Discord, API Bot Telegram, webhook générique, e-mail |
secrets.rs | 92 | Chiffrement/déchiffrement AES-256-GCM, interpolation ${secrets.KEY} |
monitors.rs | 104 | CRUD heartbeat, génération de tokens de ping, détection de pings manqués |
jobs.rs | 272 | CRUD complet, résolution de planification, pause/reprise, déclenchement manuel, statistiques |
Total de core-engine : 1 274 lignes de logique métier.
Vague 2 : les routes API (en parallèle)
Pendant que core-engine implémentait les services, api-routes construisait la couche HTTP -- handlers, types requête/réponse, middleware d'authentification, et le routeur.
L'assemblage du routeur montre la surface API complète :
rustfn api_routes() -> Router<AppState> {
Router::new()
// Auth (pas d'auth requise)
.route("/auth/register", post(handlers::auth::register))
.route("/auth/login", post(handlers::auth::login))
// Jobs
.route("/jobs",
post(handlers::jobs::create_job)
.get(handlers::jobs::list_jobs))
.route("/jobs/{id}",
get(handlers::jobs::get_job)
.put(handlers::jobs::update_job)
.delete(handlers::jobs::delete_job))
.route("/jobs/{id}/trigger", post(handlers::jobs::trigger_job))
.route("/jobs/{id}/pause", post(handlers::jobs::pause_job))
.route("/jobs/{id}/resume", post(handlers::jobs::resume_job))
.route("/jobs/{id}/history", get(handlers::jobs::get_job_history))
.route("/jobs/{id}/stats", get(handlers::jobs::get_job_stats))
// Monitors
.route("/monitors",
post(handlers::monitors::create_monitor)
.get(handlers::monitors::list_monitors))
.route("/ping/{token}", get(handlers::monitors::ping))
// Account
.route("/account", get(handlers::account::get_account))
.route("/account/usage", get(handlers::account::get_usage))
}14 endpoints dans le build initial. Conventions REST standard : POST pour la création, GET pour la récupération, PUT pour les mises à jour, DELETE pour la suppression. Routes imbriquées pour les actions spécifiques aux tâches (déclenchement, pause, reprise, historique, statistiques).
Le middleware d'authentification supporte deux mécanismes -- tokens JWT pour les utilisateurs du tableau de bord et clés API pour l'accès programmatique :
rust// From middleware/auth.rs
// JWT: Authorization: Bearer <jwt-token>
// API Key: Authorization: Bearer 0c_<api-key>Les clés API sont préfixées par 0c_ pour que le middleware puisse les distinguer des tokens JWT au moment du parsing. La clé elle-même est hashée en SHA-256 avant stockage ; seuls les 8 premiers caractères (le préfixe) sont stockés en clair pour l'identification dans le tableau de bord.
L'agent api-routes a aussi produit types.rs -- 257 lignes de DTO requête et réponse. Chaque handler a une entrée typée (validée avec le crate validator) et une sortie typée (sérialisée avec serde). Pas de manipulation JSON brute dans les handlers ; le système de types impose le contrat API.
La fusion : 0 erreur, 53 warnings
Quand les quatre agents ont terminé, le chef d'équipe a fusionné leurs changements. C'est le moment de vérité de tout build parallèle -- est-ce que les pièces s'assemblent vraiment ?
cargo checkRésultat : 0 erreur. 53 warnings.
Chaque warning était le même : « unused import » ou « unused variable ». C'est attendu et correct. L'agent core-engine a écrit des fonctions de service que les handlers de l'agent api-routes appellent -- mais comme les agents travaillaient en parallèle, aucun ne pouvait vérifier les sites d'appel pendant le développement. Le compilateur voyait les fonctions de service comme non utilisées parce qu'il analysait chaque crate de manière holistique, et certains imports étaient déclarés mais leur utilisation n'avait pas encore été câblée.
Zéro erreur signifie que les contrats de types étaient corrects. Le struct AppState correspondait entre api/mod.rs et main.rs. Les types de modèles dans services/ correspondaient à ce que handlers/ attendait. Les types de retour des requêtes base de données correspondaient aux structs de modèles. Le type d'erreur implémentait IntoResponse correctement.
Ce n'est pas accidentel. C'est la conséquence de trois choses :
- Le scaffolding était bien conçu. L'agent
api-architecta défini les types partagés, le pattern d'erreur, et le struct d'état avant que quiconque ne commence à écrire du code. Chaque agent suivant a construit contre une interface stable.
- Le système de types de Rust attrape les erreurs d'intégration à la compilation. Si
core-engineavait défini une signature de fonction queapi-routesappelait incorrectement,cargo checkl'aurait attrapé. Pas besoin de tests d'intégration pour la correction structurelle de base.
- Les agents travaillaient dans des répertoires clairement délimités.
core-enginepossédaitservices/.api-routespossédaitapi/handlers/,api/types.rs, etapi/middleware/. Il n'y a eu aucun conflit au niveau des fichiers parce que le travail était partitionné par répertoire, pas par fonctionnalité.
Le bilan final
Voici ce que la session a produit :
| Livrable | Fichiers | Lignes | Agent |
|---|---|---|---|
| Site marketing | 4 | 597 (HTML) | website-builder |
| Scaffolding projet Rust | 14 | ~400 | api-architect |
| Services de logique métier | 8 | 1 274 | core-engine |
| Handlers HTTP + routes | 7 | ~900 | api-routes |
| Partagé (modèles, config, erreur, DB) | 12 | ~500 | api-architect |
| Total | 41 | ~3 671 | 4 agents |
Dépendances résolues :
Cargo.tomlavec 28 dépendances de crates (Axum, Tokio, SQLx, Redis, reqwest, serde, jsonwebtoken, argon2, aes-gcm, cron, chrono, uuid, et plus)- Dockerfile multi-stage pour les builds de production
- Migration de base de données avec 8 tables, index appropriés, et contraintes de clés étrangères
Ce que la session n'a pas produit :
- Tests (délibérément reportés -- nous préférons stabiliser la surface API avant d'écrire des tests d'intégration)
- Tableau de bord frontend (session séparée, agents séparés)
- Configuration de déploiement (géré dans une session ultérieure avec Docker Compose)
- Intégration de facturation (ajoutée dans une session de suivi le 11 mars)
Pourquoi cette approche fonctionne
Le build parallèle à quatre agents n'est pas la bonne approche pour tout. Il fonctionne spécifiquement quand :
- Les livrables sont clairement séparables. Un site marketing et une API Rust ont zéro chevauchement de code. La logique de service et les handlers HTTP vivent dans des répertoires différents et communiquent par des signatures de fonctions.
- Le graphe de dépendances est peu profond. Deux vagues, pas cinq. Si l'agent C dépend de l'agent B qui dépend de l'agent A, vous avez un pipeline sériel déguisé en système parallèle. Notre graphe avait un seul arc de dépendance : la vague 2 dépendait du scaffolding de la vague 1.
- Les contrats d'interface sont établis en amont. L'agent de scaffolding a défini les types avant que les agents d'implémentation ne commencent. C'est l'équivalent du build parallèle de l'écriture de la spec API avant d'écrire le code.
- Le langage a un système de types fort. Le compilateur Rust est, en effet, un cinquième agent -- un testeur d'intégration qui s'exécute après la fusion et attrape chaque incompatibilité structurelle. Dans un langage dynamiquement typé, les builds parallèles comportent un risque d'intégration significativement plus élevé.
Le temps total d'horloge pour cette session a été un seul après-midi. Un build séquentiel à un seul agent aurait pris plus de temps, non pas à cause du débit brut, mais à cause du changement de contexte. Quand un agent écrit le planificateur, il maintient tout le domaine de planification en contexte. Quand un seul agent écrit le planificateur puis pivote pour écrire les traductions françaises du site marketing, il paie un coût de changement de contexte à chaque transition.
Le parallélisme ne concerne pas seulement la vitesse. Il s'agit de garder chaque agent profondément ancré dans un seul domaine.
Ce qui s'est passé ensuite
Cette session a produit le squelette v0.1. Au cours des semaines suivantes, nous avons ajouté :
- Système d'administration (11 mars) -- gestion des utilisateurs, liste globale des tâches, statistiques de la plateforme
- Intégration facturation (11 mars) -- Stripe Checkout, portail client, suivi de l'essai, application des plans
- Google OAuth -- connexion sociale en plus de l'e-mail/mot de passe
- API tableau de bord -- endpoint de résumé agrégeant les compteurs de tâches, les taux d'exécution et les métriques d'échec
- Endpoint d'analytics -- données d'exécution en séries temporelles pour les graphiques du tableau de bord
- API de gestion des notifications -- configuration des notifications par utilisateur et par canal
- Gestion des clés API -- créer, lister et révoquer des clés API avec permissions granulaires
- Gestion des secrets -- stockage de secrets chiffrés et interpolation dans les configurations de tâches
- API de formulaire de contact -- endpoint public pour le site marketing
- Historique des transactions -- journal des événements de facturation
La base de code est passée de 2 852 lignes de Rust à considérablement plus, mais l'architecture établie lors de cette première session -- le pattern AppState, la séparation service/handler, le planificateur adossé à Redis, la config de tâche en JSONB -- est restée intacte tout du long. Le scaffolding a tenu.
C'est tout l'intérêt de bien réussir la première session.
Ceci est la partie 2 d'une série en trois parties sur la construction de 0cron.dev. Prochain article : Construire un moteur de planification cron en Rust -- une plongée en profondeur dans les sorted sets Redis, le verrouillage distribué, et l'architecture de polling par tick qui fait tourner le planificateur de 0cron (littéralement).