Motorz

Le carnet de bord que je me suis écrit pour suivre les pleins, l'entretien, les pneus et le coût réel de mes véhicules, même sans réseau.

Période
mai à juillet 2026
Nature
Projet perso
Statut
Prototype

Seul aux commandes : le produit, l'app Flutter, l'API Bun, la CI/CD et le packaging.

Flutter TypeScript PostgreSQL Docker

Pourquoi je m’y suis mis

Je voulais savoir ce que me coûtent vraiment mes véhicules. Aujourd’hui, ça suppose de croiser des factures papier, des notes sur mon téléphone et l’application de la station-service — et en pratique, je ne le fais jamais.

Je me suis donc écrit le carnet que je cherchais. Trois contraintes que je me suis posées dans le brief : un seul endroit pour tout l’historique, une saisie possible au bord de la route — donc sans compter sur le réseau — et un partage avec les autres personnes qui conduisent la même voiture.

Motorz réunit donc l’historique d’un véhicule : pleins et consommation, opérations d’entretien et devis, pneus et pressions, sessions de diagnostic OBD, dépenses. Le partage d’un véhicule entre plusieurs comptes se fait par code.

Ce que j’ai construit

Une application Flutter déclarée pour six cibles (Android, iOS, macOS, Windows, Linux, web), adossée à une API REST Bun / Hono / Drizzle sur PostgreSQL. L’authentification passe par un code à usage unique envoyé par SMS puis un JWT HS256 ; les médias vont sur kDrive ; une chaîne de publication livre le même tag sur Android, macOS, Windows, Linux et web.

Côté app, j’ai gardé une architecture hexagonale découpée par couches : un domaine en Dart pur, une couche application, une couche infrastructure où je confine Riverpod, et l’UI. Côté API, un découpage routes / schémas Zod / lib / db. Le contrat offline-first, je l’ai porté des deux côtés : chaque table synchronisable a updated_at et deleted_at, jamais de DELETE physique, résolution last-write-wins.

La décision que j’ai payée

J’ai fait de la base locale ma source de vérité plutôt que d’appeler l’API directement. Toutes les lectures viennent de sqflite, les écritures partent d’abord en local puis dans une file d’attente, et je génère les identifiants en UUID côté client pour rendre le push idempotent.

Je l’ai payé : un moteur de synchronisation à écrire et à maintenir — file FIFO sérialisée par un Lock, curseur GET /sync/changes?since=, tombstones, store des opérations rejetées — et une logique de dérivation que je duplique côté client. Le web, lui, je l’ai laissé de côté : il garde son store en mémoire et repart d’un pull complet à chaque rechargement.

Trois choses que j’ai aimé faire

  • Une release atomique sur tag v*. Un même tag me produit l’Android (AAB + APK), le macOS (.pkg), le Windows, le Linux (AppImage) et le web, passe une étape de vérification, puis publie sur la piste interne du Play Store, sur TestFlight et sur Docker Hub.
  • Un updater desktop maison, que j’ai écrit en Dart et compilé en natif, faute de canal de distribution sur Windows et Linux : une installation interactive une fois, puis des mises à jour silencieuses depuis les GitHub Releases.
  • L’import de mes rapports de diagnostic OBD. Le PDF est lu côté API, et j’ai volontairement fait un parsing tolérant : il pré-remplit un formulaire que je relis, et se replie sur la simple détection des codes défaut quand le format ne lui dit rien.

Ce qui m’a résisté

Le miroir GitLab → GitHub. Le miroir natif est asynchrone et best-effort : mon tag arrivait en retard, parfois pas du tout, et ne déclenchait donc jamais les GitHub Actions de publication. J’ai ajouté un job dédié (mirror-to-github, stage .post) qui force le push des branches et des tags et échoue visiblement s’il rate. La boucle de release, je l’ai cassée avec une règle qui ignore les commits chore(release).

L’absence de CORS sur mon API, qui m’interdisait de servir le client web sur un sous-domaine dédié. Plutôt que d’ajouter un middleware, j’ai mis le web et l’API sur la même origine à un préfixe de chemin près, et le client dérive son URL d’API de window.location.origin. Effet de bord que je ne boude pas : plus aucune URL figée dans l’image Docker.

L’essayer sans rien installer d’autre

Les binaires sont sur la page des releases GitHub. Pas besoin de monter un serveur pour voir à quoi ça ressemble : l’app embarque une implémentation in-memory des dépôts, que je peux substituer à celle qui parle à l’API. C’est exactement ce que la clean architecture m’achète ici — le domaine ne sait pas d’où viennent ses données, donc je change d’adaptateur et l’application tourne en autonomie complète.

Si tu veux la version connectée, il n’y a pas d’inscription libre : les comptes sont créés à la main. Écris-moi à timothe@hofmann.fr et je t’ouvre un accès.

← Retour aux projets