Dans l’article précédent, je parlais de “mes autres side projets, dont je reparlerai sûrement”. Voilà, on y est : parlons de Moelan App, le projet qui m’occupe le plus en ce moment.
C’est quoi, Moelan App ?
Une caisse noire numérique pour mon équipe de handball. Chaque joueur a un solde, on enregistre ses consommations (bière, soft), ses amendes, ses crédits, et tout est historisé. L’objectif final est très sérieux : avec le surplus en fin de saison, on se paie un week-end à Moelan-sur-Mer. D’où le nom ^^.
Au départ, c’était un truc minuscule : une liste de joueurs et quatre boutons. Mais comme c’est un side project, et que j’aime bien me compliquer la vie, ça a pas mal grossi. L’app est multi-équipes (chaque équipe a son “espace”, validé par un super-admin), avec des rôles (admin, membre, joueur), un règlement que les joueurs peuvent amender, un suivi des matchs, des licences, des paris… Bref, bien plus qu’une caisse noire.
Ce qui est en place
La stack
- Backend en Rust (axum + sqlx), sans ORM : j’ai peu de tables et des requêtes simples, SQL vérifié à la compilation, et les migrations s’appliquent au démarrage.
- Frontend en Flutter, un seul code pour le web et Android.
- PostgreSQL pour les données.
- Keycloak pour l’authentification, celui dont je parlais dans l’article précédent. Le backend valide les JWT, et un client “service account” lui permet de créer les groupes d’un nouvel espace ou les comptes de joueurs invités.
- GitLab CI qui construit les images Docker (une pour le backend, une par environnement pour le front, puisque Flutter fige l’URL de l’API au build) et l’APK / AAB Android.
Deux environnements : prep et prod. Une branche qui passe sur main part en prep, un tag X.Y.Z part en prod. Les notes de version et le CHANGELOG sont générés à partir des jalons et des labels des issues GitLab (un petit script maison, testé contre un faux GitLab, parce que oui, je teste aussi mes scripts).
Une règle d’or : l’argent en centimes, le grand livre fait foi
Quand on manipule de l’argent, même pour de la bière à 1 €, on ne triche pas : tout est en centimes entiers, jamais de flottant. Et chaque mouvement est une ligne du grand livre (la table transactions). Le solde d’un joueur n’est qu’un cache, recalculable à tout moment depuis cet historique. Une annulation ne supprime rien : elle ajoute une écriture inverse. Une contrainte CHECK en base rend même une ligne incohérente impossible à insérer.
Une spec vivante, avec du Cucumber
Ceux qui ont lu mon article sur Cucumber ne seront pas surpris. Le dossier spec/ contient :
- la prose de chaque fonctionnalité (le pourquoi, les règles métier, les limites connues) ;
- les mêmes règles en Gherkin, soit environ 325 scénarios qui tournent à chaque
cargo test.
Ils s’exécutent contre le vrai routeur, les vrais extracteurs d’autorisation et un vrai Postgres. Seul Keycloak est simulé en mémoire, mais avec de vrais JWT RS256 : rien de la chaîne d’authentification n’est court-circuité. Il y a même une matrice des rôles qui dit qui peut faire quoi, et quel code HTTP il reçoit quand la réponse est non. Quand la prose et le Gherkin divergent, c’est le Gherkin qui a raison : lui, il tourne.
Un workflow par issue, avec l’IA dans la boucle
Chaque évolution passe par une issue GitLab avec un gabarit qui reprend ce découpage prose / Gherkin. J’ai trois skills Claude Code dans le dépôt : une pour préparer l’issue (écrire la spec, créer la branche et la MR en brouillon), une pour l’implémenter, et une pour la remettre en question (rechallenger la spec, même quand le code existe déjà). Je reviendrai sûrement là-dessus dans un article dédié, parce que c’est devenu le cœur de ma façon de travailler sur ce projet.
Les à-côtés qui font une “vraie” app
- Analytics avec Umami côté web : pas de cookie, pas de donnée personnelle, donc pas de bandeau de consentement. Comme l’app est en routage par hash, j’envoie les pages à la main, par motif de route (
/players/:id, jamais l’identifiant réel), plus quelques événements métier. Aucun nom, email ou montant ne sort. - Pages légales et RGPD : politique de confidentialité, CGU, mentions légales, un écran “Mon compte” avec export des données et suppression du compte. Les textes sont un premier jet à faire relire : je ne suis pas juriste.
- Alertes email quand un joueur passe sous un seuil de dette, via un relais SMTP générique (pas de SDK propriétaire).
- Notifications push sur Android, à chaque action sur le compte d’un joueur.
- Sauvegardes Postgres par script, avec rotation sur les 14 dernières.
Un principe tenu partout : tout ce qui est optionnel échoue en douceur. Pas de SMTP configuré ? Les alertes sont journalisées au lieu d’être envoyées. Pas de projet Firebase ? Le push ne fait rien, sans jamais bloquer le reste. Résultat : l’app complète tourne en local sans compte chez qui que ce soit.
Ce qui manque : l’observabilité
C’est là que je suis le plus honnête avec moi-même : aujourd’hui, mon observabilité est quasi inexistante. Ce que j’ai :
- des logs
tracingsur la sortie standard, une trentaine de lignesinfo/warn/error; - une route
/healthqui répond{"status": "ok"}… sans rien vérifier du tout. Elle me dit que le processus tourne, pas que la base répond ni que Keycloak est joignable ; - Umami, qui me dit combien de personnes viennent, pas si ça marche pour elles.
Autrement dit, si l’app plante un samedi soir de match, je le saurai quand un coéquipier m’enverra un message. Ce n’est pas tenable. Voilà ce que je veux mettre en place, dans l’ordre où je compte le faire :
- Un vrai
/health(ou plutôt un/ready) qui teste la connexion Postgres et la joignabilité de Keycloak, séparé d’un simple “liveness” pour que Docker et Traefik fassent la différence. - Du monitoring de disponibilité avec Uptime Kuma : une ping toutes les minutes, une alerte sur mon téléphone si ça tombe.
- Des logs structurés (JSON) avec un identifiant de requête, centralisés dans Loki et consultables dans Grafana. Aujourd’hui je fais des
docker logs | grep, ça a ses limites. - Des métriques : un endpoint Prometheus côté Rust (latence par route, taux d’erreurs, taille du pool sqlx), et des tableaux de bord Grafana. Une fois que j’ai un joli graphique, je n’ai plus de raison de ne pas le regarder.
- Du suivi d’erreurs avec GlitchTip, auto-hébergeable et compatible avec les SDK Sentry, côté Rust et côté Flutter. L’équivalent de Crashlytics, mais sans envoyer les crashs de mes coéquipiers chez Google.
- À terme, peut-être de la trace distribuée avec OpenTelemetry. Pour une app de cette taille, c’est clairement du luxe, mais c’est aussi un side project : j’ai le droit de m’amuser.
Tout ça tient sur le mini PC, et ça colle bien avec l’idée de ne plus être limité aux technos “compatibles Firebase”.
Ce qui manque aussi : les retours des utilisateurs
L’observabilité technique ne dit pas tout. Le jour où un joueur trouve que “le bouton ne marche pas” ou “ce serait bien de pouvoir…”, comment ça remonte jusqu’à moi ? Aujourd’hui : par un message WhatsApp, vague, sans capture d’écran, sans version de l’app, sans rien. Un bug report de type “ça bug” est inexploitable.
Ce que je veux ajouter :
- Un bouton “Signaler un problème / proposer une idée” dans l’écran “Plus”, qui envoie un formulaire simple au backend.
- Le backend enrichit automatiquement le message : version de l’app, plateforme (web ou Android), rôle de l’utilisateur, écran courant, et l’identifiant de requête du dernier appel en erreur pour retrouver les logs correspondants.
- Il crée directement une issue GitLab avec le bon label (
fonctionneloutechnique, ceux que mon script de release utilise déjà). Ça boucle avec mon workflow : le retour d’un joueur devient une issue, qui devient une spec, puis une MR. - Côté joueur, un message de confirmation et, plus tard, une notification push quand son retour est traité. Rien ne démotive plus que d’écrire dans le vide.
Dans la même veine : j’ai déjà l’écran qui permet aux joueurs de proposer un article du règlement, preuve que les gens sont prêts à contribuer quand c’est simple.
Et la souveraineté dans tout ça ?
Parce que dans l’article précédent, je me suis promis que “tout ce qui peut tourner chez moi, tournera chez moi”. Sur Moelan App, c’est en partie vrai, et en partie impossible.
Ce qui est souverain
Le backend Rust, PostgreSQL, Keycloak, Umami, les emails via n’importe quel relais SMTP, le front web servi par nginx : tout ça peut vivre sur mon mini PC, ou chez un hébergeur français. Le code est sur ma forge, les données sont chez moi. Même Flutter, bien que né chez Google, est open source et je ne dépends d’aucun service de leur part pour le compiler.
Ce qui ne l’est pas, et ne le sera pas (vraiment)
Il y a un angle mort, et il est gros : Android et Firebase.
- Les notifications push passent par Firebase Cloud Messaging. Sur Android, c’est le seul canal qui réveille proprement une app fermée, sans maintenir une connexion permanente qui vide la batterie. Mon backend envoie à Google, Google livre au téléphone. Même si je ne mets rien de sensible dans le message, Google voit passer qui reçoit quand. Et pour l’avoir, j’ai dû créer un projet Firebase par environnement : exactement ce que je viens de quitter pour le blog.
- Android lui-même. Le Play Store pour distribuer, les Google Play Services pour que les push arrivent, un compte Google sur le téléphone de la majorité de mes coéquipiers. Je peux publier un APK en direct, mais je ne vais pas demander à tout le monde d’activer les “sources inconnues” et de faire les mises à jour à la main.
- Les utilisateurs eux-mêmes. Ils ne sont pas dans une démarche de souveraineté, ils veulent une app qui marche sur leur téléphone. Je ne peux pas leur imposer un Android dégooglisé.
- Et iOS ? Même constat, côté Apple (APNs), sans parler du Mac nécessaire pour compiler : c’est pour ça que l’app n’est pas disponible dessus.
Ce que je peux faire quand même
- Limiter les dégâts : des notifications au contenu minimal, le détail étant récupéré auprès de mon API à l’ouverture de l’app, pour que Google ne voie transiter ni noms ni montants.
- Garder le push optionnel (c’est déjà le cas) pour qu’une instance qui ne veut pas de Firebase puisse s’en passer.
- Explorer UnifiedPush (avec un serveur ntfy auto-hébergé) pour les gens qui voudraient s’affranchir de Google. Ça ne servirait qu’à une poignée de curieux, mais le principe me plaît.
- Rester honnête : le dire clairement dans la politique de confidentialité, plutôt que d’afficher une souveraineté que je n’ai pas.
Voilà donc la conclusion : je peux maîtriser mon infrastructure, pas celle des téléphones de mes coéquipiers. C’est une limite, et j’ai décidé de la regarder en face plutôt que de l’ignorer.
Le mot de la fin
Moelan App est passée de “quatre boutons” à une vraie application, avec une spec exécutable, une CI, deux environnements et une gestion des droits solide. Il lui manque maintenant la partie que personne ne voit tant que tout va bien : savoir ce qui se passe quand ça va mal, et écouter ceux qui l’utilisent.
Au programme des prochains articles, selon ce que j’arrive à faire en premier : la stack d’observabilité sur le homelab, le formulaire de retour qui crée des issues GitLab, et mon workflow avec l’IA.
À bientôt. Ronan.