Simpl-Resultat/spec-decisions-feature-gating.md
le king fu 99ba147906 docs(spec): feature-gating decisions + plan + milestone spec-feature-gating (#297-#302)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:20:50 -04:00

6.3 KiB

Spec Decisions — Gating des fonctionnalités par édition de licence

Date: 2026-07-19 Projet: simpl-resultat Statut: Draft Slug: feature-gating

Contexte

Simpl'Résultat n'applique aujourd'hui aucun gating de fonctionnalités : un utilisateur Free a accès à tout, sauf la récupération des cours (seul gate dur, vérifié côté serveur via maximus-api). L'infrastructure de licence est pourtant déjà là — 3 éditions signées Ed25519 (free/base/premium), table FEATURE_TIERS, champ features[] signé — mais seuls 3 call-sites de gate existent (auto-update, laissé ouvert ; cours, Premium). Max veut définir un vrai périmètre d'abonnement en verrouillant des modules selon le tier.

Objectif

Appliquer une matrice d'entitlements Free/Base/Premium à travers l'app : chaque module hors du tier de l'utilisateur reste visible mais verrouillé, avec un écran d'upsell. Le gating est piloté par un hook central côté React, adossé à l'édition de licence déjà vérifiée, avec un override par-licence optionnel. Aucune donnée n'est supprimée — le durcissement bloque l'accès, il ne détruit rien.

Scope

IN

  • Matrice d'entitlements Free/Base/Premium (ci-dessous).
  • Hook central useEntitlement(feature) + garde de navigation (Sidebar) et de route.
  • Écran/zone d'upsell « verrouillé » (cadenas, CTA upgrade), i18n FR/EN.
  • Enrichir FEATURE_TIERS (Rust) + câbler l'override par-licence (features[] consulté par is_feature_allowed).
  • Gating par onglet des rapports (/reports/*) : Free = Tendance uniquement.
  • Absorption du re-gate auto-update (#271) : l'auto-update devient Base+.
  • Durcissement sec non destructif : blocage d'accès immédiat, données conservées, récupérables par upgrade.

OUT (explicitement exclu)

  • Le champ product explicite (#270) — reste dans spec-paiements.
  • Le go-live Stripe / le flux d'achat en ligne (l'upsell pointe vers l'achat, dont l'URL localisée relève de #270).
  • Un vrai système de codes d'invitation (l'émission de clés reste manuelle via l'endpoint admin generate).
  • Tout gate « dur » server-enforced au-delà de l'existant (cours) — le gating local est un soft paywall assumé (GPL).
  • Un 4e tier « admin » : l'accès total de Max = une licence Premium auto-émise.
  • Mode lecture-seule des features verrouillées (écarté : durcissement sec, pas de distinction voir/éditer).

Matrice d'entitlements

Module Free Base Premium
Dashboard
Import CSV
Transactions
Catégories (+ guide, migration)
Rapport Tendance
Autres rapports (Highlights, Comparaison, Catégorie, Cartes)
Budget
Multi-profils
Auto-update
Bilan complet (patrimoine + cours)
Export/Import chiffré
Changelog / Docs

Admin (Max) = licence Premium auto-émise (superset).

Décisions prises

Question Décision Raison
UX du gate (feature hors tier) Verrouillé + upsell : entrée visible avec cadenas ; clic → écran « Passez à Base/Premium » Meilleure conversion — l'utilisateur voit ce qu'il rate
Données existantes lors du durcissement Durcissement sec non destructif : blocage d'accès (vue + édition) immédiat, mais données conservées, jamais supprimées, récupérables par upgrade Simple à coder (pas de mode lecture-seule) ; ne détruit aucune donnée financière (clarifié par Max)
Modèle d'entitlements Statique + override licence : map tier → features en dur, PLUS license.features[] consulté en additif (allowed = feature ∈ tier OU feature ∈ license.features) Matrice centralisée et simple, + débloque le champ features[] déjà signé pour des licences spéciales sans créer de tier
Coordination spec-paiements Absorber #271 (re-gate auto-update) ici ; #270 (product) reste séparé L'auto-update est dans la matrice → gaté ici, #271 redondant ; product/URL d'achat = go-live, hors scope
Enforcement UI-only (gardes React : nav + route). current_edition() (Rust) reste la source de vérité lue ; les commandes Tauri ne refusent pas (sauf l'existant : cours server-enforced) Cohérent avec le soft-paywall GPL assumé — un gate Rust serait aussi retirable par un fork, sans gain de sécurité
Écran d'upsell Composant générique paramétré (feature + tier requis), i18n FR/EN ; CTA « Obtenir Base/Premium » (lien d'achat, dépend de #270) + « J'ai déjà une clé » → Réglages → Licence Un seul composant réutilisable, pas un écran par feature
Admin (accès total de Max) Licence Premium auto-émise (endpoint admin generate), pas de 4e édition ni mode dev Premium = superset ; réutilise l'infra existante, zéro code neuf
Free multi-profils existants Profil actif conservé ; profils supplémentaires verrouillés → upsell (données conservées) Cohérent avec durcissement sec + conservation des données

Contraintes

  • GPL-3.0 + privacy-first : le gating local est un soft paywall assumé (contournable par fork). Seules les features adossées à maximus-api (les cours, aujourd'hui) sont dures. Aucune donnée financière ne quitte l'appareil.
  • Séquencement : le gating est livrable et testable indépendamment de Stripe (pas encore en live) via des clés admin-émises. Corollaire : sans ce chantier, distribuer des clés n'a aucun effet visible (tout est déjà ouvert).

Références

Source Pertinence
spec-monetisation.md Modèle des 3 tiers, contraintes GPL / soft-paywall, format de licence
src-tauri/src/commands/entitlements.rs FEATURE_TIERS + is_feature_allowed — table à enrichir, override features[] à câbler
src-tauri/src/commands/license_commands.rs current_edition() (source de vérité de l'édition) + LicenseClaims.features (champ signé à consulter)
src/services/licenseService.ts, src/hooks/useLicense.ts, src/hooks/useIsPremium.ts Base du futur hook useEntitlement
src/App.tsx (routes /reports/*), src/components/layout/Sidebar Points de garde nav + route