88 lines · 7.9 KB

Billing release checklist

This is the exact Stripe dashboard and Worker configuration a live billing launch needs. Go through it in order. Background is in billing.md and metering.md.

1. Stripe dashboard (live mode)

Switch the dashboard to live mode before each step. Test-mode objects never carry over.

Products and prices

Create these prices with the amounts from src/lib/pricing.ts (scripts/stripe-test-setup.ts prints them, but it only accepts test keys):

Price Type Amount Lookup key Metadata
Pro Recurring, monthly, USD PRO_PRICE_CENTS (1500 = $15) gitfor_pro_monthly plan = pro
Custom domain add-on Recurring, monthly, USD, per unit CUSTOM_DOMAIN_PRICE_CENTS (500 = $5) gitfor_custom_domain_monthly addon = custom_domains
Each pack in PACKS One-time, USD The pack's cents gitfor_<pack id> none

The billing page hides any price whose amount differs from pricing.ts, so check the amounts exactly.

Customer Portal (Settings > Billing > Customer portal)

  • Turn on Update payment methods, View invoice history and Cancel subscriptions (cancel at period end).
  • Turn off "Switch plans" and quantity updates. Plan changes go through gitfor's checkout, which BILLING_CHECKOUT_DISABLED can stop. Portal plan switches could not be stopped.
  • Set the default return URL to <PUBLIC_URL>/settings/billing.

Subscription retries (Settings > Billing > Subscriptions and emails > Manage failed payments)

  • Use Smart Retries, for example 4 attempts over 2 weeks.
  • When retries run out, pick Cancel the subscription or Mark the subscription as unpaid. Either one makes the account Free. Even "leave past due" is capped: after PAST_DUE_GRACE_DAYS (default 14), gitfor meters the account as Free.
  • Turn on the customer emails for failed payments and expiring cards.

Webhook endpoint (Developers > Webhooks > Add endpoint, live mode)

  • URL: <PUBLIC_URL>/stripe/webhook
  • API version: the account default, matching the stripe package in package.json.
  • Events (exactly these):
    • checkout.session.completed, checkout.session.async_payment_succeeded
    • customer.subscription.created, customer.subscription.updated, customer.subscription.deleted
    • invoice.payment_failed, invoice.paid
    • charge.refunded, charge.dispute.created, charge.dispute.closed
  • Copy the endpoint's signing secret (whsec_...).

API keys

  • Use a restricted live key (rk_live_...) with write access to Customers, Checkout Sessions, Customer portal and Subscriptions (disputes cancel subscriptions), and read access to Prices, Charges (dispute lookup) and Events.
  • Copy the live publishable key (pk_live_...).

Radar and disputes

  • Keep Radar's default rules on. Disputes disable the billed account automatically. A dispute that matches no account notifies site admins, so make sure at least one admin user has notifications enabled.

2. Worker configuration

Secrets go in with wrangler secret put <NAME>. Plain vars can be secrets too.

Name Value Notes
STRIPE_SECRET_KEY rk_live_... Mode must match every row below, or billing turns itself off (logged as Stripe billing disabled: ...).
STRIPE_PUBLISHABLE_KEY pk_live_... Must be pk_live_ when the secret is live.
STRIPE_WEBHOOK_SECRET whsec_... of the live endpoint
STRIPE_WEBHOOK_MODE live Recommended. Catches a test endpoint's secret pasted next to live keys.
STRIPE_PRICE_PRO price_... of the live Pro price
STRIPE_PRICE_CUSTOM_DOMAIN price_... of the live add-on price Leave unset to not sell the add-on.
STRIPE_TOPUP_PRICES {"<pack id>": "price_...", ...} Every pack id from PACKS you want on sale.
STRIPE_DEV_REPLAY unset Never set in production.
BILLING_CHECKOUT_DISABLED unset (or true to launch with purchases paused) Kill switch. Webhooks keep working while it is on.
PAST_DUE_GRACE_DAYS 14 (default) Keep at or above the length of the Stripe retry schedule.
AI_DAILY_SPEND_CEILING_CENTS for example 10000 ($100/day at Cloudflare cost; the default when Stripe is on) Platform-wide. off disables it. Anything else that is not a number blocks all platform-billed AI.
METERING_PLANS unset, or an override JSON If overridden, keep pro.defaultSpendingLimitCents (default 5000).
EMAIL_FROM and email binding configured Without email, unverified accounts are not restricted (see metering.md).

Apply the migrations (bun run deploy does wrangler d1 migrations apply gitfor --remote). This release adds 0047_billing_release.sql.

3. Verify after deploy

  1. wrangler tail shows no Stripe billing disabled: line, and the billing page shows the Upgrade button.
  2. In the Stripe dashboard, Send test webhook for customer.subscription.updated. The endpoint answers 200. A test event sent to the live endpoint answers "outcome":"ignored".
  3. Upgrade a real account with a real card, check it shows Pro, then refund and cancel it from the dashboard and check it goes back to Free.
  4. Set BILLING_CHECKOUT_DISABLED=true and check that Upgrade and the packs disappear and the portal opens only on the card update. Unset it again.
  5. Check the billing page shows a $50 default spending limit for a Pro account that never set one.

4. Incident levers

  • Stop taking money: BILLING_CHECKOUT_DISABLED=true.
  • Stop AI spend platform-wide: AI_DAILY_SPEND_CEILING_CENTS=0.
  • Stop one account: Admin > disable account. This cancels its subscriptions, stops containers and CI, and blocks all metering.