Appearance
Going live
This page covers the move from the Stripe sandbox (test mode) to live mode, and the money-side tasks that follow, in particular refunds for duplicate subscriptions.
Do this only after the sandbox has been proven
At the time of writing the sandbox account does not exist and nothing has run against Stripe. Complete Setup and the manual QA checklist in the sandbox first. Dashboard names below are (general Stripe knowledge); confirm them in the live Dashboard.
Test mode and live mode are separate worlds
Keys, Products and Prices, Customers, subscriptions, Customer portal configuration and webhook endpoints all exist separately in each mode. Nothing created in the sandbox exists in live mode. The Worker holds one set of Stripe secrets at a time, so going live means replacing every Stripe secret with its live counterpart at the same moment. A mix of modes (for example a live key with sandbox Price ids) makes Stripe reject requests, which the Worker returns as 502 stripe_error, and a webhook secret from the wrong mode makes every event 400 invalid_signature.
| Worker secret | Sandbox value | Live value |
|---|---|---|
STRIPE_SECRET_KEY | sk_test_... | sk_live_... (a restricted key is also possible, general Stripe knowledge; it must be allowed to write Checkout Sessions, Billing Portal sessions and Subscriptions, and read Subscriptions and Checkout Sessions, which is what the code calls) |
STRIPE_PRICE_MONTHLY | sandbox price_... | live price_... |
STRIPE_PRICE_QUARTERLY | sandbox price_... | live price_... |
STRIPE_PRICE_ANNUAL | sandbox price_... | live price_... |
STRIPE_WEBHOOK_SECRET | whsec_... of the sandbox endpoint (or of stripe listen) | whsec_... of the live endpoint |
Steps
1. Activate the account
Finish Stripe account activation: business profile, bank account for payouts, identity verification (general Stripe knowledge). Live payments are not possible before it is complete. Use a real support email in the business settings, and replace the placeholder support@reformassistant.example in the portal's pages with it.
2. Create the live Product and Prices
Switch the Dashboard to live mode and repeat Setup step 3: product Reform Assistant with three recurring Prices that match src/plans.ts:
| Plan id | Amount | Recurring |
|---|---|---|
monthly | $8.00 USD | every 1 month |
quarterly | $21.60 USD | every 3 months |
annual | $81.60 USD | every 12 months |
Double-check the amounts and intervals: they are what real money is charged against.
3. Create the live secret key
Reveal or create a live secret key in the developer area (general Stripe knowledge: Developers → API keys, live mode).
4. Configure the live Customer portal
Repeat Setup step 5 in live mode: allow switching between the three live Prices, cancellation, payment method updates. Portal configuration is per mode; without it POST /api/billing/portal fails with 502 stripe_error in live mode.
5. Create the live webhook endpoint
In live mode add a Dashboard endpoint at https://<production-origin>/api/stripe/webhook with exactly the five events the Worker handles (Setup step 6): checkout.session.completed, checkout.session.expired, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted. Copy its signing secret. stripe listen is only for local development; do not use it for production.
6. Set the secrets and APP_URL
On the deployed Worker:
sh
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_PRICE_MONTHLY
npx wrangler secret put STRIPE_PRICE_QUARTERLY
npx wrangler secret put STRIPE_PRICE_ANNUAL
npx wrangler secret put STRIPE_WEBHOOK_SECRETSet vars.APP_URL in wrangler.jsonc to the HTTPS origin families use, with no trailing slash, and redeploy (npm run deploy). It builds the Checkout success and cancel URLs and the portal return URL (Checkout). See Cloudflare deployment for the rest of the deployment.
7. Do not carry test-mode data into production
The Worker stores stripe_customer_id and stripe_subscription_id per account and reuses the customer id on the next checkout (customer=<id>) and for the Billing Portal. A sandbox customer id does not exist in live mode, so every account row created during sandbox testing would fail with 502 stripe_error at checkout and portal. Use a fresh D1 database for production, or remove the sandbox accounts and their subscriptions, connections and stripe_events rows before launch. Keep the sandbox and the production bucket and database separate so stripe/events/ and stripe/duplicates/ contain only real events.
8. Smoke test with a real card
Pay for the cheapest plan with a real card, check that the webhook delivery shows 200 in the live Dashboard, that the subscription row is active and the connection pending, then cancel and refund it in Stripe. Test card numbers do not work in live mode.
Taxes and receipts
- Tax. The code does not enable Stripe Tax or collect tax ids: the Checkout Session is created without
automatic_taxor tax-id fields (see the form in Checkout). The customer pays exactly the Price amount, and whether that amount includes tax is whatever the Price's tax behaviour in Stripe says. If sales tax or VAT applies to this business, decide that with an accountant before going live; adding tax collection is a code change tocreateCheckout, not a dashboard setting alone. - Receipts. The portal sends no emails of its own. Payment receipts and invoice emails are Stripe's: enable them under the email settings (general Stripe knowledge: Settings → Customer emails, "successful payments"), and set the business name, support email and logo in the branding settings so receipts and Checkout look right.
- Descriptor. The statement descriptor shown on card statements comes from the business settings in Stripe (general Stripe knowledge). Use a recognisable name so families do not dispute charges.
Refunds for duplicate subscriptions
A duplicate subscription is one Stripe subscription that arrived for an account (or inmate) that already had a live one (Webhooks). The Worker records it in R2 at stripe/duplicates/<subscription_id>.json and cancels it through Stripe, which stops future charges but does not refund a charge already taken. Refunds are manual.
List the records. Browse the
reform-assistant-filesbucket under the prefixstripe/duplicates/in the Cloudflare dashboard's R2 browser (general Cloudflare knowledge), or fetch a known one:shnpx wrangler r2 object get reform-assistant-files/stripe/duplicates/sub_XXXX.json --remoteRead each record. Fields:
user_id,stripe_subscription_id,customer_id,event_type(the event that revealed it),subscription(the Stripe object as last seen),cancellation_requested,canceled, and for a cancel attemptcancellation_statusandcancellation_response.Check
canceled. If it isfalsethe Worker could not cancel it (for exampleSTRIPE_SECRET_KEYwas missing, or Stripe failed): cancel the subscription in the Stripe Dashboard now, otherwise the family is billed again at the next period.Decide whether money was taken. Open the subscription in Stripe by id. A subscription that never reached a paid state (for instance an
incompleteone) has no charge to refund. A record can also be created by a late event about an older replaced subscription of the same family; check that the live subscription belongs to the right account and that the old one was not simply replaced by the family intentionally.Refund. If the duplicate subscription has a successful payment, refund that charge in the Stripe Dashboard (open the payment, then Refund; general Stripe knowledge) and tell the family. Do not touch the live subscription.
Keep the record. Leave the R2 object in place as the audit trail; it is not read again except to recognise a late
customer.subscription.deletedfor the same subscription.
Without STRIPE_SECRET_KEY in production nothing is ever cancelled automatically, which is why it is required in every environment that receives real webhooks.
Checklist
- [ ] Sandbox manual QA run and recorded with no open defects.
- [ ] Stripe account activated; business name, support email and statement descriptor set.
- [ ] Placeholder support address in
/terms/and/privacy/replaced. - [ ] Live Product with three live recurring Prices matching
src/plans.ts; amounts and intervals verified. - [ ] Live Customer portal configured (plan switching across all three Prices, cancellation).
- [ ] Live webhook endpoint with the five events; deliveries answered
200. - [ ] All five Stripe secrets replaced with live values;
APP_URLis the production HTTPS origin; Worker redeployed. - [ ] Production D1 and R2 contain no sandbox data.
- [ ] Tax position decided; receipt emails enabled.
- [ ] Real-card smoke test paid, observed end to end (
active,pending, bridge ack) and refunded. - [ ] Someone is assigned to review
stripe/duplicates/and refund when needed. - [ ] See also the runbook pre-production list.