ReviewOS

stacks/reportshq

Billing: Free, Hobby, and Pro via Stripe

#17
Open chris opened this 8 days ago ·
8 days ago

Context

Three tiers via Stripe through @stacksjs/payments (read the stacks-payments skill): Free is implicit (no active subscription - loghq's model), Hobby and Pro are Stripe products defined in config/saas.ts and resolved by lookup key. Self-hosted installs leave STRIPE_* unset and billing stays inert.

Reference: loghq config/{payment,saas}.ts, app/Actions/Payment/CreateCheckoutAction.ts, app/Actions/StripeWebhook.ts, app/Actions/MeAction.ts, resources/views/pricing.stx.

Proposed plan matrix (final numbers are a product decision at implementation, keep the keys)

FreeHobby $9/mo, $90/yrPro $29/mo, $290/yr
Projects13Unlimited
Events/month100k1M10M
Reports3UnlimitedUnlimited
Retention30 days12 months3 years
Members/project25Unlimited
Scheduled emails-DailyHourly-capable
ExportsCSVCSV + XLSXCSV + XLSX
Share linksBrandedBrandedUnbranded + embeds

Lookup keys: reportshq_hobby_monthly, reportshq_hobby_yearly, reportshq_pro_monthly, reportshq_pro_yearly.

Scope

  • config/saas.ts products/pricing as above; config/payment.ts wired to env-driven Stripe keys.
  • Checkout: Payment.subscriptionCheckout() by lookup key from the pricing page (useMutation pattern like loghq); success/cancel routes; customer auto-created on first checkout (getOrCreateCustomer).
  • Webhook action syncing the local subscriptions table off users.stripe_id (port loghq's StripeWebhook.ts, extend for plan-change and payment-failure events); idempotent handling.
  • Plan resolution helper: one function answering "which tier is this user" from Payment.hasActiveSubscription + price lookup key, cached per request; consumed by #18 and MeAction-style session payloads.
  • Upgrade/downgrade via Payment.changeSubscription (lookup key swap), cancel at period end; proration semantics documented.
  • Billing settings page: current plan, usage summary (from #18), invoices list, payment method management, cancel/resume - designed per #3, not a raw Stripe portal dump (Stripe-hosted portal link is fine as the escape hatch).
  • Stripe test-mode fixtures + feature tests: checkout session creation, webhook lifecycle (created/updated/canceled/payment_failed), tier resolution, inert-when-unconfigured.

Acceptance

  • In Stripe test mode: subscribe to Hobby, webhook lands, tier resolves Hobby, upgrade to Pro, cancel, tier decays to Free at period end (simulated clock). With STRIPE_* unset, the app runs with everything Free and no errors.

Depends on #4. Blocks #18; feeds pricing page #19. Part of #1 (M6 Monetization). Ground rules: see #1.

Sign in to comment on this issue.