Stripe Subscriptions & Connect — Plan
Complexity: High Scope: Two phases — (1) standard subscription paywall + 30-day trial + coupons → all revenue goes to the platform; (2) Stripe Connect Express with split payments via Pay gem for partner revenue share.
Decisions Log (auto-iterated; review later)
These are decisions I made while iterating without you in front of the computer. Adjust as needed:
- Phase order. Phase 1 (paywall + trial + coupons, single recipient = platform owner) ships first. Phase 2 (Connect / split payments to partners) is layered on top later. The Connect code already exists and only activates when an
affiliate_idis attached to a checkout — it stays inert during Phase 1. - Paywall scope. Sitewide via
ApplicationController#require_subscription, gated byRails.application.credentials.dig(:billing, :paywall_enabled)(default OFF so dev/test/existing users are unaffected). Admins are always exempt. Trialing users are considered subscribed (Pay'ssubscribed?returns true while in trial). - Trial. 30-day free trial for new customers, applied via Stripe Checkout
subscription_data: { trial_period_days: 30 }. No code change to the customer flow — Stripe enforces it. - Coupon for 2-month free trial. Implemented with Stripe Promotion Codes (already enabled via
allow_promotion_codes: true). For 2-month free trial codes, create a Stripe Coupon withduration: repeating, duration_in_months: 2, percent_off: 100and attach Promotion Codes (e.g.LAUNCH60). Users either type the code at checkout OR open/subscriptions/checkout?price_id=...&promo=LAUNCH60to auto-apply. Admin UI for managing codes is inseparate-tasks.md(deferred). - Revenue split percentage. Kept at
application_fee_percent: 70(platform 70 / partner 30). Per-subscription override viaSubscriptionAffiliate#application_fee_percent. - Existing Connect code stays. No removal. It only activates when
find_affiliate_for_checkoutreturns an affiliate.
Phase 1 — Standard Subscriptions + Paywall + Trial + Coupons
All revenue goes to the platform Stripe account. No Connect split.
External Setup (Stripe Dashboard)
- Products & Prices
- Create one Product (e.g. "Lifehub Pro").
- Create two Prices: monthly (
price_xxx_month) and yearly (price_xxx_year). - Copy the price IDs into
config/settings.ymlunder the appropriate environment (production.plans).
- Customer Portal
- Enable the Customer Portal:
Dashboard → Settings → Billing → Customer portal. - Allow customers to: update payment method, cancel subscription, switch plans, view invoices.
- Set the
return_urltohttps://applifehub.com/subscriptions.
- Enable the Customer Portal:
- Webhooks
- Endpoint:
https://applifehub.com/pay/webhooks/stripe(Pay gem default). - Subscribe to:
checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,customer.subscription.trial_will_end,invoice.payment_succeeded,invoice.payment_failed. - Copy the signing secret into Rails credentials (
stripe.signing_secret).
- Endpoint:
- Coupons (for the 2-month free trial codes)
Dashboard → Products → Coupons → New.- Type:
Percent off, value:100%, duration:Repeating, duration in months:2. - Create a Promotion Code for each campaign (
LAUNCH60,FRIEND60, …).
Code Changes
SubscriptionsController#checkout— add to the existing checkout options:subscription_data: { trial_period_days: 30 }, discounts: [{ promotion_code: stripe_promo_code_id }] # only if params[:promo] is present and resolvesSkip the trial for customers who have ever had a paid subscription (Stripe enforces "one trial per customer" automatically when
trial_period_daysis set, but we also gate based oncurrent_user.payment_processor.subscriptions.exists?).ApplicationController— promote the existingrequire_subscriptionhelper into a sitewidebefore_action:before_action :require_subscription, if: :paywall_enabled?, unless: :paywall_exempt?Exempt actions: anything under
SubscriptionsController,Admin::*, Devise,Users::*,FinanceSettingsController,StaticController,NotificationsController, sign-out flows. Admins are exempt.-
Paywall feature flag —
Rails.application.credentials.dig(:billing, :paywall_enabled). Default OFF. When OFF, the app behaves exactly as today. -
Promo code resolver —
Affiliate::PromoResolver(rename toBilling::PromoResolver) looks upStripe::PromotionCode.list(code: params[:promo])and returns the promotion_code ID for the checkout session. - Tests
SubscriptionsControllerTest: checkout creates a session withtrial_period_days: 30, with/without?promo=, paywall-on redirects, paywall-off allows, admin bypass, billing portal redirect.ApplicationControllerTest: paywall gate.- Model test for
Billing::PromoResolver.
Verification (Phase 1)
- Sign up new user → checkout → see "30-day free trial" on Stripe Checkout page.
- Subscribe with a
LAUNCH60promotion code → 100% off for 2 months on the invoice preview. - After paywall enabled in credentials: unsubscribed non-admin hitting
/dashboardis redirected to/subscriptions. - Trial user can access the dashboard.
- Admin user can access the dashboard regardless.
- Cancel via billing portal → access continues until
current_period_end, then paywall kicks in.
Phase 2 — Stripe Connect Express (Split Payments)
Most of this is already implemented. Listed here for completeness; activate when partner deals are in place.
External Setup
- Stripe Dashboard → Connect → Complete platform profile:
- Business info, branding, description.
- Platform type: select appropriately for your use case.
- Connect → Settings:
- Account type: Express (recommended — Stripe hosts onboarding).
- Set branding (logo, colors, accent color).
- Configure payout schedule.
- Webhooks → Add events to existing endpoint:
account.updated,account.application.deauthorized.
- Test mode: Use test keys first. Create test connected accounts via Stripe CLI or dashboard.
- Fee flow:
- Customer pays $19 → Stripe fee (~$0.85) → Platform gets ~$12.70 (70%) → Affiliate gets ~$5.45 (30%).
Code Changes (already shipped)
Affiliatemodel +Affiliate::StripeOnboarder+Affiliate::CheckoutSplitter+Affiliate::WebhookHandler.SubscriptionAffiliatejoin table (Pay::Subscription ↔ Affiliate, with per-rowapplication_fee_percent).AffiliatesController(public onboarding) +Admin::AffiliatesController+Admin::SubscriptionAffiliatesController.SubscriptionsController#checkoutmerges inAffiliate::CheckoutSplitter#checkout_optionswhenparams[:affiliate_id]is present.
Verification (Phase 2)
- Create test affiliate → complete Stripe Express onboarding (test mode).
- Create subscription with
?affiliate_id=→ verifyapplication_feein Stripe dashboard. - Create subscription without affiliate → no split payment (Phase 1 behavior preserved).
- Check affiliate's connected account shows incoming transfer.
- Verify 70/30 math in Stripe payment detail.
- Test webhook:
account.updated→ affiliate marked as onboarded. - Run
bin/ci.