Skip to content

Décisions de la session « Plateforme v2 » (2026-11-07) ​

Décidées par l'owner pendant la session, ou proposées par le coordinateur et acceptées. Le détail technique est dans les spécifications ~/Projects/Snag/specs/2026-11-07-plateforme-v2/ (CdC = cahier des charges).

D01 — Comptes utilisateurs dans notre Postgres, en UE ​

  • Contexte : question de l'owner sur Firebase Auth pour les mots de passe et les fournisseurs de connexion.
  • Décision : les comptes, mots de passe, sessions et identités externes restent dans notre Postgres (nœud allemand). Pas de Firebase ni d'autre IdP tiers pour les utilisateurs de l'app.
  • Justification :
    • l'auth maison existe et tourne : appareil anonyme, e-mail OTP, mot de passe, sessions avec refresh, Google et Apple préparés (migration 0051) ;
    • un IdP tiers imposerait une seconde identité à faire cohabiter, un SDK Google sur le chemin critique, des données d'identité hors UE par défaut, et le verrouillage le plus coûteux à défaire ;
    • les gardes métier (tier, quotas, bêta, type de compte) ne peuvent vivre que dans notre passerelle de toute façon.
  • Conséquences : un adaptateur OIDC générique permet d'ajouter un fournisseur par configuration (fait, T02b). Firebase reste possible pour autre chose (push FCM, crash reporting).
  • Statut : adoptée. Références : CdC §2.7 ; backend #666.

D02 — Authentik pour le staff uniquement ​

  • Décision : Authentik protège les outils et la console (MFA). Les utilisateurs de l'app n'y sont pas.
  • Justification : Authentik tourne sur le nœud américain avec un seul worker, déjà mesuré lent (2026-11-01) ; il sert du forward-auth pour navigateurs, pas des jetons mobiles avec quotas ; y mettre les clients déplacerait l'identité hors UE et ajouterait une latence au scan.
  • Statut : adoptée. Références : CdC §2.7.

D03 — Une route = un contrat déclaratif ​

  • Décision : chaque route est un objet defineRoute (identifiant, résumé, tags, accès, quota, limite de débit, schémas TypeBox, erreurs, gestionnaire). Garde, validation, types, sérialisation, OpenAPI, documentation et client typé en dérivent.
  • Justification : avant, l'information d'une route était éparpillée (listes de chemins, motifs de quota, documentation à part) et pouvait dériver sans qu'aucun test ne le voie. Le schéma de réponse bloque aussi la fuite de colonnes non déclarées. tRPC, NestJS, Hono et une spécification écrite à la main ont été écartés (CdC, doc 05).
  • Conséquences : un test empêche la part de routes entièrement décrites de baisser ; les routes utilisées par l'app mobile ne sont converties qu'avec une preuve d'identité de réponse.
  • Statut : adoptée. Références : spec 05-architecture-api.md ; backend #663.

D04 — Passerelle : acteur unique, politique obligatoire ​

  • Décision : l'appelant est résolu une fois par requête (utilisateur, opérateur, service, anonyme) ; chaque route porte une politique d'accès ; le serveur refuse de démarrer si une route n'en a pas.
  • Justification : la sécurité reposait sur des listes de chemins tenues à la main ; un oubli ouvrait une route.
  • Conséquences : /metrics, /audit, /scan-debug-cases, /openapi.json sont internes ; un test compare l'ancien et le nouveau mécanisme sur toute la surface.
  • Statut : en service sur staging. Références : backend #665 ; docs/GATEWAY.md.

D05 — Migrations suivies et appliquées par un Job ​

  • Décision : table schema_migrations (nom, empreinte SHA-256) ; Job Kubernetes api-migrate lancé par ArgoCD avant chaque déploiement de l'API ; un fichier appliqué n'est jamais modifié, on en écrit un nouveau.
  • Justification : les migrations étaient rejouées à chaque démarrage sans trace et appliquées à la main sur staging. Le Job retire cette étape manuelle et bloque le déploiement si une migration échoue.
  • Conséquences : en prod, le Job est désactivé jusqu'à la bascule (sinon il créerait le schéma dans une base vide avant la restauration du dump).
  • Statut : en service sur staging. Références : backend #661 ; gitops #33.

D06 — Migrations additives pendant la migration ​

  • Décision : aucun DROP ni renommage pendant cette session ; une colonne ne disparaît qu'une fois qu'aucune image ne la lit.
  • Justification : l'ancienne et la nouvelle image coexistent pendant un déploiement ; c'est ce qui garantit zéro coupure et un retour arrière simple.
  • Statut : adoptée. Références : spec 02, section « Indisponibilité ».

