Journal des décisions¶
Chaque entrée consigne un choix structurant, son contexte et ses conséquences. L'objectif est qu'une reprise du projet dans six mois n'ait pas à redécouvrir les arbitrages.
001 — PostgreSQL comme entrepôt unique¶
Contexte. DuckDB en mode serveur (protocole Quack) était envisagé comme moteur analytique, en complément de PostgreSQL.
Décision. PostgreSQL 18.6 seul.
Motif. Le protocole Quack a été évalué et fonctionne — serveur et client, lecture et écriture, sur le port 9494 en HTTP. Mais il est en beta. Un démonstrateur commercial ne peut pas reposer sur une brique dont l'interface peut changer.
Conséquences. Une brique de moins à exploiter. On perd l'argument « moteur analytique colonne ». La recette de conteneur reste documentée si l'on veut rebrancher DuckDB.
002 — Médaillon en base, convention dbt dans le code¶
Contexte. Deux conventions concurrentes : médaillon (bronze/silver/gold) et dbt (staging/intermediate/marts).
Décision. Schémas physiques médaillon, arborescence dbt idiomatique.
Motif. Elles ne sont pas au même niveau. Médaillon est un patron d'architecture qui se raconte en vingt secondes devant un décideur ; la convention dbt est une convention de code, précise et outillée. Un démonstrateur commercial a besoin des deux : une architecture qui se raconte, un code qui tient la route si le prospect demande à voir le dépôt.
Conséquences. Une macro generate_schema_name pour neutraliser le
préfixage par défaut de dbt.
003 — Airflow en LocalExecutor¶
Décision. Pas de Redis ni de workers Celery.
Motif. La configuration de référence du projet Airflow les embarque pour permettre la répartition sur plusieurs machines. Sur une machine unique, c'est deux services de plus à maintenir pour aucun bénéfice.
Conséquences. Le parallélisme reste assuré. La bascule vers Celery reste possible sans changer les DAGs.
004 — dbt dans un virtualenv isolé¶
Décision. dbt installé dans /opt/dbt, appelé par chemin absolu.
Motif. dbt et Airflow se disputent régulièrement les mêmes dépendances (Jinja2, protobuf). Les isoler supprime une classe entière de pannes à la montée de version.
Conséquences. Ne jamais ajouter /opt/dbt/bin au PATH : l'interpréteur
Python du virtualenv masquerait celui d'Airflow. Erreur commise puis corrigée.
005 — Consoles d'administration strictement privées¶
Contexte. Airflow devait initialement être exposé sur
airflow.metrika.fr, Metabase sur metabase.metrika.fr.
Décision. Aucune console n'est joignable depuis Internet. Accès par tunnel SSH uniquement.
Motif. Une console d'administration exposée n'est protégée que par un mot de passe, sur une IP scannée en permanence. Le tunnel SSH s'appuie sur une clé.
Conséquences. AIRFLOW__API__BASE_URL doit valoir http://localhost:8080 :
un domaine public y ferait rediriger l'utilisateur vers une URL inexistante.
Metabase n'est routé que sur une liste blanche de chemins.
006 — Liens publics plutôt qu'embarquement signé¶
Décision. Les tableaux de bord sont publiés via les liens publics Metabase.
Motif. L'embarquement signé impose un composant serveur capable de signer un JWT — le calcul ne peut pas se faire dans le navigateur sans exposer la clé. Les données commerciales étant fictives et l'open data public, le surcoût n'est pas justifié.
Conséquences. Les liens sont permanents et non filtrables : quiconque connaît l'URL voit le tableau de bord. Inacceptable pour des données client réelles — il faudrait alors l'embarquement signé. Les liens sont révocables.
007 — Vitrine statique, pas d'application¶
Contexte. Une application FastAPI était prévue au cahier des charges.
Décision. Un serveur de fichiers statiques (nginx).
Motif. Sans embarquement signé, sans API publique et sans formulaire, un serveur applicatif n'apporte rien à une page qui affiche des iframes.
Conséquences. Aucune surface d'attaque applicative. La base metrika
reste inutilisée — elle était destinée aux données opérationnelles de
l'application.
008 — Provisionnement Metabase par script¶
Décision. Tableaux de bord créés par scripts/metabase_provision.py, en
requêtes structurées (MBQL).
Motif. La sérialisation (export/import YAML) est réservée aux éditions Pro/Enterprise. Les X-ray sont instantanés mais rien n'est sauvegardé. Restait l'API REST — qui a l'avantage d'être versionnable et rejouable.
MBQL plutôt que SQL brut pour préserver le drill-through : cliquer sur une barre pour descendre dans le détail. C'est le moment qui fait mouche en démonstration, et le SQL brut le supprime.
Conséquences. Le script écrase les modifications manuelles sur les objets qu'il gère. Toute personnalisation doit être portée dans le script.
009 — Comportement d'achat dépendant du segment¶
Contexte. La première version du générateur tirait le client uniformément. Résultat : les TPE pesaient 1,12 Md€ contre 173 M€ pour les grands comptes, et un chiffre d'affaires total de 2,76 Md€.
Décision. Chaque segment porte un profil : fréquence de commande, quantités, nombre de lignes, remises, attrition, nombre de postes.
Motif. Un prospect repère l'incohérence en trois secondes, et toute la crédibilité de la démonstration tombe avec elle.
Conséquences. 52,7 M€/an de produit, 32,7 M€ d'ARR, grands comptes à 49,5 % du CA pour 6 % des clients. Volumétrie réduite (71 000 lignes contre 167 000) : la crédibilité prime sur le volume brut.
010 — Planification par asset avec OU logique¶
Décision. schedule=BRONZE_DEMO | BRONZE_ECO2MIX.
Motif. Une liste signifierait ET : tous les assets doivent avoir bougé. Le jeu synthétique n'étant rechargé que manuellement, la transformation n'aurait plus jamais démarré seule après la première exécution — sans lever la moindre erreur.
Conséquences. La transformation part dès qu'une source, quelle qu'elle soit, apporte de la donnée fraîche.
011 — Une application d'analyse sur mesure, en plus de Metabase¶
Décision. Développer une application FastAPI servie sur /analyse, lisant
la même couche gold.
Motif. Le démonstrateur montrait qu'on sait installer et configurer des outils. Il ne montrait pas qu'on sait en développer. Or c'est précisément la question d'un prospect dont le besoin sort du cadre d'un outil générique : rendu imposé, catalogue de visualisations limité, identité visuelle du produit et non de l'entreprise.
Conséquences. Un conteneur de plus, et un rôle PostgreSQL supplémentaire
(app_reader, lecture seule sur gold). L'application ne remplace pas
Metabase : l'un sert l'exploration libre, l'autre ce qui doit être exactement
juste.
Aucune bibliothèque JavaScript n'est utilisée — les graphiques sont construits en SVG. Cela évite 500 Ko de dépendances, supprime toute requête vers un tiers, et démontre la maîtrise du rendu.
012 — Choroplèthes par carte GeoJSON personnalisée¶
Contexte. Les cartes intégrées de Metabase ne couvrent que les pays du monde et les États américains. Les régions françaises sont absentes.
Décision. Servir un GeoJSON allégé depuis nginx et l'enregistrer comme carte personnalisée dans Metabase.
Motif. Une première version utilisait une carte à points : un marqueur au centre de chaque région. Cela n'apprend rien sur l'intensité d'une grandeur par territoire — c'est visuellement faux.
Conséquences. Le GeoJSON doit être accessible par une URL publique : Metabase refuse les URL pointant vers des hôtes internes, par protection contre les requêtes côté serveur. Le fichier étant de l'open data, l'exposer est sans conséquence.
nginx doit également déclarer le type MIME application/geo+json, faute de
quoi Metabase rejette la carte pour « content-type invalide ».