Motivating case: a subscriber wrote in needing to change their billing card (support: HS 3403436283). The settings page offers cancel-renewal and cancel-immediately, but no way to update a payment method — so the card-change path today is a cancellation dance, and "End access now? This cannot be undone." reads as a threat to their history even though it isn't one. From the reply that resolved it: "I think you just inspired a new Yours feature — this kind of thing should be easier."
The feature
A "Manage billing" button on settings, backed by Stripe's customer billing portal:
# session-derived, request-scoped, nothing stored — same identity law as everything else
portal = Stripe::BillingPortal::Session.create(
customer: current_resonance.stripe_customer_id,
return_url: settings_url,
)
redirect_to portal.url, allow_other_host: true, status: :see_other
This composes cleanly with the topology: the server can only mint a portal session while the person is signed in (their google_id arrives with the request and unlocks stripe_customer_id), so the portal is reachable by exactly the one person it's about — Stripe's representation of our representation of this user, behind their own key.
What it solves, by subscription state
- Active, renewing: update card → future renewals ride the new card. The whole support class disappears.
- Active,
cancel_at_period_end (the motivating case's exact state): the portal natively offers "Renew subscription" for scheduled cancellations — add new card + un-schedule the cancellation = same subscription continues uninterrupted. No cancellation, no access gap, no scary buttons.
- Fully canceled: Stripe cannot reactivate; the app's existing subscribe-fresh flow remains the path (and already works).
Config lens (dashboard toggles — the portal should be the card-drawer, not a second front door)
- ON: payment method update, invoice history (harmless, maybe nice)
- OFF: plan/tier switching — Yours' tier-change ritual (cancel renewal → optionally release → resubscribe at chosen tier) is deliberate product grammar and should stay the one door
- OFF: portal cancellation — the app's own cancel flow has its own carefully-worded affect; two cancel doors with different words would split the voice
Verify in test mode before shipping
- Checkout-created subscriptions pin
default_payment_method to the card used at checkout — confirm the portal's update-card flow updates the subscription's payment method (or offers to), not just the customer default, or renewals keep riding the old card.
- Whether the "Renew subscription" affordance for
cancel_at_period_end subs appears independent of the portal-cancellation feature toggle.
Dovetail
The analytics/funnel work (GTM/Meta measurement) already brings Stripe webhooks + checkout-session touches onto the bench; this rides the same neighborhood and could share a PR cycle.
🤖 Written with Claude Code
Motivating case: a subscriber wrote in needing to change their billing card (support: HS 3403436283). The settings page offers cancel-renewal and cancel-immediately, but no way to update a payment method — so the card-change path today is a cancellation dance, and "End access now? This cannot be undone." reads as a threat to their history even though it isn't one. From the reply that resolved it: "I think you just inspired a new Yours feature — this kind of thing should be easier."
The feature
A "Manage billing" button on settings, backed by Stripe's customer billing portal:
This composes cleanly with the topology: the server can only mint a portal session while the person is signed in (their google_id arrives with the request and unlocks
stripe_customer_id), so the portal is reachable by exactly the one person it's about — Stripe's representation of our representation of this user, behind their own key.What it solves, by subscription state
cancel_at_period_end(the motivating case's exact state): the portal natively offers "Renew subscription" for scheduled cancellations — add new card + un-schedule the cancellation = same subscription continues uninterrupted. No cancellation, no access gap, no scary buttons.Config lens (dashboard toggles — the portal should be the card-drawer, not a second front door)
Verify in test mode before shipping
default_payment_methodto the card used at checkout — confirm the portal's update-card flow updates the subscription's payment method (or offers to), not just the customer default, or renewals keep riding the old card.cancel_at_period_endsubs appears independent of the portal-cancellation feature toggle.Dovetail
The analytics/funnel work (GTM/Meta measurement) already brings Stripe webhooks + checkout-session touches onto the bench; this rides the same neighborhood and could share a PR cycle.
🤖 Written with Claude Code