Stripe Subscriptions & Connect — Next Steps & External Setup

Everything you need to do outside the codebase to enable Phase 1 (paywall + 30-day trial + coupons, all revenue to platform) and Phase 2 (Connect / split payments).

This guide is written for someone who has never used the Stripe Dashboard before. Every screen, every button, every field is described. If you get lost, the URL of the page you should be on is shown like this: dashboard.stripe.com/....


Phase 1 — Enable Subscriptions, Paywall, Trial, Coupons

You will work in TEST MODE until everything is verified, then repeat the same steps in LIVE MODE. Stripe keeps test and live data completely separate — the toggle is in the top-right of the dashboard.

1.1 Create / log in to your Stripe account

  1. Go to https://stripe.com/ and click Sign in (top right). If you don't have an account yet, click Start now and create one. You'll need:
    • Your email address.
    • A password.
    • Your business / personal name (you can use a placeholder during test mode).
    • Once you want to activate live mode, Stripe will ask for additional verification (tax ID, bank account, etc.). Test mode works immediately.
  2. After signing in you land on https://dashboard.stripe.com/dashboard. This is the home / overview page.

  3. Top-right corner, look for a toggle that says "Test mode" with a small switch. Make sure it is ON (the switch is amber/orange when on). The URL changes to https://dashboard.stripe.com/test/... when in test mode.

⚠️ Everything below is done in test mode first. When you go to production, flip the toggle off and repeat steps 1.2 → 1.6 in live mode (the values are different — test keys start with sk_test_…, live keys start with sk_live_…).

1.2 Get your API keys (private / publishable)

This is what fills the stripe.private_key and stripe.public_key values in Rails credentials.

  1. In the left sidebar, click Developers (the </> icon near the bottom). If you don't see it, click your profile picture (top right) → Developers.
    • Direct URL: https://dashboard.stripe.com/test/apikeys
  2. You land on the API keys tab. You see two keys:

    Label in Stripe Looks like What we call it in Rails credentials
    Publishable key pk_test_51ABC… stripe.public_key
    Secret key sk_test_51ABC… (hidden — click "Reveal test key" to see it) stripe.private_key
  3. Copy the Publishable key (click the clipboard icon next to it).

  4. Click "Reveal test key" next to the Secret key, then copy it.

  5. Run this in your terminal:
    bin/rails credentials:edit
    

    Your editor opens with a YAML file. Paste / update:

    stripe:
      public_key: pk_test_51ABC...        # ← Publishable key (test)
      private_key: sk_test_51ABC...       # ← Secret key (test)
      signing_secret: whsec_...           # ← we'll get this in step 1.5
    

    Save and close the editor. Rails encrypts the file automatically.

🔒 Never commit the secret key to git in plaintext. It belongs in encrypted credentials (config/credentials.yml.enc) only.

1.3 Create the products (what people buy)

A product is the thing the customer subscribes to ("Lifehub Pro"). A price is how much they pay and how often (R$30/month, $6/month, R$300/year, $60/year). One product can have many prices — that's exactly how Stripe is designed.

You have two valid layouts:

Each Stripe Price uses currency_options to hold amounts in multiple currencies at once. So one Price ID covers both BRL and USD for the same interval — Stripe just picks the right amount based on the currency parameter you pass at checkout.

  • Pros: 2 Price IDs total (not 4). Less data to manage. Cleaner Stripe Dashboard.
  • Cons: Stripe's Dashboard UI for multi-currency Prices is a bit tucked away (it shows up when adding/editing a price under "Add additional currencies" or similar). Some teams find it less discoverable.

Option B — Single-currency Prices (4 distinct IDs)

Each Price has one currency baked in. To support BRL + USD across monthly + yearly, you need 4 Prices, all under the same product.

  • Pros: Each price is independently editable in the Stripe Dashboard.
  • Cons: 4 IDs to track. Settings.yml lists 4 unique entries.

Both layouts work with the same config/settings.yml shape — available_plans filters by user currency regardless. The differences:

  • Multi-currency: 2 IDs, each appears in two rows (one per currency).
  • Single-currency: 4 IDs, each appears in one row.

⚠️ Whichever option you pick, all prices belong to the SAME product (Lifehub Pro). Do not create separate products like "Lifehub Pro BRL" and "Lifehub Pro USD". Reasons:

  1. The Stripe Customer Portal only lets a user switch between prices that share a product. Two products = two disjoint switching worlds.
  2. Revenue reporting rolls up by product — split products means you manually sum "BRL + USD" every time.
  3. Coupons attach to products. One product = define a coupon once; it covers all currencies.
  4. Metadata, branding, future tiers all stay clean with one product.

