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_DISABLEDcan 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
stripepackage inpackage.json. - Events (exactly these):
checkout.session.completed,checkout.session.async_payment_succeededcustomer.subscription.created,customer.subscription.updated,customer.subscription.deletedinvoice.payment_failed,invoice.paidcharge.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
wrangler tailshows noStripe billing disabled:line, and the billing page shows the Upgrade button.- 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". - 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.
- Set
BILLING_CHECKOUT_DISABLED=trueand check that Upgrade and the packs disappear and the portal opens only on the card update. Unset it again. - 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.