Files
descrybe/docs/stripe-setup.md
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

6.9 KiB

Stripe billing setup (Descrybe v2)

Self-serve Checkout for Starter / Plus / Growth / Business / Scale (subscriptions), AI credit packs (one-time payment), Customer Portal for subscription management, and signed webhooks that assign plans / grant credits.

Enterprise stays sales-led (no self-serve price).

Plans vs credit packs

Kind Stripe mode Checkout body Webhook effect
Plan subscription subscription { "plan": "starter|plus|growth|business|scale", "term": "monthly|yearly" } Assign plan + monthly credit grant
Credit pack payment { "pack": "small|medium|large|xl" } Add pack credits to the company wallet (no plan change)

Monthly plan credits are sized with a tiered cover % of the SKU cap at ~2 credits per AI enhance (PlanAICoverPercent / MonthlyCreditsForPlan):

Plan Cover % Monthly credits
Starter / Plus 25% 250 / 1,250
Growth / Business 50% 10,000 / 40,000
Scale 25% 50,000 (bounds absolute COGS on large catalogs)
Enterprise sales / BYOK 1,000,000 managed wallet

Packs cover the remaining catalog, fixes, and tests without upgrading SKU caps.

Pack ladder (marketing USD; live Prices live in Stripe / admin settings):

Pack ID Credits List
small 500 $19
medium 2,500 $69
large 15,000 $249
xl 50,000 $699

Configuration (preferred: admin dashboard)

Platform admin → /admin/settings (GET/PUT /api/admin/settings). Stripe lives under values.* (secrets masked on GET; omit a secret on PUT to keep the existing value):

Settings key Purpose
stripe.secret_key Secret key (sk_test_… / sk_live_…). Empty → mock-oriented behavior.
stripe.webhook_secret Signing secret (whsec_…). Needed for live webhooks.
stripe.mock true forces mock even if a secret key is set (local QA).
`stripe.price.starter plus
`stripe.price.pack.small medium

WEB_ORIGIN remains a boot env var (Checkout success/cancel + Portal return URLs).

Optional env fallback

Process env (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_MOCK, STRIPE_PRICE_*) still works as fallback when admin values are unset. Prefer /admin/settings for new setups. Production does not require Stripe secrets at API boot.

Pack env keys: STRIPE_PRICE_PACK_SMALL, STRIPE_PRICE_PACK_MEDIUM, STRIPE_PRICE_PACK_LARGE, STRIPE_PRICE_PACK_XL.

Mock / dry-run mode

Set stripe.mock=true in /admin/settings (or STRIPE_MOCK=true in env) for local free Checkout (and unsigned webhooks when no webhook secret). Empty secret key alone reports mock-configured status but does not assign plans or grant pack credits.

When mock is on:

  • POST /api/billing/checkout with a plan immediately assigns the plan, grants monthly credits, and redirects to /billing?checkout=success&mock=1.
  • POST /api/billing/checkout with a pack immediately adds credits and redirects with pack + credits query params.
  • POST /api/billing/portal returns a local /billing?portal=mock link.
  • POST /api/webhooks/stripe accepts unsigned JSON only when no webhook secret is set (local scripts). If a webhook secret is present, signatures are always verified (even in mock). Events remain idempotent by event.id.

Local demo works with no Stripe account. Admin settings apply without reinventing boot secrets; restart only if you changed process env.

Stripe Dashboard setup

  1. Create a Product per plan (Starter, Plus, Growth, Business, Scale) with monthly + yearly recurring Prices.
  2. Create a Product (or one Product with four Prices) for AI credit packs — Prices must be one-time (not recurring).
  3. Paste each Price ID into /admin/settings (stripe.price.*) — or optional STRIPE_PRICE_* env fallback.
  4. Enable Customer Portal (Settings → Billing → Customer portal): allow cancel / update payment method.
  5. Webhook endpoint (API): https://<your-api-host>/api/webhooks/stripe
    • Events: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted
  6. Local forwarding:
stripe listen --forward-to localhost:8080/api/webhooks/stripe

Paste the printed whsec_… into /admin/settingsstripe.webhook_secret (or optional STRIPE_WEBHOOK_SECRET env).

Sync public plan meters to Postgres

After changing default credits / SKU caps in code:

cd apps/api && go run ./cmd/sync-plans

API surface

Method Path Auth Notes
GET /api/billing/stripe session + company { configured, mock, has_customer, has_subscription }
GET /api/billing/credit-packs session + company { packs: [...] } catalog
POST /api/billing/checkout session + company admin plan or pack body → { url, mock? }
POST /api/billing/portal session + company admin { url, mock? }
POST /api/webhooks/stripe Stripe signature (or mock) Idempotent via stripe_webhook_events

Checkout metadata:

  • Plans: kind=plan, company_id, plan, term
  • Packs: kind=credit_pack, company_id, pack, credits

Webhooks resolve the company from metadata, client_reference_id, or stripe_customer_id — never from a client-supplied company without session scope on authenticated routes. Pack completion grants credits from the catalog amount for the pack id (ignores tampered credits metadata when the pack id is known).

Migration

# from repo root, with DATABASE_URL set
pwsh -File scripts/migrate.ps1
# applies 016_stripe_billing.sql (companies.stripe_customer_id, company_plans stripe fields, stripe_webhook_events)

Local status (dev machine)

  • Migration applied locally: goose version 16 (016_stripe_billing.sql) creates stripe_webhook_events plus companies.stripe_customer_id / company_plans.stripe_*.
  • For mock Checkout smoke: set stripe.mock=true in /admin/settings (or STRIPE_MOCK=true in root .env as fallback). Restart API only if you changed process env. DB migrate alone does not require restart.
  • Smoke plan: login demo@descrybe.local / DemoPass123!, then POST /api/billing/checkout with {"plan":"starter","term":"monthly"} returns mock:true, applied:true.
  • Smoke pack: POST /api/billing/checkout with {"pack":"small"} returns mock:true, applied:true and adds 500 credits.

UI

  • /plans — Upgrade buttons call Checkout for self-serve plans.
  • /billing — Upgrade shortcuts + Buy AI credit packs + Manage subscription (Customer Portal).
  • Marketing /pricing CTAs stay register / sales; signed-in upgrades use /plans or /billing.

Verify

cd apps/api && go test ./internal/billing/ -count=1
cd apps/api && go build ./...
cd apps/web && npm run check