Setting Stripe up

This package ships the payment provider. Nothing here needs a Stripe SDK, a build step or a redeploy — a portal that holds the keys sells, and a portal that does not says so.

1. The three keys

Key What it is Where it comes from
Commerce:Stripe:SecretKey the server-side API secret (sk_test_… / sk_live_…) Stripe dashboard → Developers → API keys
Commerce:Stripe:WebhookSecret the endpoint signing secret (whsec_…) Stripe dashboard → the endpoint you add in step 2
Commerce:BaseUrl this portal's public base URL the deployment

As environment variables they are Commerce__Stripe__SecretKey, Commerce__Stripe__WebhookSecret and Commerce__BaseUrl.

🚨 The key names are contract. They are read out of environment variables, Key Vault secret names and helm values on every portal that sells, so renaming one is a silent deletion: a renamed key reads as absent, and absent means "this portal does not sell".

🚨 A secret's VALUE never leaves configuration. Nothing logs it, renders it or stores it on a node. The only thing derived from the secret key is its prefix, which names test-vs-live mode.

2. The webhook endpoint

Add an endpoint in the Stripe dashboard pointing at:

{Commerce:BaseUrl}/api/hooks/Store/Payments

Subscribe it to exactly these events (the list PaymentPathAudit measures against — it is StripePaymentProvider.RequiredEvents, and a test holds it equal to the Store's runbook):

🚨 invoice.paid and customer.subscription.deleted are what keeps a subscription alive. An endpoint not subscribed to invoice.paid grants each plan exactly one month while Stripe keeps charging the card; without customer.subscription.deleted a cancelled subscriber keeps access forever.

Then allowlist the target on the portal: WebhookInbox:Targets:0 = Store/Payments. The inbox is fail-closed — without the allowlist the endpoint answers 404 and stores nothing, however correct the Stripe side is.

Adding an event to an EXISTING endpoint keeps its signing secret — nothing changes on the portal. The test-mode endpoint of the public instance (we_…) predates invoice.payment_failed, so a person adds it, in TEST mode, one of two ways. No agent calls the Stripe API with an account key.

3. The Customer Portal ("Manage billing")

Manage billing on the plan page opens Stripe's hosted Customer Portal for the subscriber (POST /v1/billing_portal/sessions with the customer and a return URL — StripeGateway.OpenBillingPortal). The session is created per click; what the portal offers is the account's portal CONFIGURATION, which lives in the dashboard, once per mode:

Nothing else is needed on the portal side: the secret key the checkout already uses opens the portal, and a portal-side cancellation needs no new event — it arrives as the customer.subscription.deleted the endpoint already carries.

4. Test and live are separate worlds

Test-mode and live-mode endpoints are configured separately at Stripe, so a live-mode endpoint never fires for an sk_test_… checkout and vice versa. A key lists only the endpoints of its own mode — which is what lets Store/Maintenance → PaymentPathAudit tell "no endpoint registered" apart from "registered in the other mode".

5. Switching the public instance to live mode

Going live is a NEW key, a NEW endpoint and a NEW portal configuration — nothing from test mode carries over. The steps, in order; a person does the Stripe half, and no agent reads, writes or calls anything with a live key.

  1. Precondition — one payment, one period, on the RUNNING image. A plan's opening month is granted by checkout.session.completed alone, keyed by its checkout session; invoice.paid extends only for billing_reason: subscription_cycle, keyed by its invoice and landing on the invoice line's period end — so any number of deliveries of either event grant exactly one period (see Subscriptions → One payment, one grant). The fix (Plugins #2406 and #2410) ships in the Store and Stripe PACKAGES, which advance independently of the portal image, so a running image proves nothing about it. The precondition is #2405's own closing check, still in TEST mode: one plan checkout on the public instance ends Fulfilled with no error and exactly one new [active] line on Admin/Subscriptions/<buyer>. Do not replace the test secrets before that has passed — otherwise a live subscriber can be credited twice.
  2. In the Stripe dashboard, LIVE mode (the maintainer):
    • create the live secret key (Developers → API keys, sk_live_…);
    • add a live webhook endpoint at the SAME URL, https://portal.example.com/api/hooks/Store/Payments, subscribed to the SAME five events as §2 — checkout.session.completed, checkout.session.expired, invoice.paid, customer.subscription.deleted and invoice.payment_failed — and copy its signing secret (whsec_…; a live endpoint has its own);
    • save the live Customer Portal configuration exactly as §3 describes (it is per mode, and until it is saved every Manage billing click is refused).
  3. Enter the two values through the write-only GUI, never by hand. On the control instance open Deployments/<deployment> → Set Key Vault secrets… and write <prefix>-Stripe-SecretKey (the live secret key) and memexcloud-Stripe-WebhookSecret (the live endpoint's signing secret). The GUI writes without showing the value back; nothing else — no az keyvault, no chat, no issue — ever carries it.
  4. Make the portal read them. The secrets are read only at pod start, so run the governed Reconcile and then Restart for the public instance on the control instance — Hosting/InstanceAction nodes the operator executes, never kubectl.
  5. Check the mode, then buy. Store/Maintenance → PaymentPathAudit must now report LIVE mode with the live endpoint registered and all five events present. Then work through the real-card checklist on #2440: one real purchase of the smallest plan, validUntil exactly one period ahead, cancel at period end, refund, and the evidence recorded on Payments.

What the module does, and deliberately does not

Reading further