Contexte
Meridian boucle la chaîne data → décision → action → €. La facturation n’est pas une quatrième application portfolio : c’est une feature compte dans GreenOps (/billing), là où vivent déjà l’auth multi-org, les rôles et les gates métier.
Le backend Stripe (Checkout, Billing Portal, webhooks) vit dans un micro-service FastAPI déployé sur Railway — repo VoltFlow. Pas de frontend dédié : l’utilisateur ne voit que GreenOps.
Architecture
GreenOps (Next.js · Vercel)
/billing → UI plan Free/Pro, boutons Checkout / portail
/api/billing/* → proxy serveur (clé service, jamais exposée au client)
↓
VoltFlow (FastAPI · Railway)
/checkout/session · /billing/portal · /api/v1/subscriptions/{org_id}
/usage/record · /webhooks/stripe
↓
Stripe (test mode) → Supabase PostgreSQL (tables subscriptions, billing_lines)
Pourquoi séparer le backend ? Stripe et les webhooks sont plus simples à tenir dans une API Python isolée (signature, idempotence, SDK). GreenOps reste l’app métier : il appelle VoltFlow via lib/billing/client.ts avec X-VoltFlow-Service-Key, comme pour l’intégration FlexSlot → GreenOps.
Données : même Supabase, RLS lecture seule
Les tables subscriptions et billing_lines vivent sur le même projet Supabase que GreenOps (migration VoltFlow 001_subscriptions.sql). Les membres d’une org peuvent lire leur abonnement ; les écritures passent uniquement par le service role côté VoltFlow (webhooks Stripe, enregistrement d’usage).
| Table | Rôle |
|-------|------|
| subscriptions | org_id, plan (free / pro), status, IDs Stripe, fin de période |
| billing_lines | Traçabilité usage — lien flex_slot_id → ligne facturable (stub V1) |
Parcours utilisateur
- Admin org ouvre
/billing— badge plan, statut abonnement. - Free → bouton « Passer au plan Pro » → Route Handler
POST /api/billing/checkout→ VoltFlow crée une session Stripe Checkout → redirection Stripe. - Paiement test validé → webhook
checkout.session.completed→ upsertsubscriptions→ retour GreenOps : badge Pro sur le dashboard. - Pro → bouton « Gérer l’abonnement » → Billing Portal Stripe (annulation, factures).
Seuls les rôles admin peuvent lancer Checkout ou le portail ; les viewer voient le plan en lecture seule.
Gate métier : Free vs Pro
Le billing ne sert à rien sans feature gate côté produit. V1 :
| Plan | Comportement |
|------|----------------|
| Free | Dashboard, flex, REC, export CSV ; export PDF limité aux 10 dernières lignes par section |
| Pro | Export PDF illimité ; enregistrement d’usage à chaque flex_slot créé |
La gate PDF vit dans lib/export/actions.ts : isOrgPro(orgId) interroge VoltFlow avant de tronquer les lignes du rapport. Le badge plan sur /dashboard utilise la même helper — une seule source de vérité côté Stripe/sync.
Règle importante : recordUsage et isOrgPro sont best-effort (ne bloquent jamais la création d’un créneau flex). La facturation ne doit pas casser le flux ops principal.
Webhooks Stripe : piège stripe-python 15.x
En prod, tous les webhooks renvoyaient 500 alors que la signature était valide. Cause : à partir de stripe-python 15, les objets StripeObject ne se comportent plus comme des dicts — .get() lève AttributeError.
Correction dans le handler FastAPI :
data_object = event["data"]["object"].to_dict()
Puis traitement classique : checkout.session.completed, customer.subscription.updated, customer.subscription.deleted → sync table subscriptions. Test E2E ajouté (tests/test_webhook_route.py) pour éviter la régression au prochain bump SDK.
Déploiement et variables
| Où | Variables clés |
|----|----------------|
| Vercel (GreenOps) | VOLTFLOW_API_URL, VOLTFLOW_SERVICE_KEY |
| Railway (VoltFlow) | STRIPE_*, SUPABASE_SERVICE_ROLE_KEY, VOLTFLOW_INTEGRATION_SECRET (= même valeur que la clé côté GreenOps) |
| Stripe Dashboard | Webhook endpoint → https://…/webhooks/stripe ; événements checkout + subscription |
Le webhook Stripe doit pointer vers Railway, pas Vercel : c’est VoltFlow qui vérifie la signature et écrit en base.
Ce que ce chantier démontre
- Intégrer un billing SaaS B2B sans dupliquer l’UI — feature compte dans l’app où l’utilisateur agit déjà.
- Tenir un flux Checkout → webhook → sync → gate en production (test mode), pas seulement en local.
- Isoler les secrets (clé service, webhook signing secret) et respecter le modèle multi-org existant.
- Débugger une régression SDK Stripe en prod — compétence rare en portfolio junior, très lisible en entretien SaaS.
Dépôt GreenOps : github.com/ChristopheChollet/GreenOps · Backend billing : github.com/ChristopheChollet/VoltFlow · Démo : green-ops-five.vercel.app/billing (connexion requise)