Split products only make sense when the offering itself differs (different features / tax category / data residency), not when the only difference is currency.

  1. Left sidebar → Product catalog (sometimes labeled just Products). Direct URL: https://dashboard.stripe.com/test/products.

  2. Click the + Create product button (top right).

  3. Fill in the Product information card:
    • Name: Lifehub Pro (this is what customers see at checkout).
    • Description: something short like All Lifehub features, unlimited use. (optional but appears on the receipt).
    • Image: optional — upload your logo if you have one (shown at checkout).
    • Leave the rest at defaults.
  4. Scroll down to the Pricing card. The first price is created with the product.
    • Pricing model: Standard pricing.
    • Price: 30.00 and currency BRL.
    • Billing period: RecurringMonthly.
    • Click Save product (top right).
  5. You're now on the product detail page. Copy the Price ID for BRL Monthly — it looks like price_1Q… next to the price row.

Add a USD amount to the same Monthly Price:

  1. On the product detail page, click the monthly price (the one you just created).
  2. Look for "Add additional currencies" or "+ Add currency" on the price detail page (it appears under the existing currency row).
  3. Add USD with amount 6.00. Save.
  4. The Price ID stays the same — it now holds both BRL 30,00 and USD 6,00.

Repeat for the yearly price:

  1. Create the yearly price first: click + Add another price on the product page → 300.00 BRL, Recurring → Yearly, save.
  2. Open the yearly price → Add additional currenciesUSD 60.00.

After this, you have:

Stripe Price ID Currencies on that Price
price_xxx_Monthly BRL 30,00 + USD 6,00
price_xxx_Yearly BRL 300,00 + USD 60,00

5b. If using Option B (single-currency Prices)

Click + Add another price three more times on the same product:

# Currency Interval Amount Settings in Stripe
1 BRL Monthly 30.00 (already created in step 4)
2 BRL Yearly 300.00 Recurring → Yearly, currency BRL
3 USD Monthly 6.00 Recurring → Monthly, currency USD
4 USD Yearly 60.00 Recurring → Yearly, currency USD

After each one, copy the resulting Price ID.

✅ At this point you should have:

  • Option A: one product, two Price IDs, each holding both BRL and USD amounts.
  • Option B: one product, four Price IDs, each in one currency.

If you ended up with two products instead of one, archive the duplicate and re-do this step on the original.

1.4 Wire the Price IDs into the app

  1. Open config/settings.yml in your editor.

  2. The file has two sectionsshared: (used in development/test, also inherited by production) and production: (live-mode overrides). For local testing, update shared:. For production, update production:.

  3. Paste your Price IDs. The YAML always lists four rows (one per currency × interval), regardless of which Stripe layout you chose:

    Option A (multi-currency Prices) — the BRL and USD rows for the same interval share the same id:

    shared:
      plans:
        # ── BRL ────────────────────────────────────────────────────────────
        - id: price_1Q…MONTHLY      # ← monthly Price ID (covers BRL + USD)
          unit_amount: 3000          # cents (3000 = R$30,00)
          currency: BRL
          interval: month
        - id: price_1Q…YEARLY       # ← yearly Price ID (covers BRL + USD)
          unit_amount: 30000         # 30000 = R$300,00
          currency: BRL
          interval: year
        # ── USD ────────────────────────────────────────────────────────────
        - id: price_1Q…MONTHLY      # SAME ID as BRL monthly above
          unit_amount: 600           # 600 = $6.00
          currency: USD
          interval: month
        - id: price_1Q…YEARLY       # SAME ID as BRL yearly above
          unit_amount: 6000          # 6000 = $60.00
          currency: USD
          interval: year
    

    Option B (single-currency Prices) — four unique ids:

    shared:
      plans:
        - id: price_1Q…BRL_M
          unit_amount: 3000
          currency: BRL
          interval: month
        - id: price_1Q…BRL_Y
          unit_amount: 30000
          currency: BRL
          interval: year
        - id: price_1Q…USD_M
          unit_amount: 600
          currency: USD
          interval: month
        - id: price_1Q…USD_Y
          unit_amount: 6000
          currency: USD
          interval: year
    
  4. Save. Restart the server (bin/dev or bin/rails server).

1.4b How locale-based filtering works

Once the four entries are in place, the app filters them automatically based on the viewer:

Viewer state Currency shown
Signed-in user with finance_settings.currency = "USD" USD plans only
Signed-in user with finance_settings.currency = "BRL" (default for new pt-BR users) BRL plans only
Signed-in user with no currency preference + locale pt-BR BRL
Signed-in user with no currency preference + locale en USD
Guest viewer + locale pt-BR BRL
Guest viewer + locale en USD

