Ever Teams

Travail d'architecture sur l'application web Next.js d'une plateforme de travail open source : migration d'état, réorganisation du core, React Query, bascule d'espace de travail.
- Rôle
- Contributeur principal de l'application web, travail d'architectureEver Technologies
- Période
- Depuis oct. 2023
- Liens
Contexte et problème
Ever Teams est une plateforme open source de travail et de productivité publiée par Ever Technologies sous licence AGPL-3.0 : suivi du temps, gestion des tâches et gestion d'équipe dans un seul produit, avec son site produit sur ever.team. Le code vit dans un monorepo TypeScript ; l'application web Next.js se trouve dans apps/web et dialogue avec une API NestJS. J'ai rejoint Ever Technologies en octobre 2023. Ma première pull request sur le dépôt, #1643, date du 25 octobre 2023 et a ajouté l'internationalisation avec i18next.
Le travail que je décris ici est structurel. Une application web qui livre des fonctionnalités chaque semaine pendant des années accumule des raccourcis : des composants qui vivent à côté de la page qui en a eu besoin en premier, des hooks copiés d'une fonctionnalité à l'autre, des types déclarés en ligne, des données serveur et de l'état d'interface mélangés dans le même store. Chaque nouvelle fonctionnalité coûte un peu plus que la précédente, et un nouveau contributeur n'a aucune structure de dossiers pour s'orienter.
Les titres de mes pull requests fusionnées en 2025 décrivent assez bien l'état d'apps/web : il fallait centraliser les composants, les hooks et les types, et nettoyer le dossier app.
Contraintes
Le dépôt est public et compte 42 contributeurs. Chaque changement est relu au grand jour, la branche develop ne s'arrête jamais, et une refactorisation ne peut pas bloquer le travail sur les fonctionnalités. Dans ce contexte, un changement structurel ne peut pas attendre une fenêtre de gel : il doit arriver par morceaux assez petits pour être relus et annulés indépendamment, tout en gardant l'application déployable entre deux fusions.
Le produit est open source et peut être auto-hébergé, donc une pull request d'architecture doit être neutre en comportement : mêmes écrans, plomberie différente. Les conventions doivent aussi se lire dans l'arborescence elle-même, parce que les contributeurs vont et viennent et que personne ne lit un wiki avant d'ouvrir une pull request. Mon propre temps était partagé avec la relecture du travail des autres, 180 pull requests sur la période, ce qui favorise beaucoup de petites PR plutôt que quelques grosses.
Mon rôle précis
Entre le 25 octobre 2023 et le 22 mars 2026, j'ai ouvert 187 pull requests sur ever-co/ever-teams, dont 177 fusionnées, et relu 180 pull requests d'autres contributeurs. Au nombre de commits, je suis le 8e contributeur sur 42, avec 204 commits. Le volume est inégal : 5 pull requests en 2023, 15 en 2024, 145 en 2025 et 22 en 2026. Le travail d'architecture décrit ci-dessous est l'histoire de 2025.
Mon intitulé chez Ever Technologies est Senior Product Engineer. Sur Ever Teams, la description honnête est contributeur principal de l'application web : j'ai réalisé le travail d'architecture sur apps/web décrit ci-dessous, dans le code et en relecture, et j'ai pris part aux décisions d'architecture qui l'entourent.
Mon travail chez Ever Technologies porte aussi sur l'IA : fonctions de collaboration et parcours de productivité appuyés sur l'IA, et logique d'entraînement IA intégrée aux fonctions back-end. Cette page ne couvre que l'architecture de l'application web.
Décisions clés et compromis
Remplacer Recoil par Jotaiseptembre 2024
En 2024, le développement de Recoil avait visiblement ralenti alors que React et Next.js continuaient d'avancer. Jotai a un modèle d'atomes assez proche pour que l'essentiel de la migration soit mécanique : un atome pour un atome, useAtom pour useRecoilState, un atome dérivé pour un sélecteur. La migration est la #3037.
Les alternatives étaient de rester sur Recoil, coût nul aujourd'hui et risque croissant à chaque montée de version du framework, ou Zustand, dont le modèle à store unique aurait imposé de repenser la forme de l'état pendant le changement de bibliothèque. Un changement de bibliothèque de ce genre arrive généralement en une seule grosse pull request qui touche tous les consommateurs de l'état partagé, relue d'un bloc, parce qu'on ne peut pas la découper sans casser l'application entre deux étapes. La suite, #3855, a regroupé les stores Jotai par domaine, pour qu'un contributeur qui cherche l'état d'un domaine trouve un seul fichier.
// Un atome par élément d'état d'interface, des atomes dérivés pour les valeurs calculées.
import { atom } from 'jotai';
type Team = { id: string; name: string };
export const activeTeamIdAtom = atom<string | null>(null);
export const teamsAtom = atom<Team[]>([]);
export const activeTeamAtom = atom((get) => {
const id = get(activeTeamIdAtom);
return get(teamsAtom).find((team) => team.id === id) ?? null;
});Réorganiser apps/web par couche, en quatre pull requestsavril à mai 2025
Le débat opposait l'organisation par couche (core/components, core/hooks, core/types) à la colocalisation par fonctionnalité. Dans un produit construit autour du suivi du temps, des tâches et des membres d'équipe, les dossiers par fonctionnalité finissent de toute façon par s'importer les uns les autres. Une couche core avec un ensemble fixe de sous-dossiers donne une seule réponse à « où est-ce que ça va », et cette réponse est visible dans l'arborescence. J'ai découpé le déplacement en quatre pull requests : les composants dans la #3771, les hooks dans la #3777, les types dans la #3778, puis un nettoyage du dossier app dans la #3780, suivi de l'organisation des layouts dans la #3784.
L'alternative était une seule PR big-bang. Quatre PR plus petites, c'était quatre cycles de relecture et une fenêtre où l'arborescence était à moitié ancienne, à moitié nouvelle. Le coût retombe aussi sur les autres contributeurs : les pull requests ouvertes doivent se rebaser par-dessus les imports déplacés, et git blame a besoin de --follow pour traverser le déplacement. Je continue de penser qu'une étape réversible vaut plus qu'un diff propre.
Déplacer les données serveur vers React Query, une tranche par pull requestjuin 2025
Jusque-là, les données serveur passaient par des hooks écrits à la main, le schéma habituel où chaque hook finit par porter son propre indicateur de chargement et sa propre idée de la péremption. React Query donne un seul cache, la déduplication des requêtes, le rechargement en arrière-plan et l'invalidation explicite, et il sépare l'état serveur de l'état d'interface, ce que Jotai seul ne fait pas. SWR était l'autre candidat, plus léger mais plus mince sur les mutations et l'invalidation, et garder les hooks maison était la troisième option : un cache avec des règles de péremption est précisément ce qu'on ne veut pas écrire vingt fois à la main.
L'ordre comptait plus que la bibliothèque. Une PR de préparation, la #3864, a fait entrer React Query dans l'application avant qu'aucune donnée ne bouge. Puis une vingtaine de petites PR, de la #3872 à la #3915, ont déplacé les hooks une tranche à la fois, par exemple le hook de l'utilisateur authentifié dans la #3902. Le coût est une période où deux couches de données coexistent, et l'ensemble ne tient que si chaque mutation nomme les clés qu'elle invalide.
// Clés de requête sous forme de fabrique typée, un seul endroit à modifier, invalidation partielle facile.
export const queryKeys = {
teams: {
all: ['teams'] as const,
detail: (teamId: string) => ['teams', teamId] as const,
members: (teamId: string) => ['teams', teamId, 'members'] as const,
},
} as const;
// queryClient.invalidateQueries({ queryKey: queryKeys.teams.detail(id) })Résoudre l'espace de travail une seule fois, tout en hautjuillet 2025 et mars 2026
Dans un produit à plusieurs espaces de travail, presque chaque écran dépend de l'espace de travail actif. Le résoudre page par page revient à répéter sur chaque page la même garde et le même repli. Le sélecteur d'espace de travail (Workspace switcher) de la #3930 et la refonte du layout global de la #4286 sont les deux pull requests publiques sur ce chemin : le sélecteur fait de l'espace de travail actif un choix explicite et global, et le layout global est l'endroit où ce dont chaque page a besoin est résolu une seule fois, pour que les pages consomment le résultat au lieu de le reconstruire. Le coût, c'est que le layout global devient un chemin critique, un bug à cet endroit casse toutes les routes d'un coup, et tout changement y est gros par nature.
Ce que j'ai livré
- Internationalisation de l'application web avec i18next, #1643 (octobre 2023).
- Migration de l'état de Recoil vers Jotai, #3037 (septembre 2024), puis stores Jotai regroupés par domaine, #3855.
- Réorganisation d'
apps/weben une couchecore: #3771, #3777, #3778, #3780 et l'organisation des layouts #3784 (avril à mai 2025). - Adoption de React Query : préparation dans la #3864, puis migration domaine par domaine de la #3872 à la #3915 (juin 2025).
- Sélecteur d'espace de travail (Workspace switcher), #3930 (juillet 2025).
- Refonte du layout global, #4286 (mars 2026).
- 180 relectures de pull requests d'autres contributeurs sur la même période.
Tout ce qui précède est public et se lit pull request par pull request.
Résultats et chiffres
Chiffres publics, relevés le 28 septembre 2026 :
Au 28 septembre 2026, le dépôt compte 588 étoiles et 109 forks. Mon propre bilan s'y lit ainsi : 204 commits, 187 pull requests ouvertes, 177 fusionnées et 180 relues, entre le 25 octobre 2023 et le 22 mars 2026.
Je n'ai aucune mesure avant/après à publier, donc les effets restent qualitatifs. Après la réorganisation en core, un nouveau hook ou un nouveau composant a exactement un endroit où aller. Après la migration React Query, les données serveur ont un seul cache et un seul chemin d'invalidation au lieu d'un indicateur de chargement par hook. Après le travail sur le layout global, l'espace de travail est résolu une seule fois. Ce que ces changements devaient faire gagner, c'est du temps de relecture et du temps d'intégration des nouveaux contributeurs ; je n'ai mesuré ni l'un ni l'autre.
À propos des chiffres
Les étoiles et les forks appartiennent au projet, qui est le travail de 42 contributeurs. Les nombres de pull requests et de relectures sont les miens.
Ce que je ferais différemment
Si je recommençais la migration React Query, je livrerais la fabrique de clés de requête dans sa propre pull request avant de déplacer la première tranche, pour que la vingtaine de PR de migration soit relue face à une convention déjà en place. J'ajouterais une règle de lint interdisant les imports depuis les anciens chemins le jour même de chaque déplacement, pour que les rebases échouent bruyamment au lieu de compiler en silence contre un doublon. Et je mettrais un contrôle de taille de bundle dans l'intégration continue avant la migration d'état, ne serait-ce que pour avoir un chiffre à montrer aujourd'hui.
Côté humain, j'écrirais la liste de contrôle de migration dans le modèle de pull request. Une vingtaine de PR qui suivent la même recette, c'est le moment où un modèle se rentabilise.