Appearance
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.jsonsont 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 Kubernetesapi-migratelancé 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
DROPni 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.ioest gérée par Cloudflare (le domaine reste chez IONOS).apietapi-stagingsont en « DNS only » ;mediaest en proxy avec cache ;snag-app.io/api/*reste l'adresse de l'app mobile en prod ; la web app sera surapp.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,businesset leurs limites danssubscription_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-deletionest 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
baselineinterdit 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 ».