This is implemented in app/helpers/subscription_helper.rb#available_plans. The /subscriptions page only renders plans matching the viewer's currency.

💡 Users can change their currency at /settings (Finance Settings → Currency). They'll then see the corresponding plan grid.

1.4c How the right currency gets charged (multi-currency Prices)

When the user clicks Subscribe:

  1. The pricing page builds a checkout URL like /subscriptions/checkout?price_id=price_xxx&currency=USD.
  2. SubscriptionsController#checkout reads current_user.finance_currency and passes it to Billing::CheckoutOptions.
  3. Billing::CheckoutOptions adds currency: "usd" (or "brl") to the Stripe Checkout Session payload.
  4. Stripe sees the Price has currency_options and charges the right amount.

If you went with Option B (4 distinct IDs), the currency parameter is harmless — it must just match the Price's currency, which it will.

1.5 Configure the Customer Portal

This is the Stripe-hosted page where customers manage their subscription (change plan, cancel, update card, see invoices). We use it for the "Manage billing" button.

  1. Left sidebar → Settings (gear icon, bottom-left). Direct URL: https://dashboard.stripe.com/test/settings/billing/portal.

  2. Click BillingCustomer portal. If it asks you to "Activate", click Activate.

  3. On the Customer portal settings page, work through each section:

    Functionality

    • Toggle ON: "Allow customers to update their payment methods".
    • Toggle ON: "Allow customers to update billing addresses".
    • Cancellations: toggle ON "Allow customers to cancel subscriptions". Set the dropdown to At end of billing period (so they get the rest of what they paid for).
    • Subscription updates: toggle ON "Allow customers to switch plans". Click + Add product and select Lifehub Pro with both prices.
    • Invoice history: toggle ON.

    Branding (optional but recommended)

    • Upload a logo / icon.
    • Set accent color to match your app.

    Business information

    • Add your support email (e.g. [email protected]).
    • Add your privacy policy URL: https://applifehub.com/privacy.
    • Add your terms URL: https://applifehub.com/terms.

    Default return URL

    • Set to https://applifehub.com/subscriptions (in test mode you can use http://localhost:3000/subscriptions for local testing).
  4. Click Save at the top.

1.6 Set up Webhooks

A webhook is a URL that Stripe calls when something happens (e.g. subscription created, payment succeeded). The Pay gem handles all of these automatically — you just need to tell Stripe where to send them.

  1. Left sidebar → DevelopersWebhooks. Direct URL: https://dashboard.stripe.com/test/webhooks.

  2. Click + Add an endpoint (or + Add destination in newer dashboard versions).

  3. Endpoint URL:
    • For production: https://applifehub.com/pay/webhooks/stripe
    • For local development: you need a tunnel (Stripe CLI). See section 1.7 below — for now, just create the production endpoint.
  4. Description: Lifehub subscriptions (test).

  5. Listen to: select Events on your account (NOT "Events on Connected accounts" — that's Phase 2).

  6. Select events. Click + Select events and add these (search by name):
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • customer.subscription.trial_will_end
    • invoice.payment_succeeded
    • invoice.payment_failed
    • customer.updated
    • payment_method.attached
  7. Click Add events → then Add endpoint at the bottom.

  8. After creating, you're on the endpoint's detail page. Find "Signing secret" (it's near the top). Click Reveal → copy the value (whsec_…).

  9. Paste it into Rails credentials:
    bin/rails credentials:edit
    
    stripe:
      public_key: pk_test_…
      private_key: sk_test_…
      signing_secret: whsec_…   # ← paste it here
    

1.7 (Optional, for local dev) Set up Stripe CLI to receive webhooks on localhost

If you want to test the full subscription flow on your own machine:

  1. Install the Stripe CLI: https://stripe.com/docs/stripe-cli (one-liner depending on your OS).

  2. In your terminal:
    stripe login              # opens browser, authenticate
    stripe listen --forward-to localhost:3000/pay/webhooks/stripe
    
  3. The CLI prints a webhook signing secret like whsec_xxx. Use this secret (not the dashboard one) when testing locally:
    bin/rails credentials:edit --environment development
    
    stripe:
      signing_secret: whsec_xxx   # CLI secret, dev-only
    

1.8 Create the 2-month free trial coupon

This is the coupon code people redeem to get a 2-month free trial (instead of the default 30 days).

  1. Left sidebar → Product catalogCoupons (top tab). Direct URL: https://dashboard.stripe.com/test/coupons.

  2. Click + New coupon.

  3. Fill in:
    • Type: select Percentage discount.
    • Percentage off: 100.
    • Duration: select Multiple months.
    • Number of months: 2.
    • ID: leave blank (Stripe will generate one) OR set a memorable ID like TWO_MONTHS_FREE.
    • Name (internal): 2-month free trial.
  4. Scroll down to Redemption (optional):
    • Max redemptions — leave empty for unlimited, or set a cap.
    • Redeem by — leave empty for "no expiry", or set a deadline.
  5. Click Create coupon.

  6. Now you need to attach Promotion Codes to the coupon. Promotion codes are the things customers actually type (e.g. LAUNCH60).
    • On the coupon detail page, click + Create promotion code.
    • Code: LAUNCH60 (or anything you want — case-insensitive at checkout).
    • Customer: leave blank for "anyone can use it".
    • First-time order: optional — if you only want new customers, toggle it on.
    • Limit redemptions: optional.
    • Limit by date: optional.
    • Click Create promotion code.
  7. Repeat step 6 to create as many codes as you need (FRIEND60, EARLYBIRD60, …). All point at the same 2-month coupon.

1.9 How customers redeem the coupon

Two ways:

A) Auto-apply via URL (most reliable) — share a link like:

https://applifehub.com/subscriptions?promo=LAUNCH60

The pricing page shows a "Promo code LAUNCH60 will be applied at checkout" banner. Clicking Subscribe opens Stripe Checkout with the discount already applied.

B) Type at checkout — on the Stripe Checkout page there's an "Add promotion code" link. Customers paste the code there. This works because allow_promotion_codes: true is on the checkout session.