D07 — DNS sur Cloudflare, api.* hors proxy ​

  • Décision : la zone snag-app.io est gérée par Cloudflare (le domaine reste chez IONOS). api et api-staging sont en « DNS only » ; media est en proxy avec cache ; snag-app.io/api/* reste l'adresse de l'app mobile en prod ; la web app sera sur app.snag-app.io.
  • Justification : bascule prod en secondes en changeant l'origine côté Cloudflare, cache des rendus, protection. api* reste hors proxy à cause des uploads de scan et de la limite de débit par IP (l'API ne connaîtrait sinon que les IP de Cloudflare).
  • Statut : en service (NS chez Cloudflare depuis le 2026-11-07, 11 h 20). Références : gitops #31, docs/dns.md.

D08 — Abonnements : tiers en table, RevenueCat ​

  • Décision : tiers free, pro, business et leurs limites dans subscription_tiers / tier_features ; RevenueCat est la source de vérité des achats ; son webhook est authentifié par la passerelle comme un service.
  • Justification : ajouter un niveau ou changer une limite sans déploiement ; un seul mécanisme d'accès pour toute l'API ; jamais de confiance accordée au client pour le tier.
  • Conséquences : noms et prix des tiers restent une décision produit en attente.
  • Statut : adoptée ; tiers en service sur staging. Références : backend #662, #664 ; docs/SUBSCRIPTIONS.md.

D09 — Le profil règle l'affichage, le tier règle les droits ​

  • Décision : les personas choisies à l'onboarding (collectionneur, joueur, investisseur…, combinables) décident des modules visibles et de leur ordre ; elles ne donnent ni ne retirent jamais un droit.
  • Justification : un utilisateur doit pouvoir afficher un module masqué ; mélanger profil et droits rendrait les gardes impossibles à raisonner.
  • Statut : adoptée. Références : CdC §2.3.15 ; spec 04 §5.

D10 — Journaux pseudonymisés ​

  • Décision : les journaux ne contiennent ni identifiant utilisateur, ni e-mail, ni jeton, ni IP complète ; l'utilisateur y est un user_hash (HMAC journalier) et l'IP est tronquée.
  • Justification : Loki et Prometheus tournent sur le nœud américain ; le support reste possible le jour même grâce au user_hash, sans permettre de suivre une personne sur la durée.
  • Statut : en service sur staging. Références : backend #666 ; gitops #35.

D11 — Accès web : QR, passkey, révocation immédiate ​

  • Décision : connexion à la web app par QR scanné depuis le mobile, passkey ou e-mail + mot de passe ; un second facteur (mot de passe ou passkey) est exigé avant d'approuver un QR ; la révocation est vérifiée à chaque requête ; le temps réel passe par SSE.
  • Justification : la révocation immédiate vient de la vérification serveur, pas du canal temps réel ; SSE suffit pour prévenir l'écran, se reconnecte seul et passe partout.
  • Statut : adoptée, à construire (T32, T33).

D12 — Gemini retiré du scan ​

  • Décision : plus d'appel à Gemini dans le scan ; lecture de texte par ML Kit sur l'appareil ou par un modèle maison si la mesure montre une perte.
  • Justification : souveraineté du chemin de scan, coût par scan, entraînement sur nos propres données.
  • Conséquences : la clé Google est à révoquer et à retirer des secrets ; mesure avant / après à faire sur le banc.
  • Statut : code retiré (backend #660). Références : T46.

D13 — Webhook eBay public dans l'API, provisoirement ​

  • Décision : POST /ebay/account-deletion est public ; il ne fait qu'écrire une ligne de journal.
  • Justification : eBay exige cette adresse pour garder les clés de production ; l'ancienne porte la refusait. Aucune donnée n'est exposée : nous ne stockons rien sur les utilisateurs eBay.
  • Conséquences : la route partira avec le futur service eBay (D14) ; la vérification de signature eBay est une amélioration possible.
  • Statut : adoptée.

D14 — Sources de marché isolées, bus d'événements à évaluer ​

  • Décision : une source (eBay, Vinted, Cardmarket, CardTrader, TCGplayer…) = un service isolé, avec ses identifiants, son budget de requêtes et ses journaux, produisant des observations normalisées et un événement. Le bus d'événements est à choisir sur besoin réel : outbox Postgres, NATS JetStream, Redis Streams, Kafka en dernier.
  • Justification : une source bloquée ne doit toucher ni l'API ni les autres sources ; Kafka est probablement surdimensionné pour trois nœuds et ce volume.
  • Statut : reportée au bloc post-migration. Références : CdC §5.

D15 — Construction des images ​

  • Décision : pendant la migration, les tests tournent sur GitHub Actions et les images sont construites sur le laptop (ops/build-image.sh). Après la migration, construire les images dans le cluster sans GitHub Actions.
  • Justification : le build dans le cluster échoue (PodSecurity baseline interdit les profils dont BuildKit rootless a besoin) ; le script laptop est reproductible et tracé ; la décision de fond ne doit pas ralentir la migration.
  • Statut : adoptée / suite reportée. Références : CdC §5.1 ; gitops #32, #37.

D16 — Pas de Nx ni de monorepo ​

  • Décision : l'organisation reste en dépôts séparés ; le catalogue d'interfaces vient d'OpenAPI.
  • Justification : Nx apporte surtout du cache et du graphe de projets dans un monorepo ; une consolidation au milieu d'une session à dix agents casserait toutes les branches. À reconsidérer si web app, landing et console partagent un jour un design system.
  • Statut : adoptée.

D17 — Documentation interne Docusaurus ​

  • Décision : site interne docs.snag-app.io (Docusaurus) derrière Authentik, regroupant infra (graphe interactif généré depuis gitops), features, routes générées depuis OpenAPI, et ce registre.
  • Justification : une seule source pour l'équipe, routes toujours synchrones avec le code.
  • Statut : adoptée, à construire (T37). Ce registre y sera repris tel quel.

D18 — Volet jeu ​

  • Décision : règles de format et listes de bannissement en données versionnées éditables en console, moteur de validation par TCG ; le deck référence la carte de jeu, pas l'impression ; recommandation déterministe d'abord, modèles appris ensuite, toute sortie passant par le validateur.
  • Justification : formats et bannissements changent plusieurs fois par an ; la possession doit compter toutes les impressions d'une carte ; un deck suggéré doit toujours être légal.
  • Statut : adoptée. Références : spec 04 ; documentation « Règles de jeu ».