Sandbox billing
When it is active
Section titled “When it is active”Whenever STRIPE_SECRET_KEY is unset. That single condition switches:
| Flow | With a key | Without |
|---|---|---|
| Checkout | Stripe Checkout | /billing/sandbox |
| Manage billing | Stripe customer portal | /billing/sandbox/manage |
| Receipts | Stripe invoices | /billing/sandbox/receipt |
| Plan changes | /api/webhooks/billing |
Written directly |
The plan card and the invoice list say so in place: “Sandbox receipts. They become real Stripe invoices once a secret key is set.”
What it does
Section titled “What it does”The sandbox writes the same rows Stripe would: a subscription with a plan,
interval, amount, status and period, and an invoice for each charge. The rest
of the app never learns the difference — accounts.plan and
accounts.events_quota are denormalised from the subscription exactly as they
are in production, so gating, quotas and the usage panel all behave normally.
Subscription ids are prefixed sandbox_ instead of sub_.
The dev controls
Section titled “The dev controls”/billing/sandbox/manage offers what a portal would, minus the card, plus the
things that otherwise take a month to reach:
| Action | Effect |
|---|---|
| Change plan | Moves the subscription to another plan or interval |
| Cancel at period end | Sets cancelAtPeriodEnd, keeps the plan until the period ends |
| Resume | Clears it |
| Simulate renewal | Advances the period and writes the next invoice |
| Simulate a failed payment | Puts the subscription into past_due |
| Settle the open invoice | Back to active |
Those last three are how you see dunning, renewal and trial-expiry states without waiting.
The Revenue screen has an equivalent: a sample payment button on the setup card that delivers the same two events Stripe sends for a new subscription, signed the same way, handled by the same route. Also development-only.
Switching to real Stripe
Section titled “Switching to real Stripe”-
Set the secret key
Section titled “Set the secret key”Terminal window wrangler secret put STRIPE_SECRET_KEYSandbox mode switches off the moment the key is present.
-
Create the products, prices, meter and portal configuration
Section titled “Create the products, prices, meter and portal configuration”Terminal window pnpm --filter @tracing/web stripe:setupPrices are addressed by lookup key, never by price id, because ids are environment-specific and lookup keys are not:
Plan Monthly Yearly Overage Pro tracing_pro_monthtracing_pro_yeartracing_pro_overageBusiness tracing_business_monthtracing_business_yeartracing_business_overageThe metered prices read from one Stripe billing meter,
tracing_events_overage. -
Point the billing webhook at the app
Section titled “Point the billing webhook at the app”https://tracing.tools/api/webhooks/billingLocally:
Terminal window stripe listen --forward-to localhost:3000/api/webhooks/billingwhich prints the signing secret for
STRIPE_BILLING_WEBHOOK_SECRET. -
Schedule the usage report
Section titled “Schedule the usage report”Terminal window curl -X POST -H "x-cron-secret: $CRON_SECRET" \https://tracing.tools/api/billing/report-usageHourly is plenty. Guarded by
CRON_SECRET; without that variable set, the route allows unauthenticated calls outside production only.