Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
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/checkoutwith a plan immediately assigns the plan, grants monthly credits, and redirects to/billing?checkout=success&mock=1.POST /api/billing/checkoutwith a pack immediately adds credits and redirects withpack+creditsquery params.POST /api/billing/portalreturns a local/billing?portal=mocklink.POST /api/webhooks/stripeaccepts 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 byevent.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
- Create a Product per plan (Starter, Plus, Growth, Business, Scale) with monthly + yearly recurring Prices.
- Create a Product (or one Product with four Prices) for AI credit packs — Prices must be one-time (not recurring).
- Paste each Price ID into
/admin/settings(stripe.price.*) — or optionalSTRIPE_PRICE_*env fallback. - Enable Customer Portal (Settings → Billing → Customer portal): allow cancel / update payment method.
- Webhook endpoint (API):
https://<your-api-host>/api/webhooks/stripe- Events:
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted
- Events:
- Local forwarding:
stripe listen --forward-to localhost:8080/api/webhooks/stripe
Paste the printed whsec_… into /admin/settings → stripe.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) createsstripe_webhook_eventspluscompanies.stripe_customer_id/company_plans.stripe_*. - For mock Checkout smoke: set
stripe.mock=truein/admin/settings(orSTRIPE_MOCK=truein root.envas fallback). Restart API only if you changed process env. DB migrate alone does not require restart. - Smoke plan: login
demo@descrybe.local/DemoPass123!, thenPOST /api/billing/checkoutwith{"plan":"starter","term":"monthly"}returnsmock:true,applied:true. - Smoke pack:
POST /api/billing/checkoutwith{"pack":"small"}returnsmock:true,applied:trueand 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
/pricingCTAs stay register / sales; signed-in upgrades use/plansor/billing.
Verify
cd apps/api && go test ./internal/billing/ -count=1
cd apps/api && go build ./...
cd apps/web && npm run check