1.10 Flip the paywall ON

Until you do this, the app behaves exactly like today (no enforcement). When you're ready:

bin/rails credentials:edit
billing:
  paywall_enabled: true

Restart the server. Now:

  • Unsubscribed non-admin users are redirected to /subscriptions when hitting any feature page.
  • Trialing users keep access.
  • Admins (user.admin = true) always have access.

💡 To test the paywall locally before turning it on for everyone, temporarily set billing.paywall_enabled: true in development credentials and try it as a non-admin user.

1.11 Verify everything works (test mode)

  1. As a brand-new user (not admin, never subscribed before):
    • Go to /subscriptions.
    • You see two plans (monthly / yearly) and the "30-day free trial" badge.
    • Click Subscribe on monthly.
    • You land on Stripe Checkout. Look for "Free trial for 30 days. Cancel anytime." in the order summary.
    • Use the test card 4242 4242 4242 4242, any future expiry, any CVC, any ZIP.
    • Complete checkout.
    • You're redirected back to /subscriptions and see "Active Subscription".
    • In the Stripe Dashboard → Customers, find your test customer. Their subscription should show status: trialing with trial_end ≈ 30 days from now.
  2. Test the 2-month coupon:
    • As another new user, go to /subscriptions?promo=LAUNCH60.
    • You see the "Promo code LAUNCH60 will be applied at checkout" banner.
    • Click Subscribe.
    • On Stripe Checkout, the order summary shows a 100% discount applied for the first 2 months.
    • Complete with 4242 4242 4242 4242.
    • Stripe Dashboard → Customer → Invoice should show $0.00 for the first 2 months, then full price.
  3. Test the paywall (after enabling in 1.10):
    • As a non-admin user with NO subscription, go to /dashboard. You should be redirected to /subscriptions.
    • As the same user, complete a checkout → go to /dashboard. Should work.
    • As an admin user with no subscription, go to /dashboard. Should work (admins bypass).
  4. Test billing portal:
    • As a subscribed user, click Manage Billing on /subscriptions. You should land on the Stripe-hosted portal.

1.12 Phase 1 Deployment Checklist

  • [ x] Products + Prices created in Stripe live mode; Price IDs in config/settings.yml under production:.
  • [ x] Live-mode API keys (sk_live_…, pk_live_…) in Rails credentials.
  • [ x] Customer Portal configured + activated in live mode.
  • [ x] Webhook endpoint subscribed to Phase 1 events; live-mode signing secret in credentials.
  • At least one live coupon (LAUNCH60 or whatever you named it) created.
  • [ x] billing.paywall_enabled: true in Rails credentials (only when ready to enforce).
  • Smoke test in production: subscribe with a real card, see trial start.
  • Smoke test in production: redeem promo URL, see 100% discount.
  • Smoke test (paywall ON): unsubscribed non-admin redirected to /subscriptions.

Phase 2 — Stripe Connect (Split Payments to Partner Affiliates)

