# 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](billing.md) and [metering.md](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_` | 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 `/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: `/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 `. 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` | `{"": "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.