Aller au contenu

ADR-0003 — dynors-entitlements source-agnostique, alimenté par events multi-producteurs

Date : 2026-05-07 (rédigée rétroactivement 2026-06-30 — backfill d'une décision déjà actée) Catégorie : A (modèle d'autorisation transverse) Décideur : Lead architecte DYNORS Statut : Acceptée — la 1ʳᵉ phrase du §1 (« il ne décide rien par lui-même ») est amendée par [[0033-capacites-contextualisees-politique-declarative]] (2026-08-29). Le reste de la décision est inchangé ; conformément à la convention du dépôt, le texte ci-dessous n'est pas réécrit. S'appuie sur : [[0002-jwt-minimal-aud-realm]] (les droits ne sont pas dans le JWT) Source de vérité : dynors-docs/docs/05-architecture-si/dynors-entitlements.md ; décision modèle entitlements du 2026-05-07


Contexte

Une fois acté que le JWT ne porte pas les droits ([[0002-jwt-minimal-aud-realm]]), il faut un endroit qui réponde à la question runtime : « ce sujet a-t-il le droit de faire ça maintenant ? ».

Le réflexe serait de faire de dynors-billing (la facturation B2B SaaS) la source unique des droits. C'est incorrect pour DYNORS : ELISA onboarde des e-commerçants et des livreurs sans abonnement mensuel, DAWALALE vend un pack permis à un apprenant qui n'est pas un tenant, un compte démo n'a aucune facture. Forcer ces quatre modèles commerciaux (B2B SaaS, B2B onboardé, B2C transactionnel, grant administratif) dans le moule facturation pollue dynors-billing et place des dépendances critiques au mauvais endroit.

Décision

  1. dynors-entitlements est une projection runtime source-agnostique. Il ne décide rien par lui-même : il consomme des events émis par les producteurs légitimes, projette l'état (UPSERT idempotent par event_id), et expose une API de lecture via SLY (/entitlements/**, SlyTransitFilter HMAC obligatoire — jamais appelé par un frontend).

  2. Plusieurs producteurs, pas un seul. dynors-billing (contrats B2B → tenant.contract.*), elisa-backend (onboarding tenants/livreurs), dawalale-backend (achats packs B2C), medisen-backend (consultations B2C), dynors-auth (rôles users), admin-panel (grants). dynors-billing n'est qu'un producteur parmi d'autres, maître du seul B2B SaaS récurrent.

  3. Modèle de donnée unifié : un entitlement = (subject_type ∈ {TENANT,USER,CUSTOMER,COURIER}, subject_id, scope, permissions, expires_at, source, source_ref, status). Les quatre modèles commerciaux cohabitent sans conflit car les subject_type et scope diffèrent.

  4. Fraîcheur : cache 60 s côté apps + event entitlement.invalidated republié à la révocation pour purge immédiate. Latence changement→effet : ~5 s nominal, 60 s en mode dégradé bus.

  5. Audit : chaque décision tracée (entitlement_decision_log), conservation 7 ans (obligations banque/assurance Sénégal), archive immuable S3 Object Lock.

Conséquences

Positif : dynors-billing reste propre (facturation pure) ; ajouter un nouveau modèle commercial = ajouter un producteur d'events, sans toucher au cœur ; révocation rapide ; piste d'audit conforme régulé ; frontière billing / entitlements explicite (§11 du doc source).

Coût : un service de plus à exploiter (PostgreSQL + Redis + consumer bus) ; discipline nécessaire sur le format normalisé des events et l'idempotence ; fail-closed obligatoire côté client (un entitlements indisponible doit refuser, pas autoriser).

Composants impactés : dynors-platform/entitlements, dynors-entitlements-client, tous les producteurs d'events, dynors-events (transport).

Alternatives écartées

  • dynors-billing source unique : casse sur ELISA (pas d'abonnement), DAWALALE (pas de tenant), démos (pas de facture). Pollue le modèle de facturation. Écartée.
  • Droits dans chaque app, en silo : duplication, incohérence, audit impossible à centraliser. Écartée au profit de la projection unique alimentée par events.

Références

  • dynors-docs/docs/05-architecture-si/dynors-entitlements.md (modèle complet, §3 events, §11 frontière billing)
  • dynors-docs/docs/05-architecture-si/dynors-auth.md (JWT minimal en amont)
  • ADR-0002 (JWT minimal)