Answers · Billing

How do I run a launch price that expires on a date?

Set STRIPE_PRICE_KIT_LAUNCH and KIT_LAUNCH_PRICE_ENDS_AT. The app decides which price id to use from the date, and the same decision drives the number shown on the pricing page and the structured data. One source, so the price a customer is charged can never disagree with the price they were shown.

The two variables

Variable What it holds
STRIPE_PRICE_KIT_LAUNCH The discounted price id, used while the window is open.
KIT_LAUNCH_PRICE_ENDS_AT An ISO date. After it, the standard price id is used.

STRIPE_PRICE_KIT is the standard price and is what the app falls back to.

bin/rails runner 'puts KitPrice.current.cents'

That prints the price the app will actually charge right now, which is the number worth checking before you announce anything.

Why it is derived rather than written twice

The obvious version of this feature is a price id in the config and a number typed into the pricing page. They drift the first time someone edits one and not the other, and the failure is a customer charged one amount after being shown another, which is a refund and an apology at minimum.

Here the displayed price, the checkout price and the offers.price in the page's structured data all read the same object. A spec asserts the structured data matches, so the three cannot separate without failing bin/check.

Expiry is evaluated per request, not cached at boot

The window closes on its own at the date you set. There is no deploy to remember and no job to schedule. That also means moving the date is a configuration change and a restart, not a release.

Do not delete the old price in Stripe

Archive it instead. Existing customers on a recurring launch price keep that price, and deleting it breaks their renewals. Archiving hides it from new checkouts and leaves the existing ones alone.

Test the boundary

Set KIT_LAUNCH_PRICE_ENDS_AT to yesterday in development and reload the pricing page. It should show the standard price, and checkout should use the standard id. Then set it to tomorrow and confirm both switch back. Two minutes, and it is the only way to know the boundary works before the night it matters.

Related questions

Can I charge a one-time price instead of a subscription?

Yes. Create a one-time price in Stripe, use payment mode rather than subscription mode in the checkout session, and change what Entitlement.for returns so a completed purchase entitles the account permanently. The gate on each controller does not change, because RequireEntitlement never knew what it was checking.

Do I have to handle failed payments and refunds myself?

No. Stripe retries failed charges, emails customers, and processes refunds through its dashboard. Your app subscribes to invoice.payment_failed, invoice.payment_succeeded, charge.refunded and customer.subscription.deleted, and keeps one local Subscription row in sync. That is the whole of your responsibility.

How do I change what a paid plan unlocks in my app?

To gate a feature, add include RequireEntitlement to its controller. To ungate it, delete that line. To change what counts as paid at all, edit the single strategy line in app/models/entitlement.rb. Those are two different decisions and they live in two different places on purpose.