Stripe + beta deployment

Source: docs/STRIPE_BETA_SETUP.md

Stripe + beta deployment setup

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_…).

1. Stripe Dashboard (test mode)

In test mode create the four SKUs the app already understands (see src/billingOverview.ts):

ProductPlan SKURole after signupIntervals
Solosolopersonalmonthly + yearly
Edueduteachermonthly + yearly

For each price, copy the price id (price_…) into env:

(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.

Webhook endpoint

Developers → Webhooks → Add endpoint:

Do the same later in live mode with a separate URL pointing at the production container, and a separate signing secret.

2. Bunny Magic Containers

Provision a second container (or separate app) for beta. The two only differ in env:

VariableProductionBeta
DEPLOY_ENVproductionbeta
BUNNY_USER_DATABASE_URLlernlaterne-user URLbeta-user URL
BUNNY_USER_DATABASE_AUTH_TOKENlive tokenbeta token
BUNNY_DATA_DATABASE_URLlernlaterne-data URLbeta-data URL
BUNNY_DATA_DATABASE_AUTH_TOKENlive tokenbeta token
STRIPE_SECRET_KEYsk_live_…sk_test_…
STRIPE_PUBLISHABLE_KEYpk_live_…pk_test_…
STRIPE_WEBHOOK_SECRETprod endpoint secretbeta endpoint secret
STRIPE_PRICE_*live price idstest price ids
STRIPE_PAYMENT_LINK_*live links (optional)test links
CORS_ORIGINShttps://www.lernlaterne.dehttps://beta.lernlaterne.de
WEBSITE_ORIGINhttps://www.lernlaterne.dehttps://beta.lernlaterne.de
API_TEST_PAGE0 (default)1 (enables /info)
INTERNAL_API_SECRETprod secretdistinct beta secret
MAIL_LOG_ONLYunset1 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.

3. End-to-end test (local + beta)

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:

  1. Open Stripe test-mode Payment Link from /info on the beta API host
  2. Pay with 4242 4242 4242 4242
  3. Confirm POST /webhooks/stripe got 200 in Stripe Dashboard → Webhook

delivery log

  1. 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

  1. Open the signup link in the email (or the URL printed in beta logs) and

complete signup

  1. 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.

4. Backups + rollback

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.

re-run BunnyWay/actions/container-update-image with a previous SHA — no rebuild required.

5. Observability

Quick proof a host is on the right config.

status (ok / ignored / error / duplicate), userId if matched, and a free-form message for diagnosis.