GreenOps — Stripe billing en prod (Checkout, webhooks, gate Pro)

July 9, 2026

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

  1. Admin org ouvre /billing — badge plan, statut abonnement.
  2. Free → bouton « Passer au plan Pro » → Route Handler POST /api/billing/checkout → VoltFlow crée une session Stripe Checkout → redirection Stripe.
  3. Paiement test validé → webhook checkout.session.completed → upsert subscriptions → retour GreenOps : badge Pro sur le dashboard.
  4. 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)