Source: docs/STRIPE_BETA_SETUP.md
The same Docker image runs in two Bunny Magic Containers: production talks to lernlaterne-user / lernlaterne-data LibSQL databases with live Stripe keys; beta talks to beta-user / beta-data with Stripe test-mode keys. The code is identical — everything that differs lives in env vars.
The startup guard in validateRuntimeConfig refuses to boot when these are inconsistent (e.g. production with sk_test_…, beta with beta-user URL but sk_live_…).
In test mode create the four SKUs the app already understands (see src/billingOverview.ts):
| Product | Plan SKU | Role after signup | Intervals |
|---|---|---|---|
| Solo | solo | personal | monthly + yearly |
| Edu | edu | teacher | monthly + yearly |
For each price, copy the price id (price_…) into env:
STRIPE_PRICE_SOLO_MONTHLYSTRIPE_PRICE_SOLO_YEARLYSTRIPE_PRICE_EDU_MONTHLYSTRIPE_PRICE_EDU_YEARLY(All four required together — the catalog refuses partial config.)
For manual buy-it-now testing during bring-up, also create Payment Links and copy their URLs into the matching STRIPE_PAYMENT_LINK_* vars; they render as test buttons on /info. Once POST /v1/me/account/checkout is wired into the website you can stop using Payment Links.
Developers → Webhooks → Add endpoint:
https://<beta-api-host>/webhooks/stripecheckout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidSTRIPE_WEBHOOK_SECRET on the beta container only.Do the same later in live mode with a separate URL pointing at the production container, and a separate signing secret.
Provision a second container (or separate app) for beta. The two only differ in env:
| Variable | Production | Beta |
|---|---|---|
DEPLOY_ENV | production | beta |
BUNNY_USER_DATABASE_URL | lernlaterne-user URL | beta-user URL |
BUNNY_USER_DATABASE_AUTH_TOKEN | live token | beta token |
BUNNY_DATA_DATABASE_URL | lernlaterne-data URL | beta-data URL |
BUNNY_DATA_DATABASE_AUTH_TOKEN | live token | beta token |
STRIPE_SECRET_KEY | sk_live_… | sk_test_… |
STRIPE_PUBLISHABLE_KEY | pk_live_… | pk_test_… |
STRIPE_WEBHOOK_SECRET | prod endpoint secret | beta endpoint secret |
STRIPE_PRICE_* | live price ids | test price ids |
STRIPE_PAYMENT_LINK_* | live links (optional) | test links |
CORS_ORIGINS | https://www.lernlaterne.de | https://beta.lernlaterne.de |
WEBSITE_ORIGIN | https://www.lernlaterne.de | https://beta.lernlaterne.de |
API_TEST_PAGE | 0 (default) | 1 (enables /info) |
INTERNAL_API_SECRET | prod secret | distinct beta secret |
MAIL_LOG_ONLY | unset | 1 recommended (don't email real users from beta) |
Set the GitHub Actions repo variables BETA_APP_ID and (optionally) BETA_CONTAINER_NAME so docker-ghcr-bunny.yml deploys the same image to both containers.
Local (Stripe CLI forwards test webhooks to your dev API):
bun run dev:bunny # API on beta DBs
stripe listen --forward-to localhost:8003/webhooks/stripe
# (set STRIPE_WEBHOOK_SECRET to the value `stripe listen` prints)
# In another shell, simulate a checkout:
stripe trigger checkout.session.completed
Beta:
/info on the beta API host4242 4242 4242 4242POST /webhooks/stripe got 200 in Stripe Dashboard → Webhookdelivery log
GET /v1/admin/stripe-events on the beta API (with admin auth) should show the event with status: "ok" and a message like minted invitation INV_… for solo/1m
complete signup
GET /v1/me/account should show subscription.plan: "solo", subscription.stripeCustomerId: "cus_…"
Renewals & cancellations: use Stripe test clocks (or the Dashboard's "Cancel" button on the subscription) to drive customer.subscription.updated and .deleted. The webhook calls applySubscriptionTransition which both flips role/plan on the user and clears subscription_period_* for cancel.
seed:bunny wipes the beta DBs (refuses unless URLs contain beta-user and beta-data). Snapshot/export beta-user + beta-data via the Bunny Dashboard before any seed run once you have meaningful test data.
:${{ github.sha }} tags. To revert a bad deploy re-run BunnyWay/actions/container-update-image with a previous SHA — no rebuild required.
GET /health returns { ok, deployEnv, stripe: { secretConfigured, webhookSecretConfigured, catalogReady } }.Quick proof a host is on the right config.
GET /v1/admin/stripe-events?limit=50 lists recent webhook events with status (ok / ignored / error / duplicate), userId if matched, and a free-form message for diagnosis.