Do this only when you want to add partner / affiliate revenue share. Phase 1 is fully usable without it.

2.1 Enable Connect in Stripe Dashboard

  1. Left sidebar → Connect (a paper-airplane icon, near the top of the sidebar). Direct URL: https://dashboard.stripe.com/test/connect/overview.

  2. If you've never used Connect before, you see a "Get started with Connect" splash:
    • Platform name: Lifehub.
    • Business URL: https://applifehub.com.
    • Industry: select the closest match (e.g. "Software-as-a-service").
    • Country of operation: Brazil (or wherever).
    • Click Get started.
  3. Connect → Settings (sidebar item under Connect). Direct URL: https://dashboard.stripe.com/test/settings/connect.

    • Account types: enable Express. (Stripe hosts the onboarding form so you don't have to build it.)
    • Branding: upload the same logo / accent color you used for the customer portal.
    • Allowed countries: check the countries your affiliates can be in.
  4. Connect → Payouts:
    • Payout schedule: Daily, Weekly, or Monthly — your call. Most platforms default to Weekly to reduce admin noise.

2.2 Add Connect events to the existing webhook

  1. Dashboard → DevelopersWebhooks → click the endpoint you created in step 1.6.

  2. Click Update details (or the pencil icon next to "Events listened to").

  3. Click + Select events and add:
    • account.updatedrequired (keeps affiliates' onboarding status in sync)
    • account.application.deauthorized (recommended — fires when an affiliate disconnects)
  4. Click Update endpoint.

The signing secret stays the same — no credentials change needed.

2.3 Revenue split

  • The platform takes 70% by default, affiliate gets 30%.
  • Default value lives in app/models/affiliate/checkout_splitter.rb (DEFAULT_PLATFORM_FEE_PERCENT = 70).
  • Override per subscription via the admin UI: /admin/affiliates/:id → assign a subscription → set the % field.

2.4 Create your first affiliate (test mode)

  1. Go to /admin/affiliates+ New Affiliate.
  2. Select a user, give them a display name, click Create.
  3. Click Start Onboarding on the affiliate's page. You're redirected to Stripe's hosted Express onboarding.
  4. Use Stripe's test onboarding data: Acuity Inc., 000-00-0000 (test SSN), 123 Main St, etc. Stripe accepts test values when in test mode.
  5. After completing, you're redirected back to Lifehub. The affiliate's onboarding_completed should flip to true (the account.updated webhook fires immediately).
  6. Open /subscriptions/checkout?price_id=PRICE&affiliate_id=ID as a different user and complete checkout. On the Stripe payment detail, you'll see "Application fee" and "Transferred to acct_…".

2.5 Phase 2 Deployment Checklist

  • Connect enabled in live mode.
  • Express account type configured + branded in live mode.
  • Live webhook endpoint subscribed to account.updated (+ account.application.deauthorized).
  • At least one real affiliate fully onboarded in live mode.
  • Test checkout with ?affiliate_id= shows application_fee in the Stripe dashboard.
  • Test checkout without affiliate_id shows no application_fee (Phase 1 behavior preserved).
  • 70/30 split math verified on a real payment.

Reference: Stripe Test Cards

When testing, never use a real card. Use Stripe's test cards:

Scenario Card number Expiry / CVC / ZIP
Successful payment 4242 4242 4242 4242 any future date / any 3-digit CVC / any ZIP
Payment requires authentication 4000 0025 0000 3155 same
Payment is declined 4000 0000 0000 0002 same
Trial setup w/o payment method 4000 0000 0000 9995 same

Full list: https://stripe.com/docs/testing#cards.


Reference: Common Stripe Dashboard URLs

Page URL (test mode)
Home https://dashboard.stripe.com/test/dashboard
API keys https://dashboard.stripe.com/test/apikeys
Products https://dashboard.stripe.com/test/products
Coupons https://dashboard.stripe.com/test/coupons
Customers https://dashboard.stripe.com/test/customers
Subscriptions https://dashboard.stripe.com/test/subscriptions
Webhooks https://dashboard.stripe.com/test/webhooks
Customer Portal https://dashboard.stripe.com/test/settings/billing/portal
Connect overview https://dashboard.stripe.com/test/connect/overview
Connect settings https://dashboard.stripe.com/test/settings/connect

Replace /test/ with nothing (e.g. https://dashboard.stripe.com/products) to access the same page in live mode.


  • plan.md — strategic plan (Phase 1 + Phase 2)
  • implementation.md — code-level changes
  • separate-tasks.md — deferred coupon-admin-UI work + step-by-step setup for